Type.FullName returns null when ...

Type.FullName
returns null when the type is not generic type definition but contains generic paramters.

The rational behind this design is to ensure Type.FullName (if not null) can uniquely
identify a type in an assembly; or given the string returned by Type.FullName, Type.GetType(string
typeName) can return the original type back. It is hard to keep this invariant if

!type.IsGenericTypeDefinition && type.ContainsGenericParameters.

For instance, suppose we have an assembly compiled with the following C# code:

class G<T> {

    public void M<S>() { }

}

typeof(G<>).FullName is "G`1", and we can round trip this type from Type.GetType("G`1").
But we can build more complicated generic types, such as G<S> (type G<>
bound with the generic parameter from the method M<>); in order to identify
such type with a string, a lot of extra information is need.

Below are some examples where Type.FullName returns null.

class G<T> {

  public class C
{ }

  public void M(C
arg) { }

}

class G2<T> : G<T> { }

string s1 = typeof(G<>).GetGenericArguments()[0].FullName;

// T in G<T>: generic parameter

string s2 = typeof(G<>).GetMethod("M").GetParameters()[0].ParameterType.FullName;

// check out the IL, it is G`1/C<!T> (not generic type
definition)

// Related topic, see
this

string s3 = typeof(G2<>).BaseType.FullName;

// base type of G2<>, which is not generic type definition
either

// it equals to typeof(G<>).MakeGenericType(typeof(G2<>).GetGenericArguments()[0])