A lot of the time when creating domain models you want to avoid using simple types such as string, int, byte etc. This allows you to add additional constraints and enforce immutability of the values after construction (also helps with Intellisense). In other scenarios you just want to wrap a value type in an object so that it can be treated like an object (boxing) and maintain strong typing. Generally you end up creating a "Wrapper<T>" or "ValueObject<T>" as a base class, or even a concrete class, for wrapping your other type but you'd like to treat it like it's still the underlying type when using it in your code (such as when calling ToString() or when comparing for equality - in the various different manners that this can be done). I've written a base class which I inherit from when I want to wrap a simple type in this way. The base class allows me to easily cast to/from the underlying type, it redirects ToString the original type, redirects GetHashCode to the original type and redirects equality checks to the original type. Code below:
public abstract class ValueObject<T> : IEquatable<ValueObject<T>>
{
    private readonly T value;

    protected ValueObject(T value)
    {
        this.value = value;
    }

    public static bool operator ==(ValueObject<T> value1, ValueObject<T> value2)
    {
        if (ReferenceEquals(value1, value2))
        {
            return true;
        }

        if (value1 is null || value2 is null)
        {
            return false;
        }

        return value1.Equals(value2);
    }

    public static bool operator !=(ValueObject<T> value1, ValueObject<T> value2)
    {
        return !(value1 == value2);
    }

    public static implicit operator T(ValueObject<T> valueObject)
    {
        return valueObject.InnerValue();
    }

    public T InnerValue()
    {
        return this.value;
    }

    public override bool Equals(object obj)
    {
        if (!(obj is ValueObject<T> compareTo))
        {
            return false;
        }

        return this.Equals(compareTo);
    }

    public override int GetHashCode()
    {
        return this.InnerValue().GetHashCode();
    }

    public override string ToString()
    {
        return this.InnerValue().ToString();
    }

    public virtual bool Equals(ValueObject<T> other)
    {
        return !(other is null) && this.InnerValue().Equals(other.InnerValue());
    }
}
What that means is I can now create a concrete complex type which wraps a simple type and treat it as the underlying type:
public class Age : ValueObject<int>
{
    public Age(int value) : base(value)
    {
        if (value < 18 || value > 60)
        {
            throw new ArgumentOutOfRangeException("Age must be between 18 and 60.");
        }
    }
}
In this example I've created "Age" to add some domain rules around "int" - but I can still easily compare two "Age" instances as if they were ints (by ==, .Equals or any checks that rely on the interfaces for equality). If I plug an instance into a string interpolation statement I'll get the int value not the type name. I can add them into a Dictionary based on the int value for keys etc.