Search code examples
c#reflectionoptional-parametersvalue-typebase-class-library

Getting DefaultValue for optional Guid through reflection?


I have the following code, that I use as sample for illustrating different scenarios:

    public static void MethodWithOptionalGuid(Guid id = default(Guid)) { }

    public static void MethodWithOptionalInteger(int id = 2) { }

    public static void MethodWithOptionalString(string id = "33344aaa") { }

    public static void MethodWithoutOptionalParameter(int id, Guid longId) { }

    static void Main(string[] args)
    {
        var methods = typeof(Program).GetMethods(BindingFlags.Public | BindingFlags.Static).ToList();

        foreach (var method in methods)
        {
            PrintMethodDetails(method);
        }

        Console.ReadLine();
    }

    static void PrintMethodDetails(MethodInfo method)
    {
        Console.WriteLine(method.Name);

        foreach (var parameter in method.GetParameters().ToList())
        {
            Console.WriteLine(parameter.Name +
                              " of type " + parameter.ParameterType.ToString() +
                              " with default value:" + parameter.DefaultValue);
        }

        Console.WriteLine();
    }

It prints the following:

MethodWithOptionalGuid
id of type System.Guid with default value:

MethodWithOptionalInteger
id of type System.Int32 with default value:2

MethodWithOptionalString
id of type System.String with default value:33344aaa

MethodWithoutOptionalParameter
id of type System.Int32 with default value:
longId of type System.Guid with default value:

The output seems fine for the last 3 methods.

My question is regarding the first one, MethodWithOptionalGuid: why the Guid's default value is not recognized?

I expected to receive something like "0000000-..." . I also tried initializing the optional parameter with new Guid() and same result. I tried as well other structs, like TimeSpan and the behavior is the same.

I expected that all value types would behave the same (as seen in integer example).

Extra: I found this thing while trying to use in Asp.Net MVC an action with an optional Guid parameter and failed (had to make the Guid nullable). Went through MVC code and found that it uses the DefaultValue at some point. So I made this code sample to better illustrate my issue.


Solution

  • The why is pretty relevant and something you really need to heed when you want to do something like this. Dragons live here. If you use ildasm.exe to look at the method then you'll see:

      .param [1] = nullref
    

    A null as a default for a structure type, uh-oh. The semantics for the .param directive is described in the CLI spec, Ecma-335, chapter II.15.4.1.4. It is very short, I'll copy-paste the entire chapter (edited for readability)

    MethodBodyItem ::= .param ‘[’ Int32 ‘]’ [ ‘=’ FieldInit ]

    This directive stores in the metadata a constant value associated with method parameter number Int32, see §II.22.9. While the CLI requires that a value be supplied for the parameter, some tools can use the presence of this attribute to indicate that the tool rather than the user is intended to supply the value of the parameter. Unlike CIL instructions, .param uses index 0 to specify the return value of the method, index 1 to specify the first parameter of the method, index 2 to specify the second parameter of the method, and so on.

    [Note: The CLI attaches no semantic whatsoever to these values—it is entirely up to compilers to implement any semantic they wish (e.g., so-called default argument values). end note]

    The [Note] is most relevant. When you use Reflection then you play the role of the compiler and it is up to you to interpret the default value correctly. In other words, you have to know that in C# the default value for a structure type is null.

    This is otherwise a limitation in what can be stored in a [FieldInit]. The initialization value must be stored in the metadata of the assembly and is subject to the same kind of limitations of an [attribute] declaration. Just simple value types can be used, not structures. So by convention the C# designers worked around this by using null instead. Good enough for the C# compiler to understand that default(Guid) was intended.

    That's not where the trouble ends, there are other ways to indicate an optional argument, the [OptionalAttribute] was used prior to C# v4.0. And is still used today, by COM interop libraries and other languages like VB.NET and C++/CLI. That's where the dragons live.

    Ignoring those beasts for now, workaround code for what you have could look like this:

        var def = parameter.DefaultValue;
        if (parameter.ParameterType.IsValueType && def == null) {
            def = Activator.CreateInstance(parameter.ParameterType);
        }
        Console.WriteLine(parameter.Name +
                          " of type " + parameter.ParameterType.ToString() +
                          " with default value:" + def);