Skip to main content

Types and Reflection

These methods answer questions about a Type without hand-written reflection: what is its C# name, does it implement IRepository<>, is it a record, a nullable or a number, which attributes does it carry. Reach for them in mappers, serializers, DI registration, validation and error messages, wherever you inspect types at run time.

The Type and Guid methods live in the System namespace. The attribute methods (GetAttribute, GetAttributes, HasAttribute) live in System.Reflection, so they need using System.Reflection;, which reflection code usually has anyway.

At a glance​

MethodWhat it does
GetFriendlyNameGets the type name as you would write it in C#: Dictionary<string, List<int>>, int?, (int, string).
GetFullNameWithAssemblyNameGets "Full.Type.Name, AssemblyName" without version or public key.
IsAssignableTotrue if a value of the type can be assigned to the target type.
IsAssignableToGenericTypetrue if the type is, inherits from or implements an open generic type such as IRepository<>.
GetGenericArgumentsOfGets the type arguments the type supplies to an open generic type, such as Order for IRepository<Order>.
GetInterfacesAndAbstractClassesGets every interface the type implements plus its abstract base classes.
IsNullableValueTypetrue for Nullable<T> (int?, DateTime?), false for everything else.
UnwrapNullableGets T for Nullable<T>, or the type itself.
GetDefaultValueGets what default(T) would be, for a Type known only at run time.
IsNumerictrue for integer and floating-point types, decimal, Half, Int128, UInt128 and their nullables.
IsComplextrue for types with properties of their own; false for primitives, enums, string, dates, Guid, Uri.
IsRecordtrue for record classes and record structs.
IsAnonymoustrue for compiler-generated anonymous types (new { Id = 1 }).
IsStatictrue for static class types.
GetAttributeGets the single attribute of a given type applied to a member, or null.
GetAttributesGets every attribute of a given type applied to a member, in declaration order.
HasAttributetrue if the member has the attribute at least once.
ToShortIdEncodes a Guid as a 22-character URL-safe id instead of 36 characters.

Shared rules​

  • Every Type method throws ArgumentNullException (parameter name type) when the type is null. The one exception is IsAssignableToGenericType, which returns false.
  • The attribute methods throw ArgumentNullException (parameter name member) when the member is null.
  • Several methods carry [DynamicallyAccessedMembers] annotations, so the trimmer keeps the members they read. See Trimming and AOT.

The examples below use these domain types:

Domain types used on this page
namespace Shop.Orders;

public interface IEntity { }
public interface IAuditable : IEntity { }
public interface IRepository<TEntity> { }

public abstract class AggregateRoot { }

public sealed class Order : AggregateRoot, IAuditable
{
public Guid Id { get; set; }
public decimal Total { get; set; }
}

public sealed class OrderFilter
{
public decimal? MinTotal { get; set; }
}

public class RepositoryBase<TEntity> : IRepository<TEntity> { }
public sealed class OrderRepository : RepositoryBase<Order> { }

public record OrderPlaced(Guid OrderId, decimal Total);
public record struct Money(decimal Amount, string Currency);

Type names​

GetFriendlyName​

GetFriendlyName() returns the type name the way C# source writes it. Type.Name gives you Dictionary`2; this gives you Dictionary<string, List<int>>. Use it in log messages, exception messages and diagnostics. Namespaces are left out.

Readable type names
string lookup = typeof(Dictionary<string, List<int?>>).GetFriendlyName(); // → "Dictionary<string, List<int?>>"
string keyword = typeof(int).GetFriendlyName(); // → "int"
string nullable = typeof(Guid?).GetFriendlyName(); // → "Guid?"
string tuple = typeof((int Id, List<string> Names)?).GetFriendlyName(); // → "(int, List<string>)?"
string jagged = typeof(int[][,]).GetFriendlyName(); // → "int[][,]"
string task = typeof(Task<int>).GetFriendlyName(); // → "Task<int>"
string open = typeof(Dictionary<,>).GetFriendlyName(); // → "Dictionary<TKey, TValue>"
string domain = typeof(OrderRepository).GetFriendlyName(); // → "OrderRepository"

