- C# 14 Mastery Series Part 1: Introduction to C# 14 – What’s New and Why It Matters
- C# 14 Mastery Series Part 2: Field-Backed Properties Deep Dive – The `field` Keyword Revolution
- C# 14 Mastery Series Part 3: Extension Members Foundation – New Syntax and Capabilities
- C# 14 Mastery Series Part 5: Generic Types Enhancement – nameof with Unbound Generics
- C# 14 Mastery Series Part 4: Advanced Extension Members – Properties and Complex Scenarios
- C# 14 Mastery Series Part 6: Performance Analysis – Benchmarking C# 14 Features
- C# 14 Mastery Series Part 7: Migration Strategies – From C# 13 to C# 14
- C# 14 Mastery Series Part 8: Real-World Implementation – Building Production Applications
Welcome to Part 5 of our C# 14 mastery series! After exploring field-backed properties and extension members, we’re now diving into one of the most significant enhancements in C# 14: improvements to the generic type system, particularly the revolutionary updates to the nameof
operator with unbound generic types.
The Evolution of nameof
The nameof
operator has been a valuable tool for refactoring-safe string literals since C# 6.0. However, it had limitations when working with generic types, requiring you to specify type arguments even when you only wanted the generic type name.
Before C# 14: Limitations with Generic Types
// Previous approach required type arguments
var listTypeName = nameof(List<string>); // Returns "List"
var dictTypeName = nameof(Dictionary<string, int>); // Returns "Dictionary"
// This was not possible:
// var genericListName = nameof(List<>); // Compiler error
// Workarounds were verbose and not refactoring-safe
var listName = typeof(List<>).Name; // Returns "List`1"
var cleanName = listName.Substring(0, listName.IndexOf('`')); // "List"
C# 14: nameof with Unbound Generic Types
// Now you can use nameof with unbound generics!
var listTypeName = nameof(List<>); // Returns "List"
var dictTypeName = nameof(Dictionary<,>); // Returns "Dictionary"
var actionTypeName = nameof(Action<,,,>); // Returns "Action" (4 type parameters)
var funcTypeName = nameof(Func<,,,>); // Returns "Func" (3 params + return type)
// Works with any generic type
var customTypeName = nameof(MyGenericClass<,>); // Returns "MyGenericClass"
Syntax Rules and Technical Implementation
Syntax Rules for Unbound Generics
// Correct syntax: empty angle brackets with commas for multiple type parameters
var list = nameof(List<>); // Single type parameter
var dict = nameof(Dictionary<,>); // Two type parameters
var triple = nameof(MyClass<,,>); // Three type parameters
var quad = nameof(Action<,,,>); // Four type parameters
// Invalid syntax - these won't compile:
// var invalid1 = nameof(List<T>); // T is not in scope
// var invalid2 = nameof(List<object>); // Use bound generic if you need specific type
// var invalid3 = nameof(List); // Missing angle brackets for generic type
Metaprogramming Applications
Type Registration and Dependency Injection
public static class ServiceRegistrationExtensions
{
// Generic service registration with type-safe names
public static IServiceCollection RegisterGenericRepository(this IServiceCollection services)
{
// C# 14: Type-safe registration with meaningful logging
var interfaceName = nameof(IRepository<>);
var implementationName = nameof(Repository<>);
Console.WriteLine($"Registering {interfaceName} with implementation {implementationName}");
return services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
}
// Factory registration with generic type names
public static IServiceCollection RegisterFactory<TInterface, TImplementation>(
this IServiceCollection services)
where TImplementation : class, TInterface
{
var factoryTypeName = nameof(IFactory<>);
var serviceTypeName = typeof(TInterface).Name;
services.AddTransient<IFactory<TInterface>>(provider =>
new Factory<TInterface>(() => provider.GetRequiredService<TImplementation>()));
LogRegistration(factoryTypeName, serviceTypeName);
return services;
}
private static void LogRegistration(string factoryType, string serviceType)
{
Console.WriteLine($"Registered {factoryType} for {serviceType}");
}
}
// Usage
services.RegisterFactory<IUserService, UserService>();
services.RegisterFactory<IProductService, ProductService>();
API Documentation Generation
public class ApiDocumentationGenerator
{
private readonly Dictionary<string, TypeInfo> _genericTypes = new();
public void RegisterGenericTypes()
{
// Clean, refactoring-safe type name registration
RegisterGenericType<List<>>(nameof(List<>), "A generic list collection");
RegisterGenericType<Dictionary<,>>(nameof(Dictionary<,>), "A key-value pair collection");
RegisterGenericType<IEnumerable<>>(nameof(IEnumerable<>), "A sequence of elements");
RegisterGenericType<Task<>>(nameof(Task<>), "An asynchronous operation returning a value");
RegisterGenericType<Func<,>>(nameof(Func<,>), "A function delegate with one parameter");
RegisterGenericType<Action<>>(nameof(Action<>), "An action delegate with one parameter");
}
private void RegisterGenericType<T>(string displayName, string description)
{
_genericTypes[displayName] = new TypeInfo
{
Type = typeof(T),
DisplayName = displayName,
Description = description,
Arity = typeof(T).GetGenericArguments().Length
};
}
public string GenerateDocumentation()
{
var sb = new StringBuilder();
sb.AppendLine("# Generic Types Documentation");
sb.AppendLine();
foreach (var typeInfo in _genericTypes.Values.OrderBy(t => t.DisplayName))
{
sb.AppendLine($"## {typeInfo.DisplayName}");
sb.AppendLine($"**Type Parameters:** {typeInfo.Arity}");
sb.AppendLine($"**Description:** {typeInfo.Description}");
sb.AppendLine();
}
return sb.ToString();
}
}
Code Generation Scenarios
Source Generator Enhancement
[Generator]
public class GenericRepositoryGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
// Generate repository implementations for various generic types
var repositories = new[]
{
(nameof(IRepository<>), "Repository", 1),
(nameof(IAsyncRepository<>), "AsyncRepository", 1),
(nameof(ICachedRepository<,>), "CachedRepository", 2),
(nameof(IQueryableRepository<,>), "QueryableRepository", 2)
};
foreach (var (interfaceName, implName, arity) in repositories)
{
var source = GenerateRepositoryImplementation(interfaceName, implName, arity);
context.AddSource($"{implName}.g.cs", source);
}
}
private string GenerateRepositoryImplementation(string interfaceName, string implementationName, int arity)
{
var typeParams = string.Join(", ", Enumerable.Range(0, arity).Select(i => $"T{i}"));
var constraints = arity == 1 ? "where T0 : class" : "";
return $@"
// Auto-generated repository for {interfaceName}
public class {implementationName}<{typeParams}> : {interfaceName}<{typeParams}>
{constraints}
{{
private readonly DbContext _context;
private readonly ILogger<{implementationName}<{typeParams}>> _logger;
public {implementationName}(DbContext context, ILogger<{implementationName}<{typeParams}>> logger)
{{
_context = context;
_logger = logger;
_logger.LogInformation(""Creating {{TypeName}} repository"", ""{interfaceName}"");
}}
// Implementation details...
}}";
}
public void Initialize(GeneratorInitializationContext context) { }
}
Type-Safe Configuration and Mapping
Configuration Mapping
public class GenericConfigurationMapper
{
private readonly Dictionary<string, Func<IConfiguration, object>> _mappers = new();
public void RegisterMapper<T>(Func<IConfiguration, T> mapper)
{
var typeName = typeof(T).IsGenericType
? GetCleanGenericName<T>()
: typeof(T).Name;
_mappers[typeName] = config => mapper(config);
}
private string GetCleanGenericName<T>()
{
var type = typeof(T);
if (!type.IsGenericTypeDefinition)
type = type.GetGenericTypeDefinition();
// Use nameof with unbound generics for clean naming
return type.GetGenericArguments().Length switch
{
1 when type == typeof(IOptions<>) => nameof(IOptions<>),
1 when type == typeof(List<>) => nameof(List<>),
2 when type == typeof(Dictionary<,>) => nameof(Dictionary<,>),
2 when type == typeof(KeyValuePair<,>) => nameof(KeyValuePair<,>),
_ => ExtractGenericName(type)
};
}
public void SetupStandardMappings()
{
// Register common generic configuration patterns
RegisterMapper<IOptions<DatabaseConfig>>(config =>
Options.Create(config.GetSection("Database").Get<DatabaseConfig>()));
RegisterMapper<Dictionary<string, string>>(config =>
config.GetSection("ConnectionStrings").Get<Dictionary<string, string>>());
RegisterMapper<List<string>>(config =>
config.GetSection("AllowedHosts").Get<List<string>>());
}
public T GetConfiguration<T>(IConfiguration configuration)
{
var typeName = GetCleanGenericName<T>();
if (_mappers.TryGetValue(typeName, out var mapper))
{
return (T)mapper(configuration);
}
throw new InvalidOperationException($"No mapper registered for type {typeName}");
}
}
Performance Analysis
Performance Comparison
public class NameofPerformanceBenchmarks
{
private const int IterationCount = 100000;
[Benchmark(Baseline = true)]
public void HardcodedStrings()
{
for (int i = 0; i < IterationCount; i++)
{
var listName = "List";
var dictName = "Dictionary";
var taskName = "Task";
}
}
[Benchmark]
public void NameofUnboundGenerics()
{
for (int i = 0; i < IterationCount; i++)
{
var listName = nameof(List<>);
var dictName = nameof(Dictionary<,>);
var taskName = nameof(Task<>);
}
}
[Benchmark]
public void ReflectionBasedNames()
{
for (int i = 0; i < IterationCount; i++)
{
var listName = typeof(List<>).Name.Split('`')[0];
var dictName = typeof(Dictionary<,>).Name.Split('`')[0];
var taskName = typeof(Task<>).Name.Split('`')[0];
}
}
}
// Results show that nameof with unbound generics performs
// identically to hardcoded strings (compile-time constants)
// while being refactoring-safe
Real-World Examples
Logging and Diagnostics
public class GenericLogger<T>
{
private readonly ILogger _logger;
private readonly string _typeName;
public GenericLogger(ILogger<GenericLogger<T>> logger)
{
_logger = logger;
// Use enhanced nameof for clean type names in logs
_typeName = typeof(T).IsGenericType
? GetGenericTypeName()
: typeof(T).Name;
}
private string GetGenericTypeName()
{
var type = typeof(T);
if (!type.IsGenericType) return type.Name;
var genericDef = type.GetGenericTypeDefinition();
return genericDef.GetGenericArguments().Length switch
{
1 when genericDef == typeof(List<>) => nameof(List<>),
1 when genericDef == typeof(Task<>) => nameof(Task<>),
2 when genericDef == typeof(Dictionary<,>) => nameof(Dictionary<,>),
2 when genericDef == typeof(KeyValuePair<,>) => nameof(KeyValuePair<,>),
_ => ExtractTypeName(genericDef)
};
}
public void LogOperation(string operation, object data = null)
{
_logger.LogInformation("Performing {Operation} on {TypeName}: {@Data}",
operation, _typeName, data);
}
public void LogError(string operation, Exception exception)
{
_logger.LogError(exception, "Error during {Operation} on {TypeName}",
operation, _typeName);
}
private string ExtractTypeName(Type type)
{
var name = type.Name;
var backtickIndex = name.IndexOf('`');
return backtickIndex > 0 ? name[..backtickIndex] : name;
}
}
Type Registry and Discovery
public class GenericTypeRegistry
{
private readonly ConcurrentDictionary<string, TypeDescriptor> _registry = new();
public void Register<T>(string description = null, params string[] tags)
{
var typeName = GetTypeKey<T>();
var descriptor = new TypeDescriptor
{
Type = typeof(T),
Name = typeName,
Description = description ?? $"Generic type {typeName}",
Tags = tags?.ToList() ?? new List<string>(),
RegistrationTime = DateTime.UtcNow,
IsGeneric = typeof(T).IsGenericType,
Arity = typeof(T).IsGenericType ? typeof(T).GetGenericArguments().Length : 0
};
_registry[typeName] = descriptor;
}
private string GetTypeKey<T>()
{
var type = typeof(T);
if (!type.IsGenericType) return type.Name;
// Use the enhanced nameof for clean keys
if (type.IsGenericTypeDefinition)
{
return type.GetGenericArguments().Length switch
{
1 => $"{type.Name[..type.Name.IndexOf('`')]}<>",
2 => $"{type.Name[..type.Name.IndexOf('`')]}<,>",
3 => $"{type.Name[..type.Name.IndexOf('`')]}<,,>",
var n => $"{type.Name[..type.Name.IndexOf('`')]}<{new string(',', n - 1)}>"
};
}
return type.Name;
}
public void RegisterCommonTypes()
{
// Register common generic types with their unbound forms
Register<List<>>("Generic list collection", "collection", "list");
Register<Dictionary<,>>("Key-value pair collection", "collection", "dictionary");
Register<IEnumerable<>>("Enumerable sequence", "enumerable", "sequence");
Register<Task<>>("Asynchronous operation with result", "async", "task");
Register<Func<,>>("Function delegate with one parameter", "delegate", "function");
Register<Action<>>("Action delegate with one parameter", "delegate", "action");
}
public IEnumerable<TypeDescriptor> SearchTypes(string searchTerm)
{
return _registry.Values
.Where(t => t.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) ||
t.Description.Contains(searchTerm, StringComparison.OrdinalIgnoreCase) ||
t.Tags.Any(tag => tag.Contains(searchTerm, StringComparison.OrdinalIgnoreCase)))
.OrderBy(t => t.Name);
}
}
Best Practices and Guidelines
When to Use Unbound Generic nameof
- Logging and Diagnostics: Type-safe type names in log messages
- Configuration: Registering services and mapping configurations
- Documentation: Generating API documentation and help text
- Code Generation: Source generators and template systems
- Error Messages: User-friendly type names in exceptions
Performance Considerations
- Compile-Time Constants: nameof expressions are resolved at compile time
- Zero Runtime Cost: No reflection or string manipulation overhead
- String Interning: Generated strings are automatically interned
- Refactoring Safety: Automatic updates when types are renamed
Migration Strategy
// Before: Hardcoded strings and reflection
public class LegacyTypeHelper
{
public static string GetTypeName<T>()
{
var type = typeof(T);
if (!type.IsGenericType) return type.Name;
var name = type.Name;
var backtickIndex = name.IndexOf('`');
return backtickIndex > 0 ? name[..backtickIndex] : name;
}
}
// After: C# 14 nameof with unbound generics
public class ModernTypeHelper
{
public static string GetTypeName<T>()
{
var type = typeof(T);
if (!type.IsGenericType) return type.Name;
// Use compile-time nameof for common types
if (type.IsGenericTypeDefinition)
{
var genericDef = type;
return genericDef.GetGenericArguments().Length switch
{
1 when genericDef == typeof(List<>) => nameof(List<>),
1 when genericDef == typeof(Task<>) => nameof(Task<>),
2 when genericDef == typeof(Dictionary<,>) => nameof(Dictionary<,>),
_ => ExtractTypeName(genericDef)
};
}
return type.Name;
}
private static string ExtractTypeName(Type type)
{
var name = type.Name;
var backtickIndex = name.IndexOf('`');
return backtickIndex > 0 ? name[..backtickIndex] : name;
}
}
Conclusion
The enhancement to the nameof
operator in C# 14 represents more than just a syntactic improvement—it opens up new possibilities for type-safe metaprogramming, cleaner code generation, and more maintainable reflection-based code.
Key benefits of the enhanced generic type support include:
- Refactoring Safety: Type names are now compile-time constants, reducing runtime errors
- Performance: Compile-time evaluation eliminates runtime reflection overhead
- Clarity: Code intent is clearer with explicit generic type references
- Tooling Support: Better IntelliSense and refactoring support in IDEs
The applications we've explored—from dependency injection registration to source generation and type analysis—demonstrate how this seemingly simple feature can significantly impact architecture and code quality in large-scale applications.
In Part 6, we'll shift our focus to performance analysis, where we'll benchmark C# 14 features, analyze their impact on application performance, and provide guidance for optimizing your code when adopting these new language capabilities.
Up next: Part 6 - Performance Analysis!