This is quite hard, and needs heavy usage of reflection to workaround limitations of .net framework.
As you pointed, you can disassemble ParameterBuilder.setConstant. This methods invokes an internal method:
[SecuritySafeCritical]
public virtual void SetConstant(object defaultValue)
{
TypeBuilder.SetConstantValue(this.m_methodBuilder.GetModuleBuilder(), this.m_pdToken.Token, (this.m_iPosition == 0) ? this.m_methodBuilder.ReturnType : this.m_methodBuilder.m_parameterTypes[this.m_iPosition - 1], defaultValue);
}
which you can also disassemble, and see where the exception is thrown from (when type is a value type):
if (destType.IsValueType && (!destType.IsGenericType || !(destType.GetGenericTypeDefinition() == typeof(Nullable<>))))
{
throw new ArgumentException(Environment.GetResourceString("Argument_ConstantNull"));
}
TypeBuilder.SetConstantValue(module.GetNativeHandle(), tk, 18, null);
Fortunately, you can invoke the same methods here, but dynamically from mscorlib:
AssemblyName aName = new AssemblyName("DynamicAssemblyExample");
AssemblyBuilder ab =
AppDomain.CurrentDomain.DefineDynamicAssembly(aName, AssemblyBuilderAccess.RunAndSave);
ModuleBuilder mb =
ab.DefineDynamicModule(aName.Name, aName.Name + ".dll");
TypeBuilder tb = mb.DefineType("MyClass", TypeAttributes.Public);
MethodBuilder meb = tb.DefineMethod("Foo", MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.HideBySig, typeof(void), new Type[] { typeof(TimeSpan) });
ParameterBuilder pb = meb.DefineParameter(1, ParameterAttributes.Optional | ParameterAttributes.HasDefault, "ts");
MethodInfo getNativeHandle = typeof(ModuleBuilder).GetMethod("GetNativeHandle", BindingFlags.NonPublic | BindingFlags.Instance);
object nativeHandle = getNativeHandle.Invoke(mb, new object[0]);
int tk = pb.GetToken().Token;
MethodInfo setConstantValue = typeof(TypeBuilder).GetMethods(BindingFlags.NonPublic | BindingFlags.Static).Where(mi => mi.Name == "SetConstantValue" && mi.GetParameters().Last().ParameterType.IsPointer).First();
setConstantValue.Invoke(pb, new object[] { nativeHandle, tk, /* CorElementType.Class: */ 18, null });
ILGenerator ilgen = meb.GetILGenerator();
FieldInfo fi = typeof(ILGenerator).GetField("m_maxStackSize", BindingFlags.NonPublic | BindingFlags.Instance);
fi.SetValue(ilgen, 8);
ilgen.Emit(OpCodes.Ret);
tb.CreateType();
ab.Save("DynamicAssemblyExample.dll");
Setting default value this way won't update the stacksize, which means you have to set it manually (again through reflection), right after getting the ILGenerator:
FieldInfo fi = typeof(ILGenerator).GetField("m_maxStackSize", BindingFlags.NonPublic | BindingFlags.Instance);
fi.SetValue(ilgen, 8);
This generates the following IL:
.method public hidebysig static
void Foo (
[opt] valuetype [mscorlib]System.TimeSpan ts
) cil managed
{
.param [1] = nullref
// Method begins at RVA 0x2050
// Code size 1 (0x1)
.maxstack 8
IL_0000: ret
} // end of method MyClass::Foo
which is the same thing as what the C# you provided compiles to.