It also handles the less common shapes:

TypeResult
Nested type Outer<int>.Inner<string>"Outer<int>.Inner<string>"
A tuple with 15 elements"(int, int, ..., string)", flattened the way C# writes it
typeof(List<string>).MakeByRefType()"ref List<string>"
typeof(int).MakePointerType()"int*"
A generic parameter (TValue of Dictionary<,>)"TValue"
note

Tuple element names (Id, Names) are not part of the runtime type, so they do not appear in the result. Built-in types use their C# keywords: int, string, object, bool, decimal, void, nint and so on.

GetFullNameWithAssemblyName​

GetFullNameWithAssemblyName() returns "Full.Type.Name, AssemblyName": the full name plus the short assembly name, without version, culture or public key token. That string is enough for Type.GetType to load the type again, so it suits type names stored in configuration, message headers or a database.

Store a type name and load it back
string typeName = typeof(Order).GetFullNameWithAssemblyName(); // → "Shop.Orders.Order, Shop.Orders"
Type? loaded = Type.GetType(typeName); // → typeof(Order)

string text = typeof(string).GetFullNameWithAssemblyName(); // → "System.String, System.Private.CoreLib"

The first line assumes Order is compiled into an assembly named Shop.Orders. The last line shows the result on .NET 8 and later.

note

For a closed generic type, Type.FullName already includes the assembly-qualified names of the type arguments, so the result is long. Only the outer assembly name is shortened.

Assignability and generics​

IsAssignableTo​

IsAssignableTo<TTarget>() and IsAssignableTo(targetType) check whether a value of the type can be stored in a variable of the target type. They read in the natural direction: type.IsAssignableTo<IDisposable>() instead of typeof(IDisposable).IsAssignableFrom(type).

Check assignability
bool isEntity = typeof(Order).IsAssignableTo<IEntity>(); // → true (through IAuditable)
bool isRoot = typeof(Order).IsAssignableTo<AggregateRoot>(); // → true
bool boxes = typeof(int).IsAssignableTo<object>(); // → true
bool unrelated = typeof(string).IsAssignableTo<int>(); // → false

bool narrowing = typeof(object).IsAssignableTo(typeof(string)); // → false

Both forms throw ArgumentNullException when type is null. The second also throws when targetType is null (parameter name targetType).

info

.NET 5 and later have a built-in instance method Type.IsAssignableTo(Type?). When you target .NET 5 or later, type.IsAssignableTo(typeof(X)) binds to the built-in method, which returns false for a null target instead of throwing. For non-null arguments the results are the same. Call System.TypeExtensions.IsAssignableTo(type, targetType) as a static method if you want the null checks. Keep the System. prefix: System.Reflection has a TypeExtensions class of its own, so the short name does not compile in a file with using System.Reflection;. The generic form IsAssignableTo<TTarget>() has no built-in twin, so it always uses this library.

IsAssignableToGenericType​

IsAssignableToGenericType(genericType) checks whether a type matches an open generic type anywhere in its hierarchy: the type itself, a base class, or an implemented interface. Plain IsAssignableTo cannot do this, because nothing is assignable to an open type such as IRepository<>. Use it when you scan assemblies for handlers, repositories or validators.

Match an open generic type
bool isRepository = typeof(OrderRepository).IsAssignableToGenericType(typeof(IRepository<>)); // → true (interface of the base class)
bool isBase = typeof(OrderRepository).IsAssignableToGenericType(typeof(RepositoryBase<>)); // → true
bool isSequence = typeof(List<int>).IsAssignableToGenericType(typeof(IEnumerable<>)); // → true
bool isItself = typeof(List<>).IsAssignableToGenericType(typeof(List<>)); // → true
bool isDictionary = typeof(List<int>).IsAssignableToGenericType(typeof(IDictionary<,>)); // → false
bool notGeneric = typeof(int).IsAssignableToGenericType(typeof(IEnumerable<>)); // → false
Find every concrete repository in an assembly
List<Type> repositories = typeof(OrderRepository).Assembly
.GetTypes()
.Where(type => !type.IsAbstract && !type.IsGenericTypeDefinition)
.Where(type => type.IsAssignableToGenericType(typeof(IRepository<>)))
.ToList();
// → [typeof(OrderRepository)]
tip

