Skip to content

Use IStructurallyEqual in ChangeDetection #62523

@mrpmorris

Description

@mrpmorris

Is there an existing issue for this?

  • I have searched the existing issues

Is your feature request related to a problem? Please describe the problem.

There have been various suggestions in the past for ways to get Blazor to detect when the state of an instance of a reference type is unaltered and therefore a child will not need to re-render when its parent renders.

  1. Blazor change detection too liberal #13610
  2. Use IEquatable for Blazor object parameters to optimize rendering by default #40867
  3. Extend API for component attributes with render-comparer different from its value #21915

All have been closed, but I am here to hash out a new idea :)

Describe the solution you'd like

In the ChangeDetection class, have it also check if the new value implements IStructuralEquality. If so, then ask it if it is structurally equal to the old value.

That way, we can write code like this when working with immutable reference types.

public readonly record ImmutablePerson : IStructuralEquatable
{
    public required string FamilyName { get; init; }
    public required string PersonalName { get; init; }

    bool IStructuralEquatable.Equals(object other, IEqualityComparer comparer) =>
        ReferenceEquals(this, other);

    int IStructuralEquatable.GetHashCode(IEqualityComparer comparer) =>
        GetHashCode();
}

Or like this for mutable reference types (as Array already implements it).

public class Order : IStructuralEquatable
{
    public int Id { get; set; }
    public int CustomerId { get; set; }
    public OrderLine[] Lines { get; set; } = [];

    bool IStructuralEquatable.Equals(object? obj, IEqualityComparer comparer) =>
        obj is Order other
        && other.Id == Id
        && other.CustomerId == CustomerId
        && StructuralComparisons.StructuralEqualityComparer.Equals(Lines, other.Lines);

    int IStructuralEquatable.GetHashCode(IEqualityComparer comparer) =>
        HashCode.Combine(
            Id,
            CustomerId,
            StructuralComparisons.StructuralEqualityComparer.GetHashCode(Lines)
        );
}

public class OrderLine : IStructuralEquatable
{
    public int ProductId { get; set; }
    public uint Quantity { get; set; }

    bool IStructuralEquatable.Equals(object? obj, IEqualityComparer comparer) =>
        obj is OrderLine other
        && other.ProductId == ProductId
        && other.Quantity == Quantity;


    int IStructuralEquatable.GetHashCode(IEqualityComparer comparer) =>
        HashCode.Combine(ProductId, Quantity);
}

Additional context

I think the following changes to ChangeDetection.MayHaveChanged might suffice.

static bool MayHaveChanged<T1, T2>(T1 oldValue, T2 newValue)
{
    var oldIsNotNull = oldValue != null;
    var newIsNotNull = newValue != null;
    if (oldIsNotNull != newIsNotNull)
    {
        return true; // One's null and the other isn't, so different
    }
    else if (oldIsNotNull) // i.e., both are not null (considering previous check)
    {
        // *** New check for StructuralEquatable ***
        if (oldValue is IStructuralEquatable structuralEquatable)
        {
            return !structuralEquatable.Equals(newValue, EqualityComparer<T1>.Default);
        }

        var oldValueType = oldValue!.GetType();
        var newValueType = newValue!.GetType();
        if (oldValueType != newValueType            // Definitely different
            || !IsKnownImmutableType(oldValueType)  // Maybe different
            || !oldValue.Equals(newValue))          // Somebody says they are different
        {
            return true;
        }
    }

    // By now we know either both are null, or they are the same immutable type
    // and ThatType::Equals says the two values are equal.
    return false;
}

Note: Related issue in .Net runtime that would ensure this works for parameters of type List<T> etc - dotnet/runtime#117199

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-blazorIncludes: Blazor, Razor Components

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions