Showing posts with label c#. Show all posts
Showing posts with label c#. Show all posts

Sunday, 3 May 2026

Inspecting String Length Constraints in EF Core

Inspecting String Length Constraints in EF Core

Entity Framework Core exposes a rich representation of your database schema through its model metadata. This metadata reflects not only attributes on your entities, but also Fluent API configuration, conventions, and provider‑specific decisions.

In this post, we’ll look at a practical way to extract string length constraints from the EF Core model using:

  • C#
  • EF Core 8
  • LINQPad 8

The goal is to surface, programmatically:

  • 🧩 Entity name
  • 🧱 Property name
  • 🏷 Database column name
  • πŸ“ Configured maximum string length (if any)

All of this is done without reflection and without querying database system tables.


Why Use EF Core Metadata?

It’s tempting to reach for reflection and scan attributes like [MaxLength] or [StringLength]. However, reflection only tells you what was declared — not what EF Core ultimately built.

EF Core metadata reflects the resolved model, which may include:

  • ✅ Fluent API overrides
  • ✅ Convention‑based lengths
  • ✅ Provider defaults
  • ✅ Shadow properties
  • ✅ Mappings that never existed as CLR attributes

If EF Core generated it, the metadata knows about it.


Working in LINQPad 8

When you select an EF Core connection in LINQPad, LINQPad instantiates the DbContext for you. Within the query, that context instance is available via this.

This makes LINQPad an excellent environment for exploratory tooling and schema inspection.

Entry point


void Main()
{    
    var dbContext = (DbContext)this;
    
    Console.WriteLine("----------------- GET ALL THE ENTITY TYPE FIELDS/COLUMNS WITH A STRING LENGTH CONSTRAINT------------");
    
    var stringlengthConstrainedField = dbContext.GetAllStringPropertyLengths();

    foreach (var constrainedField in stringlengthConstrainedField)
    {
        Console.WriteLine($"{constrainedField.EntityName} {constrainedField.PropertyName} {constrainedField.ColumnName} {constrainedField.MaxLength}");
    }
    
    
    Console.WriteLine("-------------- GET SPECIFIC ENTITY TYPE's FIELDS/COLUMS WITH A STRING LENGTH CONSTRAINT---------------");
    
    var constrainedFieldsInSpecificTable = dbContext.GetStringPropertyLengths<Users>();

    foreach (var constrainedField in constrainedFieldsInSpecificTable)
    {
        Console.WriteLine($"{constrainedField.EntityName} {constrainedField.PropertyName} {constrainedField.ColumnName} {constrainedField.MaxLength}");
    }
}

πŸ”Ž LINQPad’s immediacy makes this kind of inspection extremely efficient.


DbContext Extension Methods

To keep the querying logic reusable and unobtrusive, the functionality is implemented as DbContext extension methods.

Enumerating all constrained string properties


public static class DbContextExtensions
{
    public static IEnumerable<(string EntityName, string PropertyName, string ColumnName, int? MaxLength)>
        GetAllStringPropertyLengths(this DbContext context, Type? specificEntityType = null)
    {
        var model = context.Model;
        
        IEntityType? specificEntity = null;
        
        if (specificEntityType != null)
        {
            specificEntity = model.FindEntityType(specificEntityType);
        }
        
        if (specificEntityType != null && specificEntity == null)
        {
            yield break;
        }

        var entityTypes = specificEntity == null
            ? model.GetEntityTypes()
            : new[] { specificEntity };
        
        
        foreach (var entityType in entityTypes)
        {
            var clrType = entityType.ClrType;

            foreach (var property in entityType.GetProperties())
            {
                if (property.ClrType != typeof(string))
                    continue;

                var maxLength = property.GetMaxLength();
                if (maxLength == null)
                    continue;

                var propertyName = property.Name;
                var columnName = property.GetColumnName();

                yield return (clrType.Name, propertyName, columnName, maxLength);
            }
        }
    }

🧠 This method reports what EF Core actually enforces, not merely what was declared.


Strongly‑typed convenience overload


    public static IEnumerable<(string EntityName, string PropertyName, string ColumnName, int? MaxLength)>
        GetStringPropertyLengths<TModel>(this DbContext context)
    {
        var stringPropertyLenghtsForType =
            GetAllStringPropertyLengths(context, typeof(TModel));

        return stringPropertyLenghtsForType;
    }
}

🎯 This keeps call‑sites expressive without duplicating logic.


Where This Is Useful

  • ✅ Generating client‑side validation rules
  • ✅ Auditing schema constraints in large models
  • ✅ Verifying legacy databases after reverse engineering
  • ✅ Debugging unexpected truncation or validation errors
  • ✅ Building internal tooling around EF Core models

Because it relies on EF Core metadata, it remains accurate across migrations and configuration changes.


Closing Thoughts

EF Core already builds a comprehensive semantic model of your database schema. Exposing and inspecting that model directly is often simpler—and more reliable— than round‑tripping through reflection or database metadata tables.

LINQPad provides a particularly effective environment for this kind of work: quick, focused, and transparent.

πŸ“Œ If you’re already using EF Core, you likely don’t need more tooling — you just need to look at the right layer.

Tuesday, 31 March 2026

Generic EqualityComparer for classes in C#

GenericEqualityComparer

Creating support for Equality comparison of classes in C# can become repititive. In this article, we will look at a Generic equality comparer that can be used for classes to do equality comparison. Please note that we are meaning here value comparison. Structs and records support via built-in functionality such a value equality comparison. For classes, it depends on what you mean by equality comparison. Usually it means the public properties, but additional state such as private properties and fields can also be considered.

Github repo with this source code

https://github.com/toreaurstadboss/GenericEqualityComparer

A reflection-based IEqualityComparer<T> that compares two objects by their member values instead of by reference. Useful for plain C# classes that don't override Equals and GetHashCode themselves.

1 — The problem it solves

In C#, class instances are compared by reference by default. Two objects with identical data are not equal unless they are the same object in memory.

var car1 = new Car { Make = "Toyota", Model = "Camry", Year = 2020 };
var car2 = new Car { Make = "Toyota", Model = "Camry", Year = 2020 };

Console.WriteLine(car1 == car2);        // False — different object references
Console.WriteLine(car1.Equals(car2));   // False — same reason

GenericEqualityComparer<T> solves this without touching the class itself. It uses reflection to compare each property (and optionally each field) value by value. But it also uses member expressions compiled into delegates to provide fast member value lookups. This makes the generic equality comparer used here possible to use inside collections with many items.

Lets first look at the GenericEqualityComparer source code. It is a generic class.

GenericEqualityComparer.cs


using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Reflection;

namespace GenericEqualityComparer.Lib
{

    /// <summary>
    /// A reflection-based <see cref="IEqualityComparer{T}"/> that compares instances of
    /// <typeparamref name="T"/> by their members rather than by reference.
    /// This is intended for use with classes that do not implement their own value-based equality semantics, and is not recommended for performance-sensitive scenarios.
    /// Types such as structs and records already have built-in value equality semantics and should not require this comparer.
    /// </summary>
    /// <typeparam name="T">The type to compare. Must be a reference type.</typeparam>
    /// <remarks>
    /// By default only public instance properties are compared. Pass the constructor flags to
    /// also include private properties and/or fields (public or private).
    /// </remarks>
    public class GenericEqualityComparer<T> : IEqualityComparer<T> where T : class
    {

        private List<Func<T, object>> _propertyGetters = new List<Func<T, object>>(); // Cache of compiled delegates for accessing the configured properties of T, used to avoid the performance overhead of reflection during comparisons.

        private List<Func<T, object>> _fieldGetters = new List<Func<T, object>>(); // Cache of compiled delegates for accessing the configured fields of T, used to avoid the performance overhead of reflection during comparisons.

        /// <summary>
        /// Initialises the comparer and builds the member accessor cache.
        /// </summary>
        /// <param name="includeFields">When <see langword="true"/>, public instance fields are included in the comparison.</param>
        /// <param name="includePrivateProperties">When <see langword="true"/>, private instance properties are included in the comparison.</param>
        /// <param name="includePrivateFields">When <see langword="true"/>, private instance fields are included in the comparison. Also enables public field comparison.</param>
        public GenericEqualityComparer(bool includeFields = false, bool includePrivateProperties = false, bool includePrivateFields = false)
        {
            CreatePropertyGetters(includePrivateProperties);
            if (includeFields || includePrivateFields)
            {
                CreateFieldGetters(includePrivateFields);
            }
        }

        private void CreatePropertyGetters(bool includePrivateProperties)
        {
            var bindingFlags = BindingFlags.Instance | BindingFlags.Public;
            if (includePrivateProperties)
            {
                bindingFlags |= BindingFlags.NonPublic;
            }

            var props = typeof(T).GetProperties(bindingFlags).Where(m => m.GetMethod != null).ToList();

            foreach (var prop in props)
            {

                //Builds the Expression<Func<T, object>> for the property getter and compiles it into a Func<T, object> delegate, which is cached for later use.
                ParameterExpression parameter = Expression.Parameter(typeof(T), "p");
                MemberExpression propertyExpression = Expression.Property(parameter, prop.Name);
                Expression boxedPropertyExpression = Expression.Convert(propertyExpression, typeof(object));
                Expression<Func<T, object>> propertyGetter = Expression.Lambda<Func<T, object>>(boxedPropertyExpression, parameter);
                _propertyGetters.Add(propertyGetter.Compile());
            }
        }

