Learn how C♯ 14 enables nameof with unbound generic types, eliminating dummy type arguments across your modern .NET 10 codebase.
-
Published by Kunal Chowdhury
on 09 September, 2026
Add kunal-chowdhury.com on Google to see more of our coverages in your search results.
If you have ever written logging, telemetry, or dependency injection setups in C#, you have almost certainly bumped into an annoying little quirk. For years, wanting the name of a generic type forced us to provide dummy type parameters that had nothing to do with our code.
With C# 14 shipping alongside .NET 10 and early previews of C# 15 taking shape, Microsoft has finally cleaned this up. The nameof operator now natively supports unbound generic types, letting us write expressive, honest code without arbitrary placeholder types.
nameof(List<>) directly instead of inventing placeholder arguments like nameof(List<int>) or nameof(List<object>).nameof(Dictionary<,>).
Ever since C# 6.0 introduced the nameof operator, it has been one of the most beloved additions to our daily coding habits. It banished hardcoded magic strings from parameter validation, event handlers, and data binding code across the board.
As I noted in our classic review of the evolution of C# language features, each major release chips away at awkward syntax corners. But for generic types, nameof always felt strangely half-baked. If you wanted the name of a generic class or interface, the C# compiler insisted that you supply actual type arguments.
So, what did all of us do? We picked a random type just to appease the compiler. We wrote things like nameof(List<object>), nameof(Dictionary<string, string>), or nameof(IRepository<int>). It looked bizarre in pull requests, and junior developers frequently asked whether the specific type parameter actually mattered. It did not, but C# had no other way to express it.
Pairing this with recent developments we looked at in C# 14 extension types and static members, C# 14 in .NET 10 finally removes this restriction once and for all.
The syntax in C# 14 adopts the exact same unbound generic notation that we have used with typeof() since the earliest days of .NET 2.0. You simply specify the type name followed by empty angle brackets or commas indicating the arity.
For types with a single generic parameter, you place an empty pair of angle brackets right after the type name:
// Simple unbound generic nameof in C# 14
string listName = nameof(List<>);
string taskName = nameof(Task<>);
string repoName = nameof(IRepository<>);
Console.WriteLine(listName); // Prints: List
Console.WriteLine(taskName); // Prints: Task
Console.WriteLine(repoName); // Prints: IRepository
When dealing with types that take two or more generic arguments, you use commas to indicate the number of parameters:
// Specifying type arity using commas
string dictName = nameof(Dictionary<,>);
string tupleName = nameof(Tuple<,,>);
string handlerName = nameof(ICommandHandler<,,>);
Console.WriteLine(dictName); // Prints: Dictionary
Console.WriteLine(tupleName); // Prints: Tuple
Console.WriteLine(handlerName); // Prints: ICommandHandler
Notice that the return value is just the clean, simple type name. It does not contain any backticks or arity counts (like List`1), which makes it immediately ready for logging and user-facing messages.
Let's look at how this changes the code we write every day in Indian enterprise environments, especially in ASP.NET Core service configuration and observability pipelines.
In older projects, developers had to choose between misleading dummy types or fragile magic strings:
// Legacy approaches in C# 13 and older
public static void ConfigureCatalogServices(IServiceCollection services, ILogger logger)
{
// Bad Option 1: Hardcoded magic string (risks silent breaking on rename)
logger.LogInformation("Registering open generic service: {Service}", "IRepository");
// Bad Option 2: Dummy type argument (misleading to team members)
logger.LogInformation("Registering open generic service: {Service}", nameof(IRepository<object>));
// Registering open generic type in DI
services.AddTransient(typeof(IRepository<>), typeof(SqlRepository<>));
}
Here is that exact same method rewritten with C# 14 unbound generic nameof:
// Clean, honest C# 14 syntax in .NET 10
public static void ConfigureCatalogServices(IServiceCollection services, ILogger logger)
{
// Clean Option: Strongly-typed, refactoring-safe, zero dummy types
logger.LogInformation("Registering open generic service: {Service}", nameof(IRepository<>));
// Notice how beautifully nameof matches typeof syntax now
services.AddTransient(typeof(IRepository<>), typeof(SqlRepository<>));
}
The symmetry is finally here: typeof and nameof now share the exact same syntax for open generics. That makes code reviews so much smoother.
Some developers wonder if passing open generics into nameof causes any runtime work or requires reflection. The answer is a clear no.
Here is what happens during your build step:
ldstr "List". At runtime, the CLR treats it as a plain string constant.
You get 100% compile-time verification with zero memory allocations and zero CPU overhead.
You might ask: why not just type "List" or "Dictionary" and move on? The answer comes down to one word: refactoring.
In large commercial software systems with hundreds of thousands of lines of code, names change. Today's OrderProcessor<T> becomes tomorrow's CheckoutProcessor<T>. If you used raw strings in your structured log templates, configuration binders, or metrics counters, those strings stay unchanged. When production alerts fire or dashboard graphs break, you find out the hard way.
When you use nameof(OrderProcessor<>), the compiler binds directly to your code model. If anyone presses F2 in Visual Studio to rename the class, every single nameof reference updates in lockstep across your solution.
If you work with local developer assistants or code generators, take a look at our practical guide on running local AI models for developer workflows to see how smart tooling helps keep these references clean across large legacy projects.
Microsoft Visual Studio 2026 provides seamless tooling for this feature as soon as you open a project targeting .NET 10.
Here is what you will notice right away in the editor:
nameof(MyType<, Visual Studio automatically prompts you with the correct comma count for that generic definition.nameof(MyService<object>), Visual Studio flags it with a subtle suggestion offering to simplify it to nameof(MyService<>) across your whole project.
nameof(List<>) or nameof(Dictionary<,>) directly, without having to supply dummy type arguments like 'int' or 'object'.
nameof(IRepository<>) instead of picking an arbitrary closed type like nameof(IRepository<object>).
nameof(Dictionary<,>) for 2 type parameters, or nameof(Tuple<,,>) for 3 type parameters.
nameof(MyService<>) is a strongly typed symbol reference in the Roslyn syntax tree, renaming MyService<T> via Visual Studio refactoring tools (F2) automatically updates the nameof reference safely.
Small syntax cleanups like unbound generic nameof might not sound as dramatic as brand-new language paradigms, but in our day-to-day coding lives, they make a world of difference. It removes another piece of friction that has bugged C# engineers since 2015.
As you start exploring .NET 10 and setting up your preview environments in Visual Studio 2026, keep an eye out for places where you can replace dummy type arguments with clean unbound syntax. Your pull requests and your teammates will thank you for it.
Have you encountered weird workarounds with generic type names in your own projects? How are you planning your team's transition to C# 14? Share your experiences, questions, and thoughts in the comments below!
Solution Architect & Former Microsoft MVP
Kunal Chowdhury is an enterprise solution architect and former multi-year Microsoft MVP. He is the author of three technical books: Windows Presentation Foundation Development Cookbook, Mastering Visual Studio 2017, and the Mastering Visual Studio 2019.
He publishes technical articles on Kunal-Chowdhury.com.