This method is null-tolerant: a null type or a null generic type returns false instead of throwing.

note

Pass the open definition, typeof(IRepository<>). For a closed type such as typeof(IRepository<Order>), use IsAssignableTo instead.

GetGenericArgumentsOf​

GetGenericArgumentsOf(openGenericType) returns the type arguments a type supplies to an open generic type it is, inherits from or implements. IsAssignableToGenericType answers "is it a repository?"; this answers "a repository of what?". It returns null when the open type is not in the hierarchy.

Read the type arguments
Type[]? entity = typeof(OrderRepository).GetGenericArgumentsOf(typeof(IRepository<>)); // → [typeof(Order)]
Type[]? items = typeof(List<int>).GetGenericArgumentsOf(typeof(IEnumerable<>)); // → [typeof(int)]
Type[]? pair = typeof(Dictionary<string, int>).GetGenericArgumentsOf(typeof(IDictionary<,>)); // → [typeof(string), typeof(int)]
Type[]? none = typeof(List<int>).GetGenericArgumentsOf(typeof(IDictionary<,>)); // → null
Register each repository under its IRepository<TEntity>
foreach (Type repositoryType in repositories)
{
Type[]? arguments = repositoryType.GetGenericArgumentsOf(typeof(IRepository<>));
if (arguments is not null)
{
Type serviceType = typeof(IRepository<>).MakeGenericType(arguments);
services.AddScoped(serviceType, repositoryType); // IRepository<Order> → OrderRepository
}
}

The type and its base classes are searched first, then its interfaces. The first match wins.

  • Throws ArgumentNullException when type or openGenericType is null.
  • Throws ArgumentException (parameter name openGenericType) when openGenericType is not a generic type definition, for example typeof(IEnumerable<int>).
warning

If a type implements the same open interface twice, for example IHandler<OrderPlaced> and IHandler<OrderShipped>, you get the arguments of whichever one Type.GetInterfaces() returns first. .NET does not guarantee that order. Use type.GetInterfaces() directly when you need every implementation.

GetInterfacesAndAbstractClasses​

GetInterfacesAndAbstractClasses() returns every interface the type implements, inherited ones included, followed by its abstract base classes from nearest to farthest. Concrete base classes and object are left out. Use it to register one implementation under all of its contracts.

List the contracts of a type
Type[] contracts = typeof(Order).GetInterfacesAndAbstractClasses();
// → [typeof(IAuditable), typeof(IEntity), typeof(AggregateRoot)]

Type[] forInterface = typeof(IAuditable).GetInterfacesAndAbstractClasses(); // → []
Type[] forObject = typeof(object).GetInterfacesAndAbstractClasses(); // → []
note

An interface type, or object, returns an empty array, because neither has a base type. The interfaces come first, in the order Type.GetInterfaces() returns them, which .NET does not guarantee. Compare the result as a set.

Nullable types and default values​

IsNullableValueType​

IsNullableValueType() returns true only for Nullable<T>, such as int? or DateTime?. Reference types return false, even though they can hold null. Use it when a property of type int? needs different handling from int, for example to allow an empty form field.

Detect Nullable<T>
bool optionalQuantity = typeof(int?).IsNullableValueType(); // → true
bool shippedAt = typeof(DateTime?).IsNullableValueType(); // → true
bool quantity = typeof(int).IsNullableValueType(); // → false
bool name = typeof(string).IsNullableValueType(); // → false
bool definition = typeof(Nullable<>).IsNullableValueType(); // → false
note

Nullable reference type annotations (string?) exist only at compile time and leave no trace on the Type: a string? property's PropertyType is typeof(string) (and typeof(string?) does not compile).

UnwrapNullable​

UnwrapNullable() returns T for Nullable<T> and the type itself for any other type. It replaces Nullable.GetUnderlyingType(type) ?? type before a type switch or a conversion.

