C#: "Pretty" type name function?

Viewed 5967

The name properties of System.Type class return a strange result in case of generic types. Is there a way to get the type name in a format closer to the way I specified it? Example: typeof(List<string>).OriginalName == "List<string>"

13 Answers

A minimal work solution that leverages CodeDomProvider is to control how the CodeTypeReference instance is built in the first place. There are special cases only for generic types and multi-rank arrays, so we only have to care about those:

static CodeTypeReference CreateTypeReference(Type type)
{
    var typeName = (type.IsPrimitive || type == typeof(string)) ? type.FullName : type.Name;
    var reference = new CodeTypeReference(typeName);
    if (type.IsArray)
    {
        reference.ArrayElementType = CreateTypeReference(type.GetElementType());
        reference.ArrayRank = type.GetArrayRank();
    }

    if (type.IsGenericType)
    {
        foreach (var argument in type.GetGenericArguments())
        {
            reference.TypeArguments.Add(CreateTypeReference(argument));
        }
    }
    return reference;
}

Using this modified factory method, it is then possible to use the appropriate code provider to get pretty typing, like so:

using (var provider = new CSharpCodeProvider())
{
    var reference = CreateTypeReference(typeof(IObservable<IEnumerable<Tuple<int?, string>>>[,]));
    var output = provider.GetTypeOutput(reference);
    Console.WriteLine(output);
}

The above code returns IObservable<IEnumerable<Tuple<Nullable<int>, string>>>[,]. The only special case that is not handled well are Nullable types but this is really more a fault of the CodeDomProvider than anything else.

I do it like this ..

public static class TypeExtensions {
    public static String GetName(this Type t) {
        String readable;

#if PREVENT_RECURSION
        if(m_names.TryGetValue(t, out readable)) {
            return readable;
        }
#endif

        var tArgs = t.IsGenericType ? t.GetGenericArguments() : Type.EmptyTypes;
        var name = t.Name;
        var format = Regex.Replace(name, @"`\d+.*", "")+(t.IsGenericType ? "<?>" : "");
        var names = tArgs.Select(x => x.IsGenericParameter ? "" : GetName(x));
        readable=String.Join(String.Join(",", names), format.Split('?'));

#if PREVENT_RECURSION
        m_names.Add(t, readable);
#endif

        return readable;
    }

    static readonly Dictionary<Type, String> m_names = new Dictionary<Type, String> { };
}

where PREVENT_RECURSION should be defined true if you don't want the type in generic type arguments be resolved recursively every time, just take from the cache dictionary; leave it undefined of false if you don't care that.

Combined a few answers, added support for nested types and array ranks, and turned it into an extension method with cached resolved names.

/// <summary>
/// Extension methods for <see cref="Type"/>.
/// </summary>
public static class TypeExtensions
{
    /// <summary>
    /// Dictionary of type names.
    /// </summary>
    private static readonly ConcurrentDictionary<Type, string> typeNames;

    /// <summary>
    /// Initializes static members of the <see cref="TypeExtensions"/> class.
    /// </summary>
    static TypeExtensions()
    {
        typeNames = new ConcurrentDictionary<Type, string>
                        {
                            [typeof(bool)] = "bool",
                            [typeof(byte)] = "byte",
                            [typeof(char)] = "char",
                            [typeof(decimal)] = "decimal",
                            [typeof(double)] = "double",
                            [typeof(float)] = "float",
                            [typeof(int)] = "int",
                            [typeof(long)] = "long",
                            [typeof(sbyte)] = "sbyte",
                            [typeof(short)] = "short",
                            [typeof(string)] = "string",
                            [typeof(uint)] = "uint",
                            [typeof(ulong)] = "ulong",
                            [typeof(ushort)] = "ushort"
                        };
    }

    /// <summary>
    /// Gets the type name with generics and array ranks resolved.
    /// </summary>
    /// <param name="type">
    /// The type whose name to resolve.
    /// </param>
    /// <returns>
    /// The resolved type name.
    /// </returns>
    public static string ToCSTypeName(this Type type)
    {
        return typeNames.GetOrAdd(type, GetPrettyTypeName);
    }

    /// <summary>
    /// Gets the type name as it would be written in C#
    /// </summary>
    /// <param name="type">
    /// The type whose name is to be written.
    /// </param>
    /// <returns>
    /// The type name as it is written in C#
    /// </returns>
    private static string GetPrettyTypeName(Type type)
    {
        var typeNamespace = type.DeclaringType != null ? ToCSTypeName(type.DeclaringType) : type.Namespace;
        if (type.IsGenericType)
        {
            if (type.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                return $"{ToCSTypeName(Nullable.GetUnderlyingType(type))}?";
            }

            var typeList = string.Join(", ", type.GenericTypeArguments.Select(ToCSTypeName).ToArray());
            var typeName = type.Name.Split('`')[0];

            return $"{typeNamespace}.{typeName}<{typeList}>";
        }

        if (type.IsArray)
        {
            var arrayRank = string.Empty.PadLeft(type.GetArrayRank() - 1, ',');
            var elementType = ToCSTypeName(type.GetElementType());
            return $"{elementType}[{arrayRank}]";
        }

        return $"{typeNamespace}.{type.Name}";
    }
}
public static string pretty_name( Type type, int recursion_level = -1, bool expand_nullable = false )
{
    if( type.IsArray )
    {
        return $"{pretty_name( type.GetElementType(), recursion_level, expand_nullable )}[]";
    }

    if( type.IsGenericType )
    {
        // find generic type name
        var gen_type_name = type.GetGenericTypeDefinition().Name;
        var index = gen_type_name.IndexOf( '`' );
        if( index != -1 )
            gen_type_name = gen_type_name.Substring( 0, index );

        // retrieve generic type aguments
        var arg_names = new List<string>();
        var gen_type_args = type.GetGenericArguments();
        foreach( var gen_type_arg in gen_type_args )
        {
            arg_names.Add(
                recursion_level != 0
                    ? pretty_name( gen_type_arg, recursion_level - 1, expand_nullable )
                    : "?" );
        }

        // if type is nullable and want compact notation '?'
        if( !expand_nullable && Nullable.GetUnderlyingType( type ) != null )
            return $"{arg_names[ 0 ]}?";

        // compose common generic type format "T<T1, T2, ...>"
        return $"{gen_type_name}<{string.Join( ", ", arg_names )}>";
    }

    return type.Name;
}

My two cents:

  • Everything is done through the Type interface
  • No Regex
  • No extra objects created except for the list that holds generic argument names
  • Supports infinite recursion or recursion to a certain level (or no recursion at all!)
  • Supports nullable types (both formats "Nullable<>" and "?")
  • Supports ranked arrays
Related