        private void CreateFieldGetters(bool includePrivateFields)
        {
            var bindingFlags = BindingFlags.Instance | BindingFlags.Public;
            if (includePrivateFields)
            {
                bindingFlags |= BindingFlags.NonPublic;
            }

            var fields = typeof(T).GetFields(bindingFlags).ToList();

            foreach (var field in fields)
            {
                // Builds the Expression<Func<T, object>> for the field getter and compiles it into a Func<T, object> delegate, which is cached for later use.
                ParameterExpression parameter = Expression.Parameter(typeof(T), "f");
                MemberExpression fieldExpression = Expression.Field(parameter, field.Name);
                Expression boxedPropertyExpression = Expression.Convert(fieldExpression, typeof(object));
                Expression<Func<T, object>> fieldGetter = Expression.Lambda<Func<T, object>>(boxedPropertyExpression, parameter);
                _fieldGetters.Add(fieldGetter.Compile());
            }

        }

        /// <summary>
        /// Determines whether <paramref name="x"/> and <paramref name="y"/> are equal by comparing
        /// each configured member in turn.
        /// </summary>
        /// <param name="x">The first object to compare.</param>
        /// <param name="y">The second object to compare.</param>
        /// <returns>
        /// <see langword="true"/> when all configured members are equal;
        /// <see langword="false"/> when any member differs, or either argument is <see langword="null"/>.
        /// </returns>
        public bool Equals(T? x, T? y)
        {
            if (x == null || y == null)
            {
                return false;
            }
            if (ReferenceEquals(x, y))
            {
                return true;
            }
            if (x.GetType() != y.GetType())
            {
                return false;
            }

            foreach (var propAccessor in _propertyGetters)
            {
                var xv = propAccessor(x);
                var yv = propAccessor(y);
                if (!xv.Equals(yv))
                {
                    return false;
                }
            }

            foreach (var fieldAccessor in _fieldGetters)
            {
                var xv = fieldAccessor(x);
                var yv = fieldAccessor(y);
                if (!xv.Equals(yv))
                {
                    return false;
                }

            }


            return true;
        }

        /// <summary>
        /// Returns an <see cref="EqualityWrapper{T}"/> for <paramref name="value"/> so that
        /// <c>==</c> and <c>!=</c> use this comparer's configured equality semantics.
        /// </summary>
        /// <param name="value">The value to wrap.</param>
        /// <returns>An <see cref="EqualityWrapper{T}"/> bound to this comparer instance.</returns>
        public EqualityWrapper<T> For(T value) => new EqualityWrapper<T>(value, this);

        /// <summary>
        /// Returns a hash code for <paramref name="obj"/> derived from the same configured members
        /// used by <see cref="Equals(T, T)"/>.
        /// </summary>
        /// <param name="obj">The object to hash.</param>
        /// <returns>A hash code consistent with the configured equality semantics.</returns>
        public int GetHashCode([DisallowNull] T obj)
        {
            int hash = 0;

            var propertyValues = _propertyGetters.Select(p => p(obj)).ToList();

            for (int i = 0; i < propertyValues.Count; i += 8)
            {
                hash = HashCode.Combine(hash,
                    propertyValues.ElementAtOrDefault(i),
                    propertyValues.ElementAtOrDefault(i + 1),
                    propertyValues.ElementAtOrDefault(i + 2),
                    propertyValues.ElementAtOrDefault(i + 3),
                    propertyValues.ElementAtOrDefault(i + 4),
                    propertyValues.ElementAtOrDefault(i + 5),
                    propertyValues.ElementAtOrDefault(i + 6));
            }

            if (_fieldGetters.Any())
            {
                var fieldValues = _fieldGetters.Select(f => f(obj)).ToList();
                for (int i = 0; i < fieldValues.Count; i += 8)
                {
                    hash = HashCode.Combine(hash,
                        fieldValues.ElementAtOrDefault(i),
                        fieldValues.ElementAtOrDefault(i + 1),
                        fieldValues.ElementAtOrDefault(i + 2),
                        fieldValues.ElementAtOrDefault(i + 3),
                        fieldValues.ElementAtOrDefault(i + 4),
                        fieldValues.ElementAtOrDefault(i + 5),
                        fieldValues.ElementAtOrDefault(i + 6));
                }
            }

            return hash;
        }

    }

}


The method For accepts a object instance of type T and returns a EqualityWrapper struct that allows the usage of operator == and !=

EqualityWrapper.cs



namespace GenericEqualityComparer.Lib;

/// <summary>
/// Pairs a value of type <typeparamref name="T"/> with a <see cref="GenericEqualityComparer{T}"/>
/// so that <c>==</c> and <c>!=</c> use the comparer's configured equality semantics instead of
/// reference equality.
/// </summary>
/// <typeparam name="T">The type of the wrapped value. Must be a reference type.</typeparam>
/// <remarks>
/// Obtain an instance via <see cref="GenericEqualityComparer{T}.For"/>:
/// <code>comparer.For(car1) == comparer.For(car2)</code>
/// </remarks>
public readonly struct EqualityWrapper<T> where T : class
{
    private readonly T _value;
    private readonly GenericEqualityComparer<T> _comparer;

    internal EqualityWrapper(T value, GenericEqualityComparer<T> comparer)
    {
        _value = value;
        _comparer = comparer;
    }

    /// <summary>
    /// Returns <see langword="true"/> when <paramref name="left"/> and <paramref name="right"/>
    /// are considered equal by their shared comparer.
    /// </summary>
    public static bool operator ==(EqualityWrapper<T> left, EqualityWrapper<T> right)
        => left._comparer.Equals(left._value, right._value);

    /// <summary>
    /// Returns <see langword="true"/> when <paramref name="left"/> and <paramref name="right"/>
    /// are not considered equal by their shared comparer.
    /// </summary>
    public static bool operator !=(EqualityWrapper<T> left, EqualityWrapper<T> right)
        => !(left == right);

    /// <inheritdoc/>
    public override bool Equals(object? obj)
        => obj is EqualityWrapper<T> other && this == other;

    /// <inheritdoc/>
    public override int GetHashCode()
        => _comparer.GetHashCode(_value);
}



2 — Quick start

2.1 Compare public properties

using GenericEqualityComparer.Lib;

var comparer = new GenericEqualityComparer<Car>();

var car1 = new Car { Make = "Toyota", Model = "Camry", Year = 2020 };
var car2 = new Car { Make = "Toyota", Model = "Camry", Year = 2020 };
var car3 = new Car { Make = "Toyota", Model = "Corolla", Year = 2020 };

Console.WriteLine(comparer.Equals(car1, car2));  // True  — all properties match
Console.WriteLine(comparer.Equals(car1, car3));  // False — Model differs

2.2 Use it with LINQ or collections

Because GenericEqualityComparer<T> implements IEqualityComparer<T> you can pass it directly to LINQ methods and collection APIs that accept one.

var cars = new List<Car>
{
    new Car { Make = "Toyota", Model = "Camry",   Year = 2020 },
    new Car { Make = "Toyota", Model = "Camry",   Year = 2020 }, // duplicate
    new Car { Make = "Toyota", Model = "Corolla", Year = 2021 },
};

var comparer = new GenericEqualityComparer<Car>();

// Distinct by value
var unique = cars.Distinct(comparer).ToList();  // 2 items

// GroupBy by value
var grouped = cars.GroupBy(c => c, comparer);

3 — Constructor options

The constructor accepts three optional boolean flags. All default to false.

Parameter Type What it includes
includeFields bool Public instance fields
includePrivateProperties bool Private instance properties
includePrivateFields bool Private instance fields (also enables public fields)

3.1 Include private fields

Imagine a Car class that stores a secret assembly number in a private field:

public class Car
{
    public string Make { get; set; } = string.Empty;
    public string Model { get; set; } = string.Empty;
    public int Year { get; set; }

    // private — not visible to external code
    private string _secretAssemblyNumber = string.Empty;
    public void SetSecretAssemblyNumber(string number) => _secretAssemblyNumber = number;
}
var ford1 = new Car { Make = "Ford", Model = "Focus", Year = 2022 };
var ford2 = new Car { Make = "Ford", Model = "Focus", Year = 2022 };
ford1.SetSecretAssemblyNumber("ASM-001");
ford2.SetSecretAssemblyNumber("ASM-999");  // intentionally different

// Default comparer — only sees public properties, ignores the private field
var defaultComparer = new GenericEqualityComparer<Car>();
Console.WriteLine(defaultComparer.Equals(ford1, ford2));  // True (field ignored)

// Include private fields — now the hidden difference is detected
var deepComparer = new GenericEqualityComparer<Car>(includePrivateFields: true);
Console.WriteLine(deepComparer.Equals(ford1, ford2));     // False

3.2 Include private properties

The same idea applies when a class uses a private property as an internal identifier:

public class Bicycle
{
    public string Brand { get; set; } = string.Empty;
    public string Model { get; set; } = string.Empty;

    private string FrameSerialNumber { get; set; } = string.Empty;
    public void SetFrameSerialNumber(string sn) => FrameSerialNumber = sn;
}
var bike1 = new Bicycle { Brand = "Trek", Model = "FX3" };
var bike2 = new Bicycle { Brand = "Trek", Model = "FX3" };
bike1.SetFrameSerialNumber("SN-001");
bike2.SetFrameSerialNumber("SN-999");

var defaultComparer = new GenericEqualityComparer<Bicycle>();
Console.WriteLine(defaultComparer.Equals(bike1, bike2));  // True

var deepComparer = new GenericEqualityComparer<Bicycle>(includePrivateProperties: true);
Console.WriteLine(deepComparer.Equals(bike1, bike2));     // False

4 — EqualityWrapper<T> and the == / != operators

