TypeFormatter: Type Name Formatting Utility¶
Verified by tests
TypeFormatterTests, TypeNameFormatterTests — library CI run #31657041675 (2026-08-13)
TypeFormatter is a static utility class that formats .NET Type objects into string representations according to TypeQualifications flags. It handles namespace, assembly, version, culture, and public key token formatting with culture-invariant output.
Overview¶
TypeFormatter provides: - ✅ Culture-invariant type name formatting - ✅ Respects all TypeQualifications flags - ✅ Handles null types safely - ✅ Fully AOT-compatible (no reflection beyond Type.GetName) - ✅ Used by source generators and message association APIs
Quick Start¶
Basic Formatting¶
Basic Formatting
using Whizbang.Core;
var type = typeof(ECommerce.Contracts.Events.ProductCreatedEvent);
// Format with preset
var simple = TypeFormatter.FormatType(type, TypeQualifications.Simple);
Console.WriteLine(simple);
// Output: "ProductCreatedEvent"
var fullyQualified = TypeFormatter.FormatType(type, TypeQualifications.FullyQualified);
Console.WriteLine(fullyQualified);
// Output: "ECommerce.Contracts.Events.ProductCreatedEvent, ECommerce.Contracts"
// Format with custom flags
var custom = TypeFormatter.FormatType(
type,
TypeQualifications.Namespace | TypeQualifications.TypeName
);
Console.WriteLine(custom);
// Output: "ECommerce.Contracts.Events.ProductCreatedEvent"
Formatting with Version Information¶
Formatting with Version Information
// Full assembly qualification with version
var withVersion = TypeFormatter.FormatType(
type,
TypeQualifications.FullyQualifiedWithVersion
);
Console.WriteLine(withVersion);
// Output: "ECommerce.Contracts.Events.ProductCreatedEvent, ECommerce.Contracts, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
// Without version
var withoutVersion = TypeFormatter.FormatType(
type,
TypeQualifications.FullyQualified
);
Console.WriteLine(withoutVersion);
// Output: "ECommerce.Contracts.Events.ProductCreatedEvent, ECommerce.Contracts"
Formatting Rules¶
Component Order¶
TypeFormatter outputs components in this order:
1. GlobalPrefix (global::) - if flag is set
2. Namespace (MyApp.Events) - if flag is set
3. Dot separator (.) - if both Namespace and TypeName are set
4. TypeName (ProductCreatedEvent) - if flag is set
5. Comma separator (,) - before Assembly, if anything precedes it (omitted when Assembly is the only component)
6. Assembly (MyApp) - if flag is set
7. Version (, Version=1.0.0.0) - if flag is set
8. Culture (, Culture=neutral) - if flag is set
9. PublicKeyToken (, PublicKeyToken=null) - if flag is set
Flag Combinations¶
Flag Combinations
var type = typeof(OrderCreatedEvent);
// TypeName only
var name = TypeFormatter.FormatType(type, TypeQualifications.TypeName);
// Result: "OrderCreatedEvent"
// Namespace + TypeName
var ns = TypeFormatter.FormatType(
type,
TypeQualifications.Namespace | TypeQualifications.TypeName
);
// Result: "MyApp.Events.OrderCreatedEvent"
// GlobalPrefix + TypeName
var global = TypeFormatter.FormatType(
type,
TypeQualifications.GlobalPrefix | TypeQualifications.TypeName
);
// Result: "global::OrderCreatedEvent"
// GlobalPrefix + Namespace + TypeName
var globalFull = TypeFormatter.FormatType(
type,
TypeQualifications.GlobalPrefix | TypeQualifications.Namespace | TypeQualifications.TypeName
);
// Result: "global::MyApp.Events.OrderCreatedEvent"
// Assembly without TypeName (edge case)
var assemblyOnly = TypeFormatter.FormatType(type, TypeQualifications.Assembly);
// Result: "MyApp"
// None flag
var empty = TypeFormatter.FormatType(type, TypeQualifications.None);
// Result: ""
Culture-Invariant Formatting¶
TypeFormatter uses CultureInfo.InvariantCulture for all formatting to ensure consistent output across locales:
Culture-Invariant Formatting
// Version, Culture, and PublicKeyToken always use InvariantCulture
var withVersion = TypeFormatter.FormatType(
type,
TypeQualifications.FullyQualifiedWithVersion
);
// Formatted string interpolation uses InvariantCulture
// This ensures version numbers, hex strings, etc. are consistent
// Example: "Version=1.0.0.0" not "Version=1,0,0,0" (some locales use commas)
Common Scenarios¶
Scenario 1: Source Generator Output¶
When: Generating C# code that references types
Scenario 1: Source Generator Output
public string GenerateEventHandler(Type eventType) {
// Use GlobalQualified to avoid namespace conflicts
var typeName = TypeFormatter.FormatType(
eventType,
TypeQualifications.GlobalQualified
);
return $@"
public class GeneratedHandler {{
public void Handle({typeName} evt) {{
// Handle event
}}
}}
";
}
// Output:
// public class GeneratedHandler {
// public void Handle(global::ECommerce.Contracts.Events.ProductCreatedEvent evt) {
// // Handle event
// }
// }
Scenario 2: Logging and Diagnostics¶
When: Displaying type information in logs
Scenario 2: Logging and Diagnostics
public void LogTypeInfo(Type type) {
// Simple name for user-friendly output
var simple = TypeFormatter.FormatType(type, TypeQualifications.Simple);
_logger.LogInformation("Processing: {TypeName}", simple);
// Fully qualified for diagnostic details
var full = TypeFormatter.FormatType(type, TypeQualifications.FullyQualified);
_logger.LogDebug("Full type: {FullType}", full);
// With version for complete diagnostics
var withVersion = TypeFormatter.FormatType(
type,
TypeQualifications.FullyQualifiedWithVersion
);
_logger.LogTrace("Type with version: {VersionedType}", withVersion);
}
// Output:
// Information: Processing: ProductCreatedEvent
// Debug: Full type: ECommerce.Contracts.Events.ProductCreatedEvent, ECommerce.Contracts
// Trace: Type with version: ECommerce.Contracts.Events.ProductCreatedEvent, ECommerce.Contracts, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
Scenario 3: Configuration and Serialization¶
When: Storing type names in configuration or serializing to JSON
Scenario 3: Configuration and Serialization
public class EventConfiguration {
// Store fully qualified name for reliable deserialization
public string EventType { get; set; } = null!;
}
public EventConfiguration CreateConfig(Type eventType) {
return new EventConfiguration {
EventType = TypeFormatter.FormatType(
eventType,
TypeQualifications.FullyQualified
)
};
}
// Later: Deserialize
public Type GetEventType(EventConfiguration config) {
// Use fully qualified name for reliable Type.GetType()
return Type.GetType(config.EventType)
?? throw new InvalidOperationException($"Type not found: {config.EventType}");
}
Scenario 4: Dynamic Type Display¶
When: Building UI that shows type information
Scenario 4: Dynamic Type Display
public class TypeDisplayInfo {
public string SimpleName { get; init; } = null!;
public string FullName { get; init; } = null!;
public string AssemblyName { get; init; } = null!;
}
public TypeDisplayInfo GetDisplayInfo(Type type) {
return new TypeDisplayInfo {
SimpleName = TypeFormatter.FormatType(type, TypeQualifications.Simple),
FullName = TypeFormatter.FormatType(type, TypeQualifications.NamespaceQualified),
AssemblyName = TypeFormatter.FormatType(type, TypeQualifications.Assembly)
};
}
// Usage in UI:
// Simple: "ProductCreatedEvent"
// Full: "ECommerce.Contracts.Events.ProductCreatedEvent"
// Assembly: "ECommerce.Contracts"
Edge Cases and Special Handling¶
Empty Namespace¶
Empty Namespace
// Type with no namespace (global namespace)
public class GlobalType { }
var formatted = TypeFormatter.FormatType(
typeof(GlobalType),
TypeQualifications.NamespaceQualified
);
// Result: "GlobalType" (no leading dot)
Generic Types¶
Generic Types
var genericType = typeof(List<OrderCreatedEvent>);
var formatted = TypeFormatter.FormatType(
genericType,
TypeQualifications.FullyQualified
);
// Result: "System.Collections.Generic.List`1, System.Private.CoreLib"
// Note: TypeFormatter uses Type.Name, so generic arity appears as `1, `2, etc.
// Generic type arguments are NOT expanded.
Nested Types¶
Nested Types
public class OuterClass {
public class InnerClass { }
}
var nestedType = typeof(OuterClass.InnerClass);
var formatted = TypeFormatter.FormatType(
nestedType,
TypeQualifications.NamespaceQualified
);
// Result: "MyApp.InnerClass"
// Note: TypeFormatter uses Type.Name, which for a nested type is just the
// inner type's name — the declaring type is NOT included. If you need the
// full '+'-separated nested chain ("MyApp.OuterClass+InnerClass"), use
// TypeNameFormatter (below), which is based on Type.FullName.
Public Key Token¶
Public Key Token
// Strong-named assembly
var strongType = typeof(System.String);
var withToken = TypeFormatter.FormatType(
strongType,
TypeQualifications.FullyQualifiedWithVersion
);
// Result: "System.String, System.Private.CoreLib, Version=<runtime version>, Culture=neutral, PublicKeyToken=7cec85d7bea7798e"
// Non-strong-named assembly
var weakType = typeof(MyApp.CustomType);
var withoutToken = TypeFormatter.FormatType(
weakType,
TypeQualifications.FullyQualifiedWithVersion
);
// Result: "MyApp.CustomType, MyApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
Performance Considerations¶
StringBuilder Allocation¶
TypeFormatter uses StringBuilder internally for efficient string building:
StringBuilder Allocation
// Efficient - single StringBuilder allocation
var formatted = TypeFormatter.FormatType(type, TypeQualifications.FullyQualified);
// Less efficient - multiple string concatenations
var manual = type.Namespace + "." + type.Name + ", " + type.Assembly.GetName().Name;
Caching Formatted Results¶
Since type formatting is deterministic, consider caching results:
Caching Formatted Results
public class CachedTypeFormatter {
private readonly ConcurrentDictionary<(Type, TypeQualifications), string> _cache = new();
public string FormatType(Type type, TypeQualifications qualification) {
return _cache.GetOrAdd(
(type, qualification),
key => TypeFormatter.FormatType(key.Item1, key.Item2)
);
}
}
Integration with Message Associations¶
TypeFormatter is used extensively in message association APIs:
Integration with Message Associations
// Format event type for lookup
var eventType = typeof(ProductCreatedEvent);
var simpleType = TypeFormatter.FormatType(eventType, TypeQualifications.Simple);
// Find perspectives handling this event (simple name)
var perspectives = PerspectiveRegistrationExtensions.GetPerspectivesForEvent(
simpleType,
serviceName,
MatchStrictness.SimpleName
);
// Format for storage in message associations
var storedType = TypeFormatter.FormatType(eventType, TypeQualifications.FullyQualified);
API Reference¶
Method Signature¶
Namespace: Whizbang.Core
Method Signature
public static class TypeFormatter {
/// <summary>
/// Formats a Type according to the specified TypeQualifications flags.
/// Uses culture-invariant formatting for consistent output.
/// </summary>
/// <param name="type">The Type to format</param>
/// <param name="qualification">Flags controlling which components to include</param>
/// <returns>Formatted type name string</returns>
/// <exception cref="ArgumentNullException">Thrown if type is null</exception>
public static string FormatType(Type type, TypeQualifications qualification);
/// <summary>
/// Parses the assembly name out of a fully qualified type name string
/// (e.g. "Namespace.Type, Assembly, Version=1.0.0.0, ...").
/// With stripVersion: true returns just the assembly name; with false,
/// returns the full assembly string including version/culture/token.
/// Returns empty string when no assembly info is present.
/// </summary>
public static string ParseAssemblyName(string fullTypeName, bool stripVersion);
}
Parameters¶
- type: The
Typeobject to format (cannot be null) - qualification:
TypeQualificationsflags controlling output format
Return Value¶
- Returns formatted type name as
string - Returns empty string if
qualificationisTypeQualifications.None - Never returns null
Exceptions¶
- ArgumentNullException: Thrown if
typeparameter is null
Best Practices¶
- Use FullyQualified for persistence - Ensures reliable deserialization with
Type.GetType() - Use Simple for user-facing displays - More readable in UI and logs
- Use GlobalQualified in generated code - Avoids namespace conflicts with
global:: - Cache formatted results - Formatting is expensive, memoize when calling frequently
- Use culture-invariant output - TypeFormatter already does this, safe for serialization
- Avoid formatting in hot paths - Pre-format and cache if used repeatedly
- Consider version implications - Decide if version matching matters for your use case
Common Pitfalls¶
❌ Formatting Null Types¶
❌ Formatting Null Types
// ❌ WRONG: Null type
Type? nullType = null;
var formatted = TypeFormatter.FormatType(nullType!, TypeQualifications.Simple);
// Throws: ArgumentNullException
// ✅ CORRECT: Check for null first
if (type != null) {
var formatted = TypeFormatter.FormatType(type, TypeQualifications.Simple);
}
❌ Assuming Default Format¶
❌ Assuming Default Format
// ❌ WRONG: Assuming ToString() matches formatted output
var toString = type.ToString();
var formatted = TypeFormatter.FormatType(type, TypeQualifications.FullyQualified);
// These may not match!
// ✅ CORRECT: Always use TypeFormatter for consistent results
var formatted = TypeFormatter.FormatType(type, TypeQualifications.FullyQualified);
❌ Hardcoding Type Names¶
❌ Hardcoding Type Names
// ❌ WRONG: Hardcoded type name
var typeName = "ECommerce.Contracts.Events.ProductCreatedEvent, ECommerce.Contracts";
// ✅ CORRECT: Use TypeFormatter
var typeName = TypeFormatter.FormatType(
typeof(ProductCreatedEvent),
TypeQualifications.FullyQualified
);
❌ Ignoring Culture in Manual Formatting¶
❌ Ignoring Culture in Manual Formatting
// ❌ WRONG: Culture-dependent formatting
var version = type.Assembly.GetName().Version;
var formatted = $"Version={version}"; // May use locale-specific format
// ✅ CORRECT: Use TypeFormatter with InvariantCulture
var formatted = TypeFormatter.FormatType(type, TypeQualifications.FullyQualifiedWithVersion);
Related: TypeNameFormatter (storage & matching)¶
Don't confuse TypeFormatter (the flags-based utility above) with TypeNameFormatter (Whizbang.Core), the fixed-format helper the runtime uses to write and match the type-name strings persisted in the database. It exposes exactly two canonical forms, and every writer of a given column goes through the matching one so the strings are byte-identical across drivers and generators:
| Method | Output | Used for |
|---|---|---|
TypeNameFormatter.Format(type) |
"Namespace.Outer+Nested, Assembly" (assembly-qualified) |
wh_event_store.event_type, wh_outbox/wh_inbox.message_type, wh_message_associations, message routing, JSON discriminators |
TypeNameFormatter.FormatClrTypeName(type) |
"Namespace.Outer+Nested" (no assembly) |
wh_event_store.aggregate_type, wh_message_type_registry.clr_type_name, wh_perspective_registry.clr_type_name |
Both are based on Type.FullName, so they use + to separate a nested type from its declaring type (unlike TypeFormatter, which uses Type.Name — see Nested Types above). The source-generator mirrors are TypeNameUtilities.FormatTypeNameForRuntime (assembly-qualified) and TypeNameUtilities.BuildClrTypeName (no assembly), so compile-time registration and runtime lookup always agree. Read paths normalize an inbound assembly-qualified name (stripping Version/Culture/PublicKeyToken) via EventTypeMatchingHelper.NormalizeTypeName before comparing, so the short Format form matches every variant. Existing rows written with the older C#-display (.-nested) form are normalized by migration 063_NormalizeClrTypeNamesV2 — see Migration Tracking → Data Migrations.
See Also¶
- TypeQualifications - Flag enum controlling formatting
- MatchStrictness - Fuzzy matching with formatted types
- TypeMatcher - Matching formatted type strings
- Perspectives - Message associations using formatted types
- Migration Tracking - Settings-gated data migrations (e.g. CLR type-name normalization)