Get the underlying type
Type discount = typeof(decimal?).UnwrapNullable(); // → typeof(decimal)
Type id = typeof(Guid?).UnwrapNullable(); // → typeof(Guid)
Type count = typeof(int).UnwrapNullable(); // → typeof(int)
Type name = typeof(string).UnwrapNullable(); // → typeof(string)
Convert a query-string value to a nullable property type
PropertyInfo property = typeof(OrderFilter).GetProperty(nameof(OrderFilter.MinTotal))!; // decimal?
Type targetType = property.PropertyType.UnwrapNullable(); // → typeof(decimal)
object value = Convert.ChangeType("49.90", targetType, CultureInfo.InvariantCulture); // → 49.90m

GetDefaultValue​

GetDefaultValue() returns what default(T) would be when you only have a Type: null for reference types and Nullable<T>, a zeroed instance for other value types. Use it to reset properties, fill missing constructor arguments, or tell "unset" from "set" in a mapper.

Default values for run-time types
object? quantity = typeof(int).GetDefaultValue(); // → 0
object? paid = typeof(bool).GetDefaultValue(); // → false
object? rate = typeof(double).GetDefaultValue(); // → 0.0
object? id = typeof(Guid).GetDefaultValue(); // → Guid.Empty
object? discount = typeof(int?).GetDefaultValue(); // → null
object? note = typeof(string).GetDefaultValue(); // → null
object? order = typeof(Order).GetDefaultValue(); // → null

Throws ArgumentException (parameter name type) when the type has unbound generic parameters, such as typeof(KeyValuePair<,>).

warning

Value types are created with Activator.CreateInstance. If a struct declares its own parameterless constructor (allowed since C# 10), that constructor runs, so the result can differ from default(T), which never runs a constructor. For public struct Price { public Price() { Currency = "USD"; } ... }, the result has Currency == "USD".

Type shape​

IsNumeric​

IsNumeric() returns true for types that hold a number: sbyte, byte, short, ushort, int, uint, long, ulong, float, double, decimal, Half, Int128 and UInt128, plus Nullable<T> of any of them. Use it to right-align a column, pick a numeric input, or choose a number format.

Is it a number?
bool total = typeof(decimal).IsNumeric(); // → true
bool discount = typeof(decimal?).IsNumeric(); // → true
bool weight = typeof(Half).IsNumeric(); // → true
bool big = typeof(Int128).IsNumeric(); // → true
bool day = typeof(DayOfWeek).IsNumeric(); // → false
bool flag = typeof(bool).IsNumeric(); // → false
bool letter = typeof(char).IsNumeric(); // → false
bool text = typeof(string).IsNumeric(); // → false
bool date = typeof(DateTime).IsNumeric(); // → false
note

Enums are not numeric here, even though an integer backs them. bool and char are not numeric either. Half, Int128 and UInt128 are recognized by name, so the netstandard2.1 build of the library handles them too when it runs on a newer .NET.

IsComplex​

IsComplex() returns true for types that have properties of their own, and false for simple values: primitives (int, double, bool, char, ...), enums, string, decimal, DateTime, DateTimeOffset, TimeSpan, Guid, Uri, DateOnly, TimeOnly, Half, Int128, UInt128, and the nullable form of each value type. Use it in a mapper, a query-string writer or a form builder to decide whether to recurse into a property or write it as one value.

Recurse or write the value?
bool order = typeof(Order).IsComplex(); // → true
bool lines = typeof(List<int>).IsComplex(); // → true
bool codes = typeof(int[]).IsComplex(); // → true
bool any = typeof(object).IsComplex(); // → true
bool contract = typeof(IAuditable).IsComplex(); // → true

bool total = typeof(decimal).IsComplex(); // → false
bool placedAt = typeof(DateTimeOffset).IsComplex(); // → false
bool id = typeof(Guid?).IsComplex(); // → false
bool day = typeof(DayOfWeek?).IsComplex(); // → false
bool link = typeof(Uri).IsComplex(); // → false
note

Collections and arrays count as complex. If you handle collections separately, check for IEnumerable first, and keep in mind that string is also an IEnumerable<char>.

IsRecord​

IsRecord() returns true for record classes and record struct types, including derived records and readonly record struct. Use it when records need different handling, for example to skip setter-based mapping for positional records.

Detect records
bool placed = typeof(OrderPlaced).IsRecord(); // → true (record class)
bool money = typeof(Money).IsRecord(); // → true (record struct)
bool order = typeof(Order).IsRecord(); // → false (plain class)
bool count = typeof(int).IsRecord(); // → false
bool text = typeof(string).IsRecord(); // → false
bool disposable = typeof(IDisposable).IsRecord(); // → false
info

The runtime has no "record" flag, so the check looks for members the compiler generates. Record classes have a public <Clone>$ method. Record structs have a non-public bool PrintMembers(StringBuilder) method. A hand-written struct that declares the same private method is reported as a record too.

IsAnonymous​

IsAnonymous() returns true for anonymous types created with new { ... }, in C# and in Visual Basic. Use it in a logger or serializer that should print anonymous objects as plain property bags.

Detect anonymous types
object payload = new { OrderId = 1042, Status = "Paid" };
bool anonymous = payload.GetType().IsAnonymous(); // → true
bool empty = new { }.GetType().IsAnonymous(); // → true

bool order = typeof(Order).IsAnonymous(); // → false
bool placed = typeof(OrderPlaced).IsAnonymous(); // → false
bool list = typeof(List<int>).IsAnonymous(); // → false

The check requires three things: the [CompilerGenerated] attribute, AnonymousType in the type name, and a name that starts with <> (C#) or VB$ (Visual Basic).

IsStatic​

IsStatic() returns true for static class types. The runtime has no static flag: the compiler emits a static class as abstract and sealed, and that is what this checks. Use it to skip types you cannot create when you scan an assembly.

Detect static classes
bool math = typeof(Math).IsStatic(); // → true
bool helpers = typeof(System.TypeExtensions).IsStatic(); // → true
bool text = typeof(string).IsStatic(); // → false
bool root = typeof(AggregateRoot).IsStatic(); // → false (abstract, not sealed)
bool contract = typeof(IDisposable).IsStatic(); // → false
bool count = typeof(int).IsStatic(); // → false

Attributes​

These three methods work on any MemberInfo: a Type, PropertyInfo, MethodInfo, FieldInfo, EventInfo or ConstructorInfo. They are typed wrappers over the attribute APIs in System.Reflection, and they include inherited attributes by default.

The examples use these types (RangeAttribute comes from System.ComponentModel.DataAnnotations):

Annotated types used below
[AttributeUsage(AttributeTargets.All, AllowMultiple = true)]
public sealed class TagAttribute : Attribute
{
public TagAttribute(string name) => Name = name;

public string Name { get; }
}

[Obsolete("Use InvoiceExporter.")]
public class LegacyInvoiceExporter { }

public class Invoice
{
[Range(1, 100_000)]
public decimal Amount { get; set; }

[Tag("billing")]
[Tag("export")]
public string Number { get; set; } = "";

public string? Notes { get; set; }
}

[Tag("orders")]
public class OrderController { }

public sealed class AdminOrderController : OrderController { }
tip

For attributes on enum members, such as Status.Pending.GetAttribute<DescriptionAttribute>(), see Enums.

GetAttribute​

GetAttribute<TAttribute>() returns the single TAttribute applied to the member, or null when the member has none. It searches inherited members too: base types for a type, overridden members for a method or property. The GetAttribute<TAttribute>(inherit) overload lets you turn that search off.

Read one attribute
PropertyInfo amount = typeof(Invoice).GetProperty(nameof(Invoice.Amount))!;
RangeAttribute? range = amount.GetAttribute<RangeAttribute>();
object? minimum = range?.Minimum; // → 1
object? maximum = range?.Maximum; // → 100000

PropertyInfo notes = typeof(Invoice).GetProperty(nameof(Invoice.Notes))!;
RangeAttribute? none = notes.GetAttribute<RangeAttribute>(); // → null
Inherited versus own attributes
TagAttribute? inherited = typeof(AdminOrderController).GetAttribute<TagAttribute>();
// → the [Tag("orders")] declared on OrderController

TagAttribute? own = typeof(AdminOrderController).GetAttribute<TagAttribute>(inherit: false);
// → null
  • Throws ArgumentNullException when member is null.
  • Throws AmbiguousMatchException when the attribute is applied more than once, as [Tag] is on Invoice.Number. Use GetAttributes for attributes that allow multiple uses.

GetAttributes​

GetAttributes<TAttribute>() returns every TAttribute applied to the member, inherited ones included, as an IReadOnlyList<TAttribute> in declaration order. When there are none it returns an empty list, never null.

Read every attribute of one kind
PropertyInfo number = typeof(Invoice).GetProperty(nameof(Invoice.Number))!;
IReadOnlyList<TagAttribute> tags = number.GetAttributes<TagAttribute>();
List<string> names = tags.Select(tag => tag.Name).ToList(); // → ["billing", "export"]

PropertyInfo amount = typeof(Invoice).GetProperty(nameof(Invoice.Amount))!;
int count = amount.GetAttributes<TagAttribute>().Count; // → 0

Throws ArgumentNullException when member is null.

HasAttribute​

HasAttribute<TAttribute>() returns true when the member has the attribute at least once, inherited ones included. It does not create the attribute instance, so prefer it over GetAttribute<T>() is not null when you only need a yes or no.

Check for an attribute
bool obsolete = typeof(LegacyInvoiceExporter).HasAttribute<ObsoleteAttribute>(); // → true
bool fromBase = typeof(AdminOrderController).HasAttribute<TagAttribute>(); // → true

PropertyInfo number = typeof(Invoice).GetProperty(nameof(Invoice.Number))!;
bool tagged = number.HasAttribute<TagAttribute>(); // → true

PropertyInfo notes = typeof(Invoice).GetProperty(nameof(Invoice.Notes))!;
bool ranged = notes.HasAttribute<RangeAttribute>(); // → false

Throws ArgumentNullException when member is null.

warning

"Inherited" follows the attribute's own [AttributeUsage(Inherited = ...)] setting. ObsoleteAttribute is declared with Inherited = false, so a class derived from LegacyInvoiceExporter is not reported as obsolete.

HasAttribute calls MemberInfo.IsDefined, which ignores inheritance for properties and events. On an overridden property, HasAttribute sees only the attributes declared on the override, while GetAttribute and GetAttributes also find the ones on the base property. For overridden properties, use property.GetAttribute<T>() is not null.

Guid short ids​

ToShortId​

ToShortId() encodes a Guid as a 22-character id instead of the usual 36. The id is Base64url without padding: letters, digits, - and _, so it is safe in URLs, file names and anything a person copies. Turn it back into a Guid with FromShortId.

Shorter ids in URLs
Guid orderId = new Guid("7c9e6679-7425-40de-944b-e07fc1f90ae7");

string shortId = orderId.ToShortId(); // → "eWaefCV03kCUS-B_wfkK5w"
string url = $"https://shop.example/orders/{shortId}";
Guid restored = shortId.FromShortId(); // → orderId

string empty = Guid.Empty.ToShortId(); // → "AAAAAAAAAAAAAAAAAAAAAA"

To create a new random short id in one call, use Guid.NewShortId() (C# 14). It does the same as Guid.NewGuid().ToShortId(). See Extension members.

warning

Short ids are case-sensitive: changing the case of one letter gives a different Guid. Do not store them in a case-insensitive column or compare them with StringComparison.OrdinalIgnoreCase.

note

The bytes come from Guid.ToByteArray(), which stores the first three fields little-endian. A tool in another language that Base64url-encodes the UUID in RFC 4122 byte order produces a different string for the same id. Use ToShortId and FromShortId on both ends.

See also​

  • Enums: descriptions, flags and attributes on enum members.
  • Objects: read and write properties by name, and other object helpers.
  • Text and humanize: FromShortId and other text helpers.
  • Trimming and AOT: what the reflection annotations mean for trimmed and AOT-compiled apps.