C# doesn't allow overloading == and != on a generic type parameter T in an external comparer class. As a workaround, GenericEqualityComparer<T> exposes a For(value) method that returns an EqualityWrapper<T>. The wrapper carries both the value and the comparer, so its == and != operators delegate to the comparer instead of defaulting to reference equality.

4.1 Basic operator usage

var comparer = new GenericEqualityComparer<Car>();

var car1 = new Car { Make = "Toyota", Model = "Camry", Year = 2020 };
var car2 = new Car { Make = "Toyota", Model = "Camry", Year = 2020 };
var car3 = new Car { Make = "Toyota", Model = "Corolla", Year = 2020 };

bool same      = comparer.For(car1) == comparer.For(car2);  // True
bool different = comparer.For(car1) != comparer.For(car3);  // True

4.2 With private member detection

var deepComparer = new GenericEqualityComparer<Car>(includePrivateFields: true);

var ford1 = new Car { Make = "Ford", Model = "Focus", Year = 2022 };
var ford2 = new Car { Make = "Ford", Model = "Focus", Year = 2022 };
ford1.SetSecretAssemblyNumber("ASM-001");
ford2.SetSecretAssemblyNumber("ASM-999");

if (deepComparer.For(ford1) != deepComparer.For(ford2))
{
    Console.WriteLine("Cars differ (private field detected)");
}

4.3 Consistent hashing

EqualityWrapper<T> also overrides GetHashCode() so it stays consistent with ==. This means wrapped values can be used safely as dictionary keys or in hash sets.

var comparer = new GenericEqualityComparer<Car>();
var car1     = new Car { Make = "Toyota", Model = "Camry", Year = 2020 };
var car2     = new Car { Make = "Toyota", Model = "Camry", Year = 2020 };

int hash1 = comparer.For(car1).GetHashCode();
int hash2 = comparer.For(car2).GetHashCode();

Console.WriteLine(hash1 == hash2);  // True — equal objects, equal hashes

5 — When not to use it

Performance: The comparer uses reflection to discover members at construction time (compiled to delegates for speed), but it is still a little slower than a hand-written Equals. Avoid it in tight loops or hot paths.
  • Records — C# records already have value equality built in. Use == directly.
  • Structs — Same as records; value equality is the default.
  • Classes you own — Prefer overriding Equals / GetHashCode or implementing IEquatable<T> for production code (due to performance). Use this comparer for tests, prototyping, or third-party types you can't modify. Or if you just would like a simple way of providing value based equality checks, but in that case you should
    really
    consider a specific implementation.
In case you work with generated code or for got a large number of POCO classes (Data transfer objects) and want to avoid using inheritance or adding value equality of your existing code, this code allows you adding value based equality, this code shown here should have you covered with a generic util class.

6 - Supported Frameworks

Please note that since we use HashCode here, supported target frameworks are netstandard 2.1 and .netcore 2.1 or later. In case you use .NET Framework 4.8 or earlier for example, you can provide a GetHashCode implementation like the following:

GetHashCode that avoids using HashCode.Combine

We can instead use two selected prime numbers and multipliers to calculate a hash of the object's propertis and fields like the following:


public int GetHashCode([DisallowNull] T obj)
{
    int hash = 0;

    var propertyValues = _propertyGetters.Select(p => p(obj)).ToList();

    for (int i = 0; i < propertyValues.Count; i += 8)
    {
        hash = Combine(hash,
            propertyValues.ElementAtOrDefault(i),
            propertyValues.ElementAtOrDefault(i + 1),
            propertyValues.ElementAtOrDefault(i + 2),
            propertyValues.ElementAtOrDefault(i + 3),
            propertyValues.ElementAtOrDefault(i + 4),
            propertyValues.ElementAtOrDefault(i + 5),
            propertyValues.ElementAtOrDefault(i + 6));
    }

    if (_fieldGetters.Any())
    {
        var fieldValues = _fieldGetters.Select(f => f(obj)).ToList();
        for (int i = 0; i < fieldValues.Count; i += 8)
        {
            hash = Combine(hash,
                fieldValues.ElementAtOrDefault(i),
                fieldValues.ElementAtOrDefault(i + 1),
                fieldValues.ElementAtOrDefault(i + 2),
                fieldValues.ElementAtOrDefault(i + 3),
                fieldValues.ElementAtOrDefault(i + 4),
                fieldValues.ElementAtOrDefault(i + 5),
                fieldValues.ElementAtOrDefault(i + 6));
        }
    }

    return hash;
}

private static int Combine(params object[] values)
{
    unchecked
    {
        int hash = 17;
        foreach (var v in values)
        {
            int h = v?.GetHashCode() ?? 0;
            hash = hash * 31 + h;
        }
        return hash;
    }
}


The strange selection of two prime numbers 17 and factor of 31 is to provide diffusion to avoid hash collisions and avoid also trouble with objects with symmetric values (a,b) equaling (b,a) is avoided
using this way of summing the hashes from each property. The HashCode.Combine allows us to avoid this.

7 — Summary

The article has presented a way to do value equality checks for instances of classes in a generic manner supporting an arbitrary number of public (and private) properties, possibly also including fields (and private fields). If you want an easy way of adding value equality checks in classes and performance allows using the expression compiled delegates shown here with a little overhead initially, you should be able to consider the code here for some scenarios. The Github Repo of mine for this source code contains a lot of tests, so the code is tested.
What you wantHow
Compare public properties new GenericEqualityComparer<T>()
Also include public fields new GenericEqualityComparer<T>(includeFields: true)
Also include private properties new GenericEqualityComparer<T>(includePrivateProperties: true)
Also include private fields new GenericEqualityComparer<T>(includePrivateFields: true)
Use == / != operators comparer.For(a) == comparer.For(b)
Use with LINQ list.Distinct(comparer), list.GroupBy(x => x, comparer)

Sunday, 15 February 2026

Blazor Property Grid - Updated version

Blazor Property Grid

I wrote a Blazor Property Grid component back in 2021, the updated the version is now available some five years later.

You will find the property grid available on Nuget here:

https://www.nuget.org/packages/BlazorPropertyGridComponents/1.2.4

The property grid allows inspection of an object's properties and also edit them. Supported data types are the fundamental data types, which means integers, date times, booleans, strings and numbers.

The property grid supports nested properties, properties that are compound objects themselves. I have not yet added template supported for custom data types, but fundamentals nested properties inside the complex property are shown. This means you can drill down into an object and both inspect the object and also edit the properties that are the mentioned fundamental data types.

This project delivers a Blazor property‑grid component capable of inspecting and editing both top‑level and deeply nested properties of an object. It works smoothly with nested structures and internal members.

It has been verified using Blazor WebAssembly running on .NET. 10. A sample client is included in the BlazorSampleClient project.

The component implementation is located inside the Razor Class Library BlazorPropertyGridComponents.

Licensing is MIT and the component is provided as‑is. If used in production, you are responsible for validating its suitability. Forks, modifications, and commercial reuse are allowed. The project originated as a personal learning exercise.

The screenshot below (from the sample client) illustrates the property grid on the right side updating the same model that the form on the left is bound to.

The component expects your project to include Bootstrap and Font Awesome. You can inspect exact versions inside libman.json. Additional styling resides in styles.css.

libman.json example

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "bootstrap@5.3.3",
      "destination": "wwwroot/bootstrap/"
    },
    {
      "library": "font-awesome@6.5.1",
      "destination": "wwwroot/font-awesome/"
    }
  ]
}

Supported Property Types

  • DateTime (full or date‑only)
  • Float
  • Decimal
  • Int
  • Double
  • String
  • Bool
  • Enums (rendered using <select> and <option>)

Using the Component – API

Below is a Razor example showing how to use the component. The PropertySetValueCallback is optional and only required if you want UI changes reflected elsewhere immediately.

<div class="col-md-6"> <!-- Property grid shown in the right column -->
  <h4 class="mb-3">Property Grid Component Demo</h4>

  <PropertyGridComponent
      PropertySetValueCallback="OnPropertyValueSet"
      ObjectTitle="Property grid - Edit form for : 'Customer Details'"
      DataContext="@exampleModel">
  </PropertyGridComponent>
</div>

@code {
  private void OnPropertyValueSet(PropertyChangedInfoNotificationInfoPayload pi)
  {
    if (pi != null)
    {
      JsRunTime.InvokeVoidAsync(
        "updateEditableField",
        pi.FieldName,
        pi.FullPropertyPath,
        pi.Value
      );
    }
  }

  private CustomerModel exampleModel = new CustomerModel
  {
    Address = new AddressInfo
    {
      Zipcode = 7045,
      AddressDetails = new AddressInfoDetails
      {
        Box = "PO Box 123"
      }
    }
  };

  private void HandleValidSubmit()
  {
  }
}

The JavaScript function updateEditableField is located in script.js.

Updating via C# Instead of JS

You can also update the model directly through reflection. This approach ensures proper Blazor re‑rendering. Be sure to call StateHasChanged(). Note that this code is only required in case you use the property grid for editing properties and show the object you edit other places on the same page.

@code {

  private void OnPropertyValueSet(PropertyChangedInfoNotificationInfoPayload pi)
  {
    if (pi == null)
      return;

    SetPropertyByPath(exampleModel, pi.FullPropertyPath, pi.Value, pi.ValueType);
    StateHasChanged();
  }

  private void SetPropertyByPath(object target, string propertyPath, object value, string valueType)
  {
    if (target == null || string.IsNullOrEmpty(propertyPath))
      return;

    var parts = propertyPath.Split('.');
    var current = target;

    // Move to parent object

        // Navigate to the parent object
        for (int i = 0; i < parts.Length - 1; i++)
        {
            var prop = current.GetType().GetProperty(parts[i]);
            if (prop == null) return;
            current = prop.GetValue(current);
            if (current == null) return;
        }

        // Set the final property
        var finalProp = current.GetType().GetProperty(parts[^1]);
        if (finalProp == null) return;

        try
        {
            object convertedValue = value;

            if (finalProp.PropertyType.IsEnum && value != null)
            {
                var valStr = value.ToString();
                if (int.TryParse(valStr, out int intVal))
                    convertedValue = Enum.ToObject(finalProp.PropertyType, intVal);
                else
                    convertedValue = Enum.Parse(finalProp.PropertyType, valStr, ignoreCase: true);
            }
            else if (finalProp.PropertyType == typeof(bool) && value != null)
            {
                convertedValue = Convert.ToBoolean(value);
            }
            else if (value != null && finalProp.PropertyType != typeof(string))
            {
                convertedValue = Convert.ChangeType(value, finalProp.PropertyType);
            }

            finalProp.SetValue(current, convertedValue);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Failed to set {propertyPath}: {ex.Message}");
        }
    }

    private CustomerModel exampleModel = new CustomerModel
    {
        Address = new AddressInfo
        {
            Zipcode = 7045,
            AddressDetails = new AddressInfoDetails
            {
                Box = "PO Box 123"
            }
        }
    };

    private void HandleValidSubmit()
    {

    }

}

I have used Claude Haiku 4.5 LLM to create a nice Architecture Documentation of my component so it is more convenient to see the structure of the Blazor property grid component. This is handy for those developers who wants to work on the component and add features and understood its structure. As mentioned before, the component is licensed with MIT license and you can adjust the component as needed free of use (and responsiblity).

πŸ”· Blazor Property Grid Component - Architecture Documentation


πŸ“Š Property Grid Component Structure

PROPERTY GRID COMPONENT ARCHITECTURE
====================================

ROOT CONTAINER: PropertyGridComponent.razor
├── EditForm (wraps the entire grid)
└── .property-grid-container
    ├── Header Table (.property-grid-header-table)
    │   └── thead (.property-grid-header)
    │       └── tr
    │           ├── th: "Property"
    │           ├── th: "Value"
    │           └── th: Edit Button (pencil icon)
    │
    └── Body Table (.property-grid-body-table)
        └── tbody (.property-grid-body)
            └── foreach KeyValuePair in Props
                ├── IF: Simple Property (IsClass = false)
                │   └── [COMMENTED OUT - NOT DISPLAYED]
                │
                └── IF: Nested Class (IsClass = true)
                    └── tr
                        ├── td (colspan=2)
                        │   ├── Expand/Collapse Button (minus icon)
                        │   └── div (.collapse .show)
                        │       └── PropertyRowComponent [Depth=1]
                        └── td (empty)


PROPERTY ROW COMPONENT: PropertyRowComponent.razor
====================================================

FOR EACH SubProperty in PropertyInfoAtLevel.SubProperties:
├── IF: Simple Type (not a class or System namespace)
│   └── tr (.property-row)
│       ├── td (.property-name-cell)
│       │   └── span (.property-name) = Property Name
│       │
│       └── td (.property-value-cell)
│           ├── IF: DateTime
│           │   └── InputDate (if editable) OR span (if readonly)
│           │
│           ├── IF: bool
│           │   └── InputCheckbox (if editable) OR span (if readonly)
│           │
│           ├── IF: int
│           │   └── InputNumber (if editable) OR span (if readonly)
│           │
│           ├── IF: double
│           │   └── InputText type="number" (if editable) OR span (if readonly)
│           │
│           ├── IF: decimal
│           │   └── InputText type="number" (if editable) OR span (if readonly)
│           │
│           ├── IF: float
│           │   └── InputText type="number" (if editable) OR span (if readonly)
│           │
│           ├── IF: string
│           │   └── InputText type="text" (if editable) OR span (if readonly)
│           │
│           ├── IF: Enum
│           │   └── select (if editable)
│           │       └── option foreach Enum value
│           │       OR span (if readonly)
│           │
│           └── ELSE: Unknown Type
│               └── span = Raw Value
│
└── IF: Nested Class (PropertyValue is HierarchicalPropertyInfo)
    └── tr (.property-row .nested-property-row)
        ├── td (colspan=2, .nested-property-cell)
        │   ├── span (.nested-property-name) = Nested Class Name
        │   ├── Expand/Collapse Button (plus icon)
        │   └── div (.collapse or .collapse.show)
        │       └── PropertyRowComponent [Depth+1] (RECURSIVE)
        └── [Empty td]


DATA STRUCTURE: HierarchicalPropertyInfo
================================================

HierarchicalPropertyInfo
├── PropertyName: string
├── PropertyValue: object
├── PropertyType: Type
├── SubProperties: Dictionary<string, HierarchicalPropertyInfo>
├── FullPropertyPath: string (dot-separated path)
├── IsClass: bool (indicates if this is a class type)
├── IsEditable: bool
├── NewValue: object (for tracking changes)
└── ValueSetCallback: EventCallback

HIERARCHY BUILD PROCESS:
========================

MapPropertiesOfDataContext(root object)
├── Create ROOT HierarchicalPropertyInfo
├── For each Public Property:
│   ├── IF: Simple Type (not class or not System namespace)
│   │   └── Add to SubProperties as leaf node (IsClass=false)
│   │
│   └── IF: Nested Class (class type, not System namespace)
│       └── Recursively call MapPropertiesOfDataContext
│           └── Add to SubProperties with nested tree (IsClass=true)
│
└── Return complete tree structure


INTERACTIVITY:
==============

Edit Mode Toggle:
├── ToggleEditButton() → IsEditingAllowed = !IsEditingAllowed
└── SetEditFlagRecursive() → walks entire tree setting IsEditable on all nodes

Value Changes:
├── SetValue() called on input change
├── Handles type conversion (enums, numbers, dates, etc.)
├── Updates PropertyValue immediately for UI reflection
└── Invokes ValueSetCallback → OnValueSetCallback()

Property Change Callback:
└── PropertySetValueCallback emits PropertyChangedInfoNotificationInfoPayload with:
    ├── FieldName
    ├── FullPropertyPath
    ├── Value
    └── ValueType (text, boolean, number, date, enum)

Expand/Collapse:
└── ToggleExpandButton() → JavaScript: blazorPropertyGrid.toggleExpandButton()
    └── Toggles Bootstrap collapse class on nested div

πŸ”§ Component Descriptions and Roles

1. PropertyGridComponent (PropertyGridComponent.razor + PropertyGridComponent.razor.cs)

Role: Root container and orchestrator for the entire property grid UI

Primary Responsibility:

Accepts a data object and transforms it into a hierarchical property structure

Key Functions:

  • OnParametersSet() - Initializes the component when parameters are passed
  • MapPropertiesOfDataContext() - Recursively walks the object graph and builds the HierarchicalPropertyInfo tree
  • IsNestedProperty() - Determines if a property should be expanded (nested classes)
  • ToggleEditButton() - Handles the edit mode button click (pencil icon in header)
  • SetEditFlag() / SetEditFlagRecursive() - Propagates the IsEditable flag through the entire tree
  • OnValueSetCallback() - Listens for value changes and emits PropertySetValueCallback events to the parent component

Parameters:

  • DataContext (object) - The root object to display
  • ObjectTitle (string) - Display title for the grid
  • IsEditingAllowed (bool) - Whether fields are editable
  • PropertySetValueCallback (EventCallback) - Callback when a property value changes

Rendering:

Header table with Property/Value columns and edit button. Body table containing rows for top-level properties (delegates nested properties to PropertyRowComponent)

2. PropertyRowComponent (PropertyRowComponent.razor + PropertyRowComponent.razor.cs)

Role: Recursive component that renders individual property rows and handles nested object expansion

Primary Responsibility:

Display a single level of properties and recursively display nested levels

Key Functions:

  • SetValue() - Handles input change events, performs type conversion, and invokes callbacks
  • ToggleExpandButton() - JS interop to toggle Bootstrap collapse classes for expand/collapse UI
  • Property type detection - Conditionally renders different input controls based on property type

Type Support:

  • DateTime → InputDate (HTML5 datetime-local)
  • bool → InputCheckbox
  • int → InputNumber
  • double, decimal, float → InputText with type="number"
  • string → InputText with type="text"
  • Enum → HTML select dropdown with enum values
  • Nested Classes → Recursive PropertyRowComponent call with Depth+1
  • Unknown Types → Raw span display

Parameters:

  • PropertyInfoAtLevel (HierarchicalPropertyInfo) - The current property node to render
  • Depth (int) - Nesting depth for styling and collapse behavior
  • DisplayedFullPropertyPaths (List<string>) - Tracks which paths have been rendered (prevents duplication)

Features:

  • Editable/Read-only modes based on IsEditable flag
  • Collapse/expand functionality for nested objects
  • Visual styling (beige background for read-only values)
  • Full property path as tooltip for clarity

3. HierarchicalPropertyInfo (HierarchicalPropertyInfo.cs)

Role: Data structure representing a single property node in the object hierarchy

Primary Responsibility:

Act as a node in a tree structure that mirrors the original object's property graph

