You can use an attached property like this:
public static class TextBlock
{
    public static readonly DependencyProperty CharacterCasingProperty = DependencyProperty.RegisterAttached(
        "CharacterCasing",
        typeof(CharacterCasing),
        typeof(TextBlock),
        new FrameworkPropertyMetadata(
            CharacterCasing.Normal,
            FrameworkPropertyMetadataOptions.Inherits | FrameworkPropertyMetadataOptions.NotDataBindable,
            OnCharacterCasingChanged));
    private static readonly DependencyProperty TextProxyProperty = DependencyProperty.RegisterAttached(
        "TextProxy",
        typeof(string),
        typeof(TextBlock),
        new PropertyMetadata(default(string), OnTextProxyChanged));
    private static readonly PropertyPath TextPropertyPath = new PropertyPath("Text");
    public static void SetCharacterCasing(DependencyObject element, CharacterCasing value)
    {
        element.SetValue(CharacterCasingProperty, value);
    }
    public static CharacterCasing GetCharacterCasing(DependencyObject element)
    {
        return (CharacterCasing)element.GetValue(CharacterCasingProperty);
    }
    private static void OnCharacterCasingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is System.Windows.Controls.TextBlock textBlock)
        {
            if (BindingOperations.GetBinding(textBlock, TextProxyProperty) == null)
            {
                BindingOperations.SetBinding(
                    textBlock,
                    TextProxyProperty,
                    new Binding
                    {
                        Path = TextPropertyPath,
                        RelativeSource = RelativeSource.Self,
                        Mode = BindingMode.OneWay,
                    });
            }
        }
    }
    private static void OnTextProxyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        d.SetCurrentValue(System.Windows.Controls.TextBlock.TextProperty, Format((string)e.NewValue, GetCharacterCasing(d)));
        string Format(string text, CharacterCasing casing)
        {
            if (string.IsNullOrEmpty(text))
            {
                return text;
            }
            switch (casing)
            {
                case CharacterCasing.Normal:
                    return text;
                case CharacterCasing.Lower:
                    return text.ToLower();
                case CharacterCasing.Upper:
                    return text.ToUpper();
                default:
                    throw new ArgumentOutOfRangeException(nameof(casing), casing, null);
            }
        }
    }
}
Then usage in xaml will look like:
<StackPanel>
    <TextBox x:Name="TextBox" Text="abc" />
    <TextBlock local:TextBlock.CharacterCasing="Upper" Text="abc" />
    <TextBlock local:TextBlock.CharacterCasing="Upper" Text="{Binding ElementName=TextBox, Path=Text}" />
    <Button local:TextBlock.CharacterCasing="Upper" Content="abc" />
    <Button local:TextBlock.CharacterCasing="Upper" Content="{Binding ElementName=TextBox, Path=Text}" />
</StackPanel>