Properties:

  • PropertyName (string) - Name of the property
  • PropertyValue (object) - Current value of the property
  • PropertyType (Type) - CLR type of the property
  • SubProperties (Dictionary<string, HierarchicalPropertyInfo>) - Child properties (for nested objects)
  • FullPropertyPath (string) - Dot-separated path from root (e.g., "Customer.Address.Street")
  • IsClass (bool) - Whether this represents a class type (true = nested object, false = leaf value)
  • IsEditable (bool) - Whether the property can be edited in the current UI state
  • NewValue (object) - Tracks modified value before submission
  • ValueSetCallback (EventCallback) - Callback when this property's value changes

4. PropertyChangedInfoNotificationInfoPayload (PropertyChangedInfoNotificationInfoPayload.cs)

Role: Payload object that carries change notification information back to the parent

Primary Responsibility:

Communicate property change events with detailed context

Properties:

  • FieldName (string) - Name of the property
  • FullPropertyPath (string) - Complete path to the property
  • Value (object) - New value
  • ValueType (string) - Type of value ("text", "number", "boolean", "date", "enum")

πŸ”„ Component Interaction Flow

User interacts with PropertyGrid
    ↓
PropertyGridComponent receives DataContext
    ↓
MapPropertiesOfDataContext() builds tree of HierarchicalPropertyInfo
    ↓
Renders PropertyRowComponent for each top-level property
    ↓
PropertyRowComponent renders based on type:
  ├─ Simple types → InputControl (text, number, checkbox, date, select)
  └─ Nested classes → Recursive PropertyRowComponent
    ↓
User edits a value
    ↓
PropertyRowComponent.SetValue() processes input
    ↓
ValueSetCallback invoked
    ↓
OnValueSetCallback() determines value type and emits PropertySetValueCallback
    ↓
Parent component receives PropertyChangedInfoNotificationInfoPayload

🎯 Key Design Patterns

1. Recursive Composition

PropertyRowComponent calls itself recursively for nested objects, allowing unlimited nesting depth.

2. Tree Structure

HierarchicalPropertyInfo forms a tree that mirrors the object graph, enabling efficient traversal and state management.

3. Event Cascading

Value changes propagate up through callbacks, maintaining separation of concerns between components.

4. Type-Driven Rendering

PropertyRowComponent dynamically renders different input controls based on CLR type, supporting datetime, enum, numeric, boolean, and string types.

5. Bootstrap Collapse Integration

Nested objects use Bootstrap's collapse classes for expand/collapse functionality, toggled via JavaScript interop.


πŸ“Š Data Flow Summary

DataContext (Object)
    ↓
MapPropertiesOfDataContext() [Reflection-based tree building]
    ↓
HierarchicalPropertyInfo Tree
    ↓
PropertyGridComponent → PropertyRowComponent Chain [Rendering]
    ↓
HTML Tables + Form Controls
    ↓
[User edits value]
    ↓
ValueSetCallback Events [Bubbling up]
    ↓
PropertyChangedInfoNotificationInfoPayload [Event payload]
    ↓
Parent Component [Handles business logic]

Sunday, 21 December 2025

Finding out file extension from byte inspection

Consider a byte array stored in a column in a table in a database column. How can we identify the file extension of the byte array by inspecting the byte array itself?
Note that byte arrays could be saved many places, also within files or similar.
The extension of a file can be discovered by inspect the File header. This is the first bytes, usually the first tens or hundreds of bytes of the byte array and constitute the file header. Some extensions got multiple file headers. A best effort to identity byte contents of a column in a database.

Let's use Powershell to inspect a file on disk, a sample JPEG file (.jpg). Lets run the following little script:

format-hex .\Stavkyrkje_RΓΈldal.jpg | Select-Object -First 16 The first few bytes are FF D8 FF
I have added a sample Github repo with utility code to check well-known file types for their file extensions.

https://github.com/toreaurstadboss/FileHeaderUtil

The following screenshot shows the application in use. It found out that a byte array seems to be a PDF file by looking at the file header and file trailer. A good match was found :


In fact, a very good match, since both the header and the trailer fully agrees. Note that the 0A bytes are just padding bytes at the end of files and ignored in this util. See the method NormalizeHex presented further below.

Using Gary Kessler`s assembled lists of known file headers and trailers for well-known file types

The util class below shows the helper methods that inspects a byte array and evalues the file header and file trailer against a list of known such headers and trailers.

It bases a compilation of known file headers and file trailers known as "Magic Numbers", compiled by Gary Kessler during the years. In all, 600+ known file types are checked against to classify the matching file extension. Please note that there are cases where multiple matches exists of file header and file trailers matching the given byte array. The matches are sorted by number of matching bytes. The assembled list is very helpful. Thanks, Gary !

Using the file header and also possibly the last bytes of a byte array, the file trailer, we can classify the file type we have in the byte array, i.e. file extension is also implied here by recognizing the file array.

Of course, if one is allowing byte array to be uploaded from a public site for example, it still would be possible to inject malicious bytes, but being able to detect the kind of file is useful both concerning security policies and also determine if the bytes should be handled by an external application or provide information to the end-user what kind of file we have provided a path to for this util.

The curated list of file headers is based upon the list of signatures gathered by Gary Kessler and published on his website here (license of that file is not stated and considered public as it is publicly available information on his website not marked with a license):

https://www.garykessler.net/library/file_sigs.html

This list contains about 650 file types and should cover most of the wellknown formats, including formats not being used so often anymore. If you want to augment the list, check other sources such as Wikipedia if there is information about the given file extension's file header and/or file trailer, so-called "Magic number".

The curated list was updated 3rd June 2023 and contains most well-known file types.

The program uses the file signatures (Json format) to identity the file types of a byte array. Most usually, this is judged by looking at the first few bytes of the file (the so-called "magic numbers"). Sometimes, the file signature may also include bytes from the end of the file (the "trailer").


FileSignatureUtil.cs



using System;
using System.Collections.Generic;
using System.Text;

namespace FileHeaderUtil;


public static class FileSignatureUtil
{

    static FileSignature[] _fileSignatures = [];

    static FileSignatureUtil()
    {
        string json = File.ReadAllText("file_Sigs.json");
        var fileSignaturesRoot = System.Text.Json.JsonSerializer.Deserialize<FileSignatureRootElement>(json, new System.Text.Json.JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true
        });
        _fileSignatures = fileSignaturesRoot?.FileSigs?.ToArray()!;
    }

    /// <summary>
    /// Scans the specified file and returns a list of file signatures that match the file's header and, if applicable,
    /// file's trailer.
    /// </summary>
    /// <remarks>Only file signatures with a defined header are considered for matching. Trailer matching is
    /// performed if both the file and the signature define a trailer. A header and trailer of 64 bytes is evaluted to also 
    /// detect file types / extensions with longer headers and trailers.</remarks>
    /// <param name="targetFile">The path to the file to be analyzed. Cannot be null or empty.</param>
    /// <param name="byteCount">The number of bytes to read from the file for signature matching. Defaults to 64.</param>
    /// <param name="offset">The byte offset at which to begin reading the file for signature matching. Defaults to 0.</param>
    /// <param name="origin">Specifies the reference point used to obtain the offset. Defaults to <see cref="SeekOrigin.Begin"/>.</param>
    /// <returns>A list of <see cref="FileSignature"/> objects that match the file's header and trailer. The list is empty if no
    /// signatures match.</returns>

    public static List<FileSignature> GetMatchingFileSignatures(string targetFile, int byteCount = 64, int offset = 0, SeekOrigin origin = SeekOrigin.Begin)
    {
        static string NormalizeHex(string? hex, bool trimPadding)
        {
            if (string.IsNullOrWhiteSpace(hex))
            {
                return string.Empty;
            }           

            var parts = hex.Replace("-", " ").Split(new[] { ' ', }, StringSplitOptions.RemoveEmptyEntries)
                           .Select(h => h.ToUpperInvariant())
                           .ToList();

            if (trimPadding)
            {
                while (parts.Count > 0 && (parts.Last() == "0A" || parts.Last() == "0D" || parts.Last() == "00"))
                {
                    parts.RemoveAt(parts.Count - 1);
                }
            }

            return string.Join(" ", parts);
        }

        var matches = new List<(FileSignature Sig, int Score)>();

        string fileHeader = NormalizeHex(FileUtil.ShowHeader(targetFile, offset: 0), trimPadding: false);
        string fileTrailer = NormalizeHex(FileUtil.ShowTrailer(targetFile), trimPadding: true);

        foreach (var signature in _fileSignatures)
        {
            if (string.IsNullOrWhiteSpace(signature?.HeaderHex) || signature.HeaderHex == "(NULL)")
                continue;

            string sigHeader = NormalizeHex(signature.HeaderHex, trimPadding: false);
            string sigTrailer = NormalizeHex(signature.TrailerHex, trimPadding: true);

            if (!fileHeader.StartsWith(sigHeader, StringComparison.OrdinalIgnoreCase))
                continue;

            // Trailer check if defined
            if (!string.IsNullOrWhiteSpace(sigTrailer) && sigTrailer != "(NULL)")
            {
                if (!fileTrailer.EndsWith(sigTrailer, StringComparison.OrdinalIgnoreCase))
                    continue;
            }

            // Compute match score (# of matching bytes in header and trailer of file)
            int headerScore = CountMatchingPrefix(fileHeader, sigHeader);
            int trailerScore = CountMatchingSuffix(fileTrailer, sigTrailer);
            int scoreMeasuredAsMatchingByteCount = headerScore + trailerScore;
            signature.MatchingBytesCount = scoreMeasuredAsMatchingByteCount;
            signature.MatchingTrailerBytesCount = trailerScore;
            signature.MatchingHeaderBytesCount = headerScore;
            matches.Add((signature, scoreMeasuredAsMatchingByteCount));
        }

        return matches.OrderByDescending(m => m.Score).Select(m => m.Sig).ToList();
    }

    // Helpers
    private static int CountMatchingPrefix(string source, string pattern)
    {
        var srcParts = source.Split(' ');
        var patParts = pattern.Split(' ');
        int count = 0;
        for (int i = 0; i < Math.Min(srcParts.Length, patParts.Length); i++)
        {
            if (srcParts[i].Equals(patParts[i], StringComparison.OrdinalIgnoreCase))
                count++;
            else break;
        }
        return count;
    }

    private static int CountMatchingSuffix(string source, string pattern)
    {
        if (string.IsNullOrWhiteSpace(pattern)) return 0;
        var srcParts = source.Split(' ');
        var patParts = pattern.Split(' ');
        int count = 0;
        for (int i = 0; i < Math.Min(srcParts.Length, patParts.Length); i++)
        {
            if (srcParts[srcParts.Length - 1 - i].Equals(patParts[patParts.Length - 1 - i], StringComparison.OrdinalIgnoreCase))
                count++;
            else break;
        }
        return count;
    }

}





As we can see in the source code of NormalizeHex, ending padding chars are removed at the end, since in some cases, byte arrays (files or byte columns in databases for examples) are padded with certain bytes. Also, upper-case is applied and '-' is replaced by space ' '.

In the example below, a PDF file is scanned with the console app and the PDF file header and trailer is recognized. In this case, we also peel of trailing bytes at the end, as the specific PDF file had trailing bytes of pad bytes, more specifically : 0A.

FileUtil.cs

The util class here is used to load a file header or file trailer, a smaller byte array usually. 64 bytes is default evaluated here and should cover most file types file headers and file trailers, actually most file types only has 8 bytes or even less as a file header or file trailer.


namespace FileHeaderUtil
{

    /// <summary>
    /// Helper class for file operations
    /// </summary>
    public static class FileUtil
    {

        /// <summary>
        /// Prints the file header HEX representation
        /// </summary>
        /// <param name="filePath"></param>
        /// <param name="byteCount">Read the first n bytes. Defaults to 64 bytes.</param>
        /// <returns></returns>
        public static string? ShowHeader(string filePath, int byteCount = 64, int offset = 0)
        {
            if (!File.Exists(filePath))
            {
                throw new FileNotFoundException(filePath);
            }

            byte[] header = ReadBytes(filePath, byteCount, offset, SeekOrigin.Begin);
            if (header == null)
            {
                return null;
            }
            return BitConverter.ToString(header);
        }

        /// <summary>
        /// Prints the file trailer HEX representation
        /// </summary>
        /// <param name="filePath"></param>
        /// <param name="byteCount">Read the last n bytes. Defaults to 64 bytes.</param>
        /// <returns></returns>
        public static string? ShowTrailer(string filePath, int byteCount = 64, int offset = 0)
        {
            if (!File.Exists(filePath))
            {
                throw new FileNotFoundException(filePath);
            }

            byte[] header = ReadBytes(filePath, byteCount, offset, SeekOrigin.End);
            if (header == null)
            {
                return null;
            }
            return BitConverter.ToString(header);
        }

        /// <summary>
        /// Reads the n bytes of a byte array. Either from the start or the end of the byte array.
        /// </summary>
        /// <param name="filePath">File path of target file to read the byets</param>
        /// <param name="byteCount">The number of bytes to read</param>
        /// <param name="offset">Offset - number of bytes</param>
        /// <param name="origin">Origin to seek from. Can be either SeekOrigin.Begin, SeekOrigin.Current or SeekOrigin.End</param>
        /// <returns></returns>
        private static byte[] ReadBytes(string filePath, int byteCount, int offset = 0, SeekOrigin origin = SeekOrigin.Begin)
        {
            if (!File.Exists(filePath))
            {
                throw new FileNotFoundException(filePath);
            }

            if (byteCount < 1)
            {
                return Array.Empty<byte>();
            }
            byte[] buffer = new byte[byteCount];
            using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
            if (origin == SeekOrigin.Begin && offset > 0)
            {
                fileStream.Seek(offset, origin);
            }
            else if (origin == SeekOrigin.End)
            {
                fileStream.Seek(-1 * Math.Abs(offset+byteCount), origin);
            }
            else
            {
                //origin must be Current - offset is expected from the current position, just like SeekOrigin.Begin
                fileStream.Seek(offset, origin); 
            }

            int bytesRead = fileStream.Read(buffer, 0, byteCount); 
            if (bytesRead < byteCount)
            {
                Array.Resize(ref buffer, bytesRead);
            }
            return buffer;
        }        
        
    }

}


This console app will only consider max three matching file headers/trailers in cases where multiple such byte array pairs matches a given byte array of a file. To adjust this, see in Program.cs and adjust the Take parameter. Matches are ordered by number of bytes matching.

Monday, 24 November 2025

Exploring Extension Blocks and Constants in C# 14

Extension blocks - Extension properties

Extension blocks and an important new feature - extension properties can be made in C#14. This is available with .NET 10.

It is not possible to define a generic extension block to add extension properties / members (yet, as of C#14 anyways -maybe for future version of C#..)

Consider this example of some well-known constants from entry-level Calculus using extension properties.


using System.Numerics;

namespace Csharp14NewFeatures
{
    /// <summary>
    /// Provides well-known mathematical constants for any numeric type using generic math.
    /// </summary>
    /// <typeparam name="T">A numeric type implementing INumber<T> (e.g., double, decimal, float).</typeparam>
    public static class MathConstants<T> where T : INumber<T>
    {
        /// <summary>Ο€ (Pi), ratio of a circle's circumference to its diameter.</summary>
        public static T Pi => T.CreateChecked(Math.PI);

        /// <summary>Ο„ (Tau), equal to 2Ο€. Represents one full turn in radians.</summary>
        public static T Tau => T.CreateChecked(2 * Math.PI);

        /// <summary>e (Euler's number), base of the natural logarithm.</summary>
        public static T E => T.CreateChecked(Math.E);

        /// <summary>Ο† (Phi), the golden ratio (1 + √5) / 2.</summary>
        public static T Phi => T.CreateChecked((1 + Math.Sqrt(5)) / 2);

        /// <summary>√2, square root of 2. Appears in geometry and trigonometry.</summary>
        public static T Sqrt2 => T.CreateChecked(Math.Sqrt(2));

        /// <summary>√3, square root of 3. Common in triangle geometry.</summary>
        public static T Sqrt3 => T.CreateChecked(Math.Sqrt(3));

        /// <summary>ln(2), natural logarithm of 2.</summary>
        public static T Ln2 => T.CreateChecked(Math.Log(2));

        /// <summary>ln(10), natural logarithm of 10.</summary>
        public static T Ln10 => T.CreateChecked(Math.Log(10));

        /// <summary>Degrees-to-radians conversion factor (Ο€ / 180).</summary>
        public static T Deg2Rad => T.CreateChecked(Math.PI / 180.0);

        /// <summary>Radians-to-degrees conversion factor (180 / Ο€).</summary>
        public static T Rad2Deg => T.CreateChecked(180.0 / Math.PI);
    }

    /// <summary>
    /// Extension blocks exposing math constants as properties for common numeric types.
    /// </summary>
    public static class MathExtensions
    {
        extension(double source)
        {
            /// <inheritdoc cref="MathConstants{T}.Pi"/>
            public double Pi => MathConstants<double>.Pi;
            public double Tau => MathConstants<double>.Tau;
            public double E => MathConstants<double>.E;
            public double Phi => MathConstants<double>.Phi;
            public double Sqrt2 => MathConstants<double>.Sqrt2;
            public double Sqrt3 => MathConstants<double>.Sqrt3;
            public double Ln2 => MathConstants<double>.Ln2;
            public double Ln10 => MathConstants<double>.Ln10;
            public double Deg2Rad => MathConstants<double>.Deg2Rad;
            public double Rad2Deg => MathConstants<double>.Rad2Deg;
        }

        extension(decimal source)
        {
            public decimal Pi => MathConstants<decimal>.Pi;
            public decimal Tau => MathConstants<decimal>.Tau;
            public decimal E => MathConstants<decimal>.E;
            public decimal Phi => MathConstants<decimal>.Phi;
            public decimal Sqrt2 => MathConstants<decimal>.Sqrt2;
            public decimal Sqrt3 => MathConstants<decimal>.Sqrt3;
            public decimal Ln2 => MathConstants<decimal>.Ln2;
            public decimal Ln10 => MathConstants<decimal>.Ln10;
            public decimal Deg2Rad => MathConstants<decimal>.Deg2Rad;
            public decimal Rad2Deg => MathConstants<decimal>.Rad2Deg;
        }

        extension(float source)
        {
            public float Pi => MathConstants<float>.Pi;
            public float Tau => MathConstants<float>.Tau;
            public float E => MathConstants<float>.E;
            public float Phi => MathConstants<float>.Phi;
            public float Sqrt2 => MathConstants<float>.Sqrt2;
            public float Sqrt3 => MathConstants<float>.Sqrt3;
            public float Ln2 => MathConstants<float>.Ln2;
            public float Ln10 => MathConstants<float>.Ln10;
            public float Deg2Rad => MathConstants<float>.Deg2Rad;
            public float Rad2Deg => MathConstants<float>.Rad2Deg;
        }
    }
}

We must define extension blocks per type here.

If we move over to extension methods, we still must use a non-generic class. However, we can use for example generic math, show below. This allows to reuse code accross multiple types, supporting INumber<T> in this case.


namespace Csharp14NewFeatures
{
    using System;
    using System.Numerics;

    namespace Csharp14NewFeatures
    {
        /// <summary>
        /// Provides generic mathematical constants via extension methods for numeric types.
        /// </summary>
        public static class MathConstantExtensions
        {
            public static T GetPi<T>(this T _) where T : INumber<T> =>
                T.CreateChecked(Math.PI);

            public static T GetTau<T>(this T _) where T : INumber<T> =>
                T.CreateChecked(2 * Math.PI);

            public static T GetEuler<T>(this T _) where T : INumber<T> =>
                T.CreateChecked(Math.E);

            public static T GetPhi<T>(this T _) where T : INumber<T> =>
                T.CreateChecked((1 + Math.Sqrt(5)) / 2);

            public static T GetSqrt2<T>(this T _) where T : INumber<T> =>
                T.CreateChecked(Math.Sqrt(2));

            public static T GetSqrt3<T>(this T _) where T : INumber<T> =>
                T.CreateChecked(Math.Sqrt(3));

            public static T GetLn2<T>(this T _) where T : INumber<T> =>
                T.CreateChecked(Math.Log(2));

            public static T GetLn10<T>(this T _) where T : INumber<T> =>
                T.CreateChecked(Math.Log(10));

            public static T GetDeg2Rad<T>(this T _) where T : INumber<T> =>
                T.CreateChecked(Math.PI / 180.0);

            public static T GetRad2Deg<T>(this T _) where T : INumber<T> =>
                T.CreateChecked(180.0 / Math.PI);
        }
    }
}

Example usage of the code above :


  #region Extension metmbers using block syntax - Math

  //Extension properties 
  double radians = double.Pi / 3.0; // Pi/3 radians = 60 degrees (1 * Pi = 180 degrees) 
  double degrees = radians * radians.Rad2Deg; // Using the extension method Rad2Deg

  Console.WriteLine($"Radians: {radians:F6}"); //outputs 1.04719..
  Console.WriteLine($"Degrees: {degrees:F6}"); //outputs 60

  //Using Extension methods 

    //Using Extension methods 

  double radiansV2 = 1.0.GetPi() / 3.0;
  double degreesV2 = radians * 1.0.GetRad2Deg();

  Console.WriteLine($"Radians: {radiansV2:F6}");
  Console.WriteLine($"Degrees: {degreesV2:F6}");

Output of the code usage above:


Radians: 1,047198
Degrees: 60,000000
Radians: 1,047198
Degrees: 60,000000

So to sum up, if you use extension blocks in C#, you can use them together with generics, but the extension block must be defined to a concrete type, not using generics. This will result in some cases in lengthier code, as we cannot use generics as much as extension methods allows. Note that extension methods also must be defined inside a non-generic class.

Saturday, 22 November 2025

C# 14 - Null-conditional assignments

What's new in C#

With .NET 10 released in November 2025, new features of C# is available.

Null-conditional assignment

In C# 14, Null-conditional assignment allows using the null-conditional member access operator on the left and side of assignment.

This allows more compact code, but also at the same time allow the code to become a bit more indeterministic since the code will not be run if the object on the left side of the assignment is null.

Consider this simple class :


public class AnotherClass
{
  public ShipmentService ShipmentService = new ShipmentService();
  public Order? CurrentOrder { get; set; }  
  public int? Counter { get; set; } = 0;   
}


public class ShipmentService
{
    public Order? GetCurrentOrder()
    {
        // Simulate fetching the current order, which may return null
        return null; // or return new Order { OrderId = 1234 };
    }
}

public class Order
{
    public int OrderId { get; set; }
}

We do a null check on the instance of <em>AnotherClass</em> ShipmentService here on the left side.


//Demonstrate AnotherClass using null-conditional assignment 
AnotherClass? anotherClass = null;
anotherClass?.CurrentOrder = anotherClass?.ShipmentService.GetCurrentOrder();
Console.WriteLine($"Current order retrieved using null-conditional assignment: {anotherClass?.CurrentOrder?.OrderId} Current order id is NULL? {anotherClass?.CurrentOrder?.OrderId is null}");

It is also possible to use the null check of the null-conditional assignment with compound assignment operators. The compound operators are += and -= . Note that this must be done with member access and cannot be used with values such as integers for example.


//anotherClass still NULL
anotherClass?.Counter += 2;
Console.WriteLine($"anotherClass.Counter = {anotherClass?.Counter}. Is anotherClass.Counter NULL ? {anotherClass?.Counter is null} : outputs NULL since anotherClass is still null");
anotherClass = new AnotherClass();
anotherClass?.Counter -= 15;
Console.WriteLine($"anotherClass.Counter = {anotherClass?.Counter} : outputs -15 since anotherClass is not null"); 
Output of the code above:

Current order retrieved using null-conditional assignment:  Current order id is NULL? True
anotherClass.Counter = . Is anotherClass.Counter NULL ? True : outputs NULL since anotherClass is still null
anotherClass.Counter = -15 : outputs -15 since anotherClass is not null

Saturday, 9 August 2025

Testing API resilience with Polly Chaos engine

Polly is a transient failure handling and resilience library that makes it convenient to build robust APIs based on policies for handling errors that occur and offer different resilience strategies for handling these errors. The errors are not only of errors occurings either externall or internally in API, but offer also alternative strategies such as fallbacks, rate-limiting, circuit breakers and other overall act upon either reactively or proactively. A great overview of Polly can be seen in this video, although some years old now - back to 2019 - of getting an overview of Polly : NDC Oslo 2019 - System Stable: Robust connected applications with Polly, the .NET Resilience Framework - Bryan Hogan With Polly, it is possible to test out API resilience with the built in Polly Chaos engine. The Chaos engine was previously offered via the Simmy library.

Simmy - Logo



The source code in this article is available in my Github repo here: Note - the code shown in the methods below are called from Program.cs to be able to be used in the API. The sample app is an Asp.net application written with C# and with .NET 8 Target Framework. https://github.com/toreaurstadboss/HttpClientUsingPolly/

Testing out API resilience with fallbacking API endpoints

First off, the fallback strategy resilience. Polly offers a way to define fallback policies. Let's look at a way to define an HTTP client that will provide a fallback if the statuscode from the endpoint is InternalServerError = 501. The fallback is just a Json payload in this simple example.

PollyExtensions.cs



    public static void AddPollyHttpClientWithFallback(this IServiceCollection services)
    {
        services.AddHttpClient(Constants.HttpClientNames.FallbackHttpClientName, client =>
        {
            client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MyApp", "1.0"));
        })
        .AddResilienceHandler(
             $"{FallbackHttpClientName}{ResilienceHandlerSuffix}",
            (builder, context) =>
        {
            var serviceProvider = services.BuildServiceProvider();
            var logger = serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger(Constants.LoggerName);

            builder.AddFallback(new FallbackStrategyOptions<HttpResponseMessage>
            {
                ShouldHandle = args =>
                {
                    // Fallback skal trigges ved status 500 eller exception
                    return ValueTask.FromResult(
                        args.Outcome.Result?.StatusCode == HttpStatusCode.InternalServerError ||
                        args.Outcome.Exception is HttpRequestException
                    );
                },
                FallbackAction = args =>
                {
                    logger.LogWarning("Fallback triggered. Returning default response.");

                    var jsonObject = new
                    {
                        message = "Fallback response",
                        source = "Polly fallback",
                        timestamp = DateTime.UtcNow
                    };

                    var json = JsonSerializer.Serialize(jsonObject);

                    var fallbackResponse = new HttpResponseMessage(HttpStatusCode.OK)
                    {
                        Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json")
                    };

                    return ValueTask.FromResult(Outcome.FromResult(fallbackResponse));
                }
            });

            // Inject exceptions in 80% of requests
            builder.AddChaosOutcome(new ChaosOutcomeStrategyOptions<HttpResponseMessage>()
            {
                Enabled = true,
                OutcomeGenerator = static args =>
                {
                    var response = new HttpResponseMessage(HttpStatusCode.InternalServerError);
                    return ValueTask.FromResult<Outcome<HttpResponseMessage>?>(Outcome.FromResult(response));
                },
                InjectionRate = 0.8,
                OnOutcomeInjected = args =>
                {
                    logger.LogWarning("Outcome returning internal server error");
                    return default;
                }
            });

        });
    }


Next up, let's look at the client endpoint defined with Minimal API in Aspnet core.

SampleEndpoints.cs



 app.MapGet("/test-v5-fallback", async (
 [FromServices] IHttpClientFactory httpClientFactory) =>
 {
     using var client = httpClientFactory.CreateClient(Constants.HttpClientNames.FallbackHttpClientName);

     HttpResponseMessage? response = await client.GetAsync("https://example.com");

     if (!response.IsSuccessStatusCode)
     {
         var errorContent = await response.Content.ReadAsStringAsync();
         return Results.Problem(
             detail: $"Request failed with status code {(int)response.StatusCode}: {response.ReasonPhrase}",
             statusCode: (int)response.StatusCode,
             title: "External API Error"
         );
     }

     var json = await response!.Content.ReadAsStringAsync();
     return Results.Json(json);

 });



Note the usage of [FromServices] attribute and IHttpClientFactory. The code creates the named Http client defined earlier. The fallback will return a json with fallback content in 80% of the requests in this concrete example.


Testing out API resilience with circuit breaking API endpoints


Next, the circuit breaker strategy for API resilience. Polly offers a way to define circuit breaker policies. Let's look at a way to define an HTTP client that will provide a circuit breaker if it fails for 3 consecutive requests within 30 seconds, resulting in a 10 second break. The circuit breaker strategy will stop requests that opens the circuit defined here. After the break, the circuit breaker half opens. It will accept new request, but fail immediately and open up the circuit again if it fails again, further postponing.

PollyExtensions.cs



  public static void AddPollyHttpClientWithExceptionChaosAndBreaker(this IServiceCollection services)
  {

      services.AddHttpClient(CircuitBreakerHttpClientName, client =>
      {
          client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MyApp", "1.0"));
      })
      .AddResilienceHandler(
          $"{CircuitBreakerHttpClientName}{ResilienceHandlerSuffix}",
          (builder, context) =>
      {
          var serviceProvider = services.BuildServiceProvider();
          var logger = serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger(Constants.LoggerName);

          //Add circuit breaker that opens after three consecutive failures and breaks for a duration of ten seconds
          builder.AddCircuitBreaker(new CircuitBreakerStrategyOptions<HttpResponseMessage>
          {
              MinimumThroughput = 3, //number of CONSECUTIVE requests failing for circuit to open (short-circuiting future requests until given BreakDuration is passed)
              FailureRatio = 1.0, //usually 1.0 is used here..
              SamplingDuration = TimeSpan.FromSeconds(30), //time window duration to look for CONSECUTIVE requests failing
              BreakDuration = TimeSpan.FromSeconds(10), //break duration. requests will be hindered at during this duration set 
              ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
                  .HandleResult(r => !r.IsSuccessStatusCode)
                  .Handle<HttpRequestException>(), //defining when circuit breaker will occur given other conditions also apply
              OnOpened = args =>
              {
                  logger.LogInformation("Circuit breaker opened");
                  return default;
              },
              OnClosed = args =>
              {
                  logger.LogInformation("Circuit breaker closed");
                  return default;
              },
              OnHalfOpened = args =>
              {
                  logger.LogInformation("Circuit breaker half opened"); //half opened state happens after the circuit has been opened and break duration has passed, entering 'half-open' state (usually ONE test call must succeeed to transition from half open to open state)
                  return default;
              }
          });


          // Inject exceptions in 80% of requests
          builder.AddChaosOutcome(new ChaosOutcomeStrategyOptions<HttpResponseMessage>()
          {
              Enabled = true,
              OutcomeGenerator = static args =>
              {
                  var response = new HttpResponseMessage(HttpStatusCode.InternalServerError);
                  return ValueTask.FromResult<Outcome<HttpResponseMessage>?>(Outcome.FromResult(response));
              },
              InjectionRate = 0.8,
              OnOutcomeInjected = args =>
              {
                  logger.LogWarning("Outcome returning internal server error");
                  return default;
              }
          });


      });
  }



Let's look at the client endpoint defined with Minimal API in Aspnet core for circuit-breaker example.

SampleEndpoints.cs



  app.MapGet("/test-v4-circuitbreaker-opening", async (
  [FromServices] IHttpClientFactory httpClientFactory) =>
  {
      using var client = httpClientFactory.CreateClient(Constants.HttpClientNames.CircuitBreakerHttpClientName);

      HttpResponseMessage? response = await client.GetAsync("https://example.com");

      if (!response.IsSuccessStatusCode)
      {
          var errorContent = await response.Content.ReadAsStringAsync();
          return Results.Problem(
              detail: $"Request failed with status code {(int)response.StatusCode}: {response.ReasonPhrase}",
              statusCode: (int)response.StatusCode,
              title: "External API Error"
          );
      }

      var json = await response!.Content.ReadAsStringAsync();
      return Results.Json(json);

  });



Testing out API resilience for latency induced timeout API endpoints

Next, the timeout strategy for API resilience. Polly offers a way to define timeout policies and can combine these for testing by injecting latency (additional execution time). Let's look at a way to define an HTTP client that will provide a timeout if it times out already after one second with a 50% chance of getting a 3 second latency, which will trigger the timeout.

PollyExtensions.cs



   public static void AddPollyHttpClientWithIntendedRetriesAndLatencyAndTimeout(this IServiceCollection services)
  {
      services.AddHttpClient(RetryingTimeoutLatencyHttpClientName, client =>
      {
          client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MyApp", "1.0"));
      })
      .AddResilienceHandler(
          $"{RetryingTimeoutLatencyHttpClientName}{ResilienceHandlerSuffix}",
      (builder, context) =>
       {
           var serviceProvider = services.BuildServiceProvider();
           var logger = serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger(Constants.LoggerName);

           // Timeout strategy : fail if request takes longer than 1s
           builder.AddTimeout(new HttpTimeoutStrategyOptions
           {
               Timeout = TimeSpan.FromSeconds(1),
               OnTimeout = args =>
               {
                   logger.LogWarning($"Timeout after {args.Timeout.TotalSeconds} seconds");
                   return default;
               }
           });

           // Chaos latency: inject 3s delay in 30% of cases
           builder.AddChaosLatency(new ChaosLatencyStrategyOptions
           {
               InjectionRate = 0.5,
               Latency = TimeSpan.FromSeconds(3),
               Enabled = true,
               OnLatencyInjected = args =>
               {
                   logger.LogInformation("... Injecting a latency of 3 seconds ...");
                   return default;
               }
           });

           // Chaos strategy: inject 500 Internal Server Error in 75% of cases
           builder.AddChaosOutcome<HttpResponseMessage>(
               new ChaosOutcomeStrategyOptions<HttpResponseMessage>
               {
                   InjectionRate = 0.5,
                   Enabled = true,
                   OutcomeGenerator = static args =>
                   {
                       var response = new HttpResponseMessage(HttpStatusCode.InternalServerError);
                       return ValueTask.FromResult<Outcome<HttpResponseMessage>?>(Outcome.FromResult(response));
                   },
                   OnOutcomeInjected = args =>
                   {
                       logger.LogWarning("Outcome returning internal server error");
                       return default;
                   }
               });
       });

  }


Let's look at the client endpoint defined with Minimal API in Aspnet core for timeout with latency example.

SampleEndpoints.cs



 app.MapGet("/test-v3-latency-timeout", async (
 [FromServices] IHttpClientFactory httpClientFactory) =>
 {
     using var client = httpClientFactory.CreateClient(Constants.HttpClientNames.RetryingTimeoutLatencyHttpClientName);

     var response = await client.GetAsync("https://example.com");

     if (!response.IsSuccessStatusCode)
     {
         var errorContent = await response.Content.ReadAsStringAsync();
         return Results.Problem(
             detail: $"Request failed with status code {(int)response.StatusCode}: {response.ReasonPhrase}",
             statusCode: (int)response.StatusCode,
             title: "External API Error"
         );
     }

     var json = await response.Content.ReadAsStringAsync();
     return Results.Json(json);

 });



The following screenshot shows the timeout occuring after the defined setup of induced latency by given probability and defined timeout.

Testing out API resilience with retries

Retries offers an API endpoint to gain more robustness, by allowing multiple retries and define a strategy for these retries.The example http client here also adds a chaos outcome internal server error = 501 that is thrown with 75% probability (failure rate).

PollyExtensions.cs



    public static void AddPollyHttpClientWithIntendedRetries(this IServiceCollection services)
   {
       services.AddHttpClient(RetryingHttpClientName, client =>
           {
               client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("MyApp", "1.0"));
           })
           .AddResilienceHandler("polly-chaos", (builder, context) =>
           {
               var serviceProvider = services.BuildServiceProvider();
               var logger = serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger(Constants.LoggerName);

               //Retry strategy
               builder.AddRetry(new RetryStrategyOptions<HttpResponseMessage>
               {
                   ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
                       .HandleResult(r => !r.IsSuccessStatusCode)
                       .Handle<HttpRequestException>(),
                   MaxRetryAttempts = 3,
                   DelayGenerator = RetryDelaysPipeline,
                   OnRetry = args =>
                   {
                       logger.LogWarning($"Retrying {args.AttemptNumber} for requesturi {args.Context.GetRequestMessage()?.RequestUri}");
                       return default;
                   }
               });

               // Chaos strategy: inject 500 Internal Server Error in 75% of cases
               builder.AddChaosOutcome<HttpResponseMessage>(
                   new ChaosOutcomeStrategyOptions<HttpResponseMessage>
                   {
                       InjectionRate = 0.75,
                       Enabled = true,
                       OutcomeGenerator = static args =>
                       {
                           var response = new HttpResponseMessage(HttpStatusCode.InternalServerError);
                           return ValueTask.FromResult<Outcome<HttpResponseMessage>?>(Outcome.FromResult(response));
                       }
                   });

           });

   }


Let's look at the client endpoint defined with Minimal API in Aspnet core for the retrying example.

SampleEndpoints.cs


   app.MapGet("/test-retry-v2", async (
       [FromServices] IHttpClientFactory httpClientFactory) =>
   {
       using var client = httpClientFactory.CreateClient(Constants.HttpClientNames.RetryingHttpClientName);

       var response = await client.GetAsync("https://example.com");

       return Results.Json(response);
   });


There are multiple resilience scenarios that Polly offers, the table below lists them up (this article has presented most of them):

Summary

The following summary explains what this article has presented.

πŸ§ͺ Testing API Resilience with Polly Chaos Engineering In this article, we have explored how to build resilient APIs using Polly v9 and its integrated chaos engine. Through practical examples, the article has demonstrated how to simulate real-world failures—like latency, timeouts, and internal server errors—and apply resilience strategies such as fallbacks, retries, and circuit breakers. By injecting controlled chaos, developers can proactively test and strengthen their systems against instability and external dependencies. Polly v9 library offers additionally scenarios also for making robust APIs. Info: Asp net core is used in this article, together with C# and .NET 8 as Target Framework.