C# Class Naming Conventions: PascalCase, Prefixes and Suffixes (.NET 10)

Say you’re reviewing a pull request for a San Diego coffee roaster’s order system and you find clsOrder, order_helper, OrderBase and a class called Orders inside the Orders namespace.

Quick answer: the C# class naming conventions are PascalCase nouns with no prefix, a suffix only when the base type asks for one, and I only on interfaces:

public class CoffeeOrder { }                      // not clsCoffeeOrder or coffee_order
public class OrderNotFoundException : Exception { }
public interface IOrderRepository { }

We fixed that whole pull request on .NET 10, then made the build catch the same names next time.

What You Need Before You Start with C# Class Names

  • The .NET 10 SDK. Every example is a file-based app you run with dotnet run File.cs.
  • VS Code with the C# Dev Kit (see how to create a console application in Visual Studio Code).
  • The sample: order code for Bayview Coffee Roasters, a made-up roaster in San Diego, California.

This page is about type names. For every other identifier, start with our naming conventions in C# overview.

Step 1: Name Classes, Records and Structs as PascalCase Nouns

Microsoft’s Framework Design Guidelines say to name classes and structs with nouns or noun phrases in PascalCase. Nouns keep types apart from methods, which get verb phrases. Records follow the same rule, and so do a positional record’s parameters, because they become public properties.

using System.Globalization;
CultureInfo.CurrentCulture = new("en-US");

var order = new CoffeeOrder("SD-1042", "Jenna Morales", 38.50m);
var shipTo = new ShippingAddress("1250 Harbor Dr", "San Diego", "CA", "92101");
var bag = new BagWeight(12);

Console.WriteLine($"{order.Number} for {order.Customer}: {order.Total:C}");
Console.WriteLine($"Ship to {shipTo.City}, {shipTo.State} {shipTo.ZipCode}");
Console.WriteLine($"{nameof(BagWeight)}: {bag.Ounces} oz");

public class CoffeeOrder(string number, string customer, decimal total)
{
    public string Number { get; } = number;
    public string Customer { get; } = customer;
    public decimal Total { get; } = total;
}
public record ShippingAddress(
    string Street, string City, string State, string ZipCode);
public readonly record struct BagWeight(int Ounces);

Output:

SD-1042 for Jenna Morales: $38.50
Ship to San Diego, CA 92101
BagWeight: 12 oz
C# class naming conventions: CoffeeOrder class, ShippingAddress record and BagWeight record struct named as PascalCase nouns in VS Code
A class, a record and a record struct, all PascalCase nouns

Order SD-1042 for Jenna Morales comes to $38.50. The primary constructor parameters stay camelCase on the class, like other parameters, while the record’s City and ZipCode are PascalCase. Our structure in C# post covers when a struct beats a class.

Acronyms and abbreviations in class names

Two-letter acronyms keep both letters upper case (IOStream), and longer ones are written like a word (XmlReader, HtmlEncoder). Spell words out: CustomerAccount, not CustAcct.

Step 2: Drop the C and cls Prefixes from C# Class Names

The guidelines say it plainly: don’t give class names a prefix such as C. clsCustomer and CCustomer are Hungarian notation from the VB6 and MFC days, and they add nothing the editor doesn’t already show you.

The same goes for underscores. Shipping_Label compiles, but it doesn’t match a single .NET type. Underscores belong on private fields, as our C# fields naming conventions guide explains.

Step 3: Add the Standard Class Suffixes (Exception, Attribute, EventArgs)

A suffix is right when the class derives from a well-known .NET type. Same roaster, now with an exception, an attribute, event data and a collection:

Type[] types = [typeof(OrderNotFoundException), typeof(RoastDateAttribute),
                typeof(OrderPlacedEventArgs), typeof(OrderCollection)];
foreach (Type t in types)
    Console.WriteLine($"{t.Name,-24} -> {t.BaseType!.Name}");

object attr = typeof(HouseBlend).GetCustomAttributes(false)[0];
Console.WriteLine($"[RoastDate] is really {attr.GetType().Name}");

public class OrderNotFoundException(string number) : Exception($"Order {number} not found");
[AttributeUsage(AttributeTargets.Class)]
public class RoastDateAttribute(string date) : Attribute { public string Date => date; }
public class OrderPlacedEventArgs(string number) : EventArgs { public string Number => number; }
public class OrderCollection : System.Collections.ObjectModel.Collection<string>;
[RoastDate("2026-10-28")] public class HouseBlend;

Output:

OrderNotFoundException   -> Exception
RoastDateAttribute       -> Attribute
OrderPlacedEventArgs     -> EventArgs
OrderCollection          -> Collection`1
[RoastDate] is really RoastDateAttribute
C# class suffixes Exception, Attribute, EventArgs and Collection shown with each class's base type in the VS Code terminal
Each suffix names the base type; [RoastDate] maps to RoastDateAttribute

Each name ends with what it is. The compiler also lets you drop Attribute when you apply one, so [RoastDate] finds RoastDateAttribute.

The guidelines also ask for Dictionary on dictionary types and Stream on streams. Never add Enum or Flags to an enum; our C# enum naming conventions post covers those.

Controller, Service and Repository in ASP.NET Core

ASP.NET Core finds MVC controllers by the Controller suffix (or the [Controller] attribute), so OrdersController is a requirement, not a style choice.

OrderService, OrderRepository and OrderValidator are team conventions. They work because every developer can guess the file before opening it.

Async is a method suffix, not a class suffix

PlaceOrderAsync is right for a method that returns a Task. OrderServiceAsync is wrong, because a class isn’t awaitable. Our C# method naming conventions guide has the Async rules.

Step 4: Name Interfaces with I and Abstract Classes Without Base

Interfaces get an I prefix, and when a class is the standard implementation, the two names should differ only by that I. See interface in C# for the language side.

IOrderRepository repo = new OrderRepository();
PaymentProcessor[] processors = [new CardPaymentProcessor(), new GiftCardPaymentProcessor()];

foreach (PaymentProcessor p in processors)
    Console.WriteLine($"{p.GetType().Name,-25} {p.Charge(38.50m)}");
Console.WriteLine($"{repo.GetType().Name} implements {nameof(IOrderRepository)}");

public interface IOrderRepository { int Count { get; } }
public class OrderRepository : IOrderRepository { public int Count => 3; }

public abstract class PaymentProcessor
{
    public abstract string Charge(decimal amount);
}
public class CardPaymentProcessor : PaymentProcessor
{
    public override string Charge(decimal amount) => $"charged {amount} to Visa";
}
public class GiftCardPaymentProcessor : PaymentProcessor
{
    public override string Charge(decimal amount) => $"charged {amount} to gift card";
}

Output:

CardPaymentProcessor      charged 38.50 to Visa
GiftCardPaymentProcessor  charged 38.50 to gift card
OrderRepository implements IOrderRepository
C# interface IOrderRepository with class OrderRepository, and abstract class PaymentProcessor with CardPaymentProcessor and GiftCardPaymentProcessor in VS Code
OrderRepository implements IOrderRepository; both processors end with their base class name

Both processors charged $38.50. Their names end with the base class name, which the guidelines suggest, the same way ArgumentOutOfRangeException ends with Exception.

For a public base class, the guidelines say to avoid a Base suffix: PaymentProcessor reads better than PaymentProcessorBase. Inside an app, Base is fine if your team likes it. Our abstract class in C# post shows when to pick one.

Step 5: Name Generic Type Parameters T, TKey and TValue

Use T when a class has one obvious type parameter, and a descriptive name with a T prefix when it has more:

var prices = new PriceCache<string, decimal>();
prices.Add("Ethiopia Yirgacheffe", 18.95m);
var lookup = new Result<decimal>(prices.Get("Ethiopia Yirgacheffe"));
Console.WriteLine($"Price: {lookup.Value}");

Type cache = typeof(PriceCache<,>);
Console.WriteLine($"{cache.Name}: {string.Join(", ", cache.GetGenericArguments().Select(t => t.Name))}");
Console.WriteLine($"Result<{typeof(Result<>).GetGenericArguments()[0].Name}>");

public class PriceCache<TKey, TValue> where TKey : notnull
{
    private readonly Dictionary<TKey, TValue> _items = [];
    public void Add(TKey key, TValue value) => _items[key] = value;
    public TValue Get(TKey key) => _items[key];
}
public record Result<T>(T Value);

Output:

Price: 18.95
PriceCache`2: TKey, TValue
Result<T>

The Ethiopia Yirgacheffe bag costs $18.95. Reflection shows PriceCache`2, because metadata adds the parameter count, while your code still says PriceCache<TKey, TValue>.

Step 6: Name Static and Helper Classes After What They Do

A static class still gets a noun, but name it after its one job. SalesTaxCalculator beats Helpers or Utils, which turn into dumping grounds. Extension method classes end with Extensions:

using System.Globalization;
CultureInfo.CurrentCulture = new("en-US");

decimal subtotal = 54.00m;
Console.WriteLine($"CA state tax: {SalesTaxCalculator.ForState("CA", subtotal):C}");
Console.WriteLine($"Bags at $13.50: {subtotal.ToBagCount(13.50m)}");

public static class SalesTaxCalculator
{
    public static decimal ForState(string state, decimal amount) =>
        state == "CA" ? Math.Round(amount * 0.0725m, 2) : 0m;
}
public static class DecimalExtensions
{
    public static int ToBagCount(this decimal total, decimal bagPrice) => (int)(total / bagPrice);
}

Output:

CA state tax: $3.92
Bags at $13.50: 4

A $54.00 order in California gets $3.92 of state tax at the 7.25% base rate. DecimalExtensions tells you exactly which type its methods extend.

Step 7: Match the File Name to the Class Name

Put each public class in its own file named after it: CoffeeOrder lives in CoffeeOrder.cs. The compiler doesn’t care, but your team scanning Solution Explorer does.

Visual Studio’s quick actions offer “Move type to CoffeeOrder.cs” and “Rename file to CoffeeOrder.cs” when they don’t match. Generic types usually use the plain name, and partial classes add a part, like CoffeeOrder.Validation.cs.

Our demo files break this on purpose: a single-file app keeps a few small types together so you can run it in one go.

Can a Class Have the Same Name as Its Namespace?

It compiles until something references it. The guidelines say not to use the same name for a namespace and a type in it, and here’s why:

Bayview.Checkout.Run();

namespace Bayview
{
    static class Checkout
    {
        public static void Run() => Console.WriteLine(new Orders().Count);
    }
}

namespace Bayview.Orders
{
    class Orders { public int Count => 3; }
}

dotnet run NamespaceClash.cs:

C:\DotNetDemo\ClassNamingDemo\NamespaceClash.cs(7,59): error CS0118: 'Orders' is a namespace but is used like a type

The build failed. Fix the build errors and run again.
C# class Orders inside namespace Bayview.Orders fails with error CS0118 'Orders' is a namespace but is used like a type in VS Code
Inside namespace Bayview, Orders means the namespace, not the class

The compiler finds the namespace Bayview.Orders before the class, so you get CS0118: 'Orders' is a namespace but is used like a type. The fix is a plural namespace and a different class name:

Bayview.Checkout.Run();

namespace Bayview
{
    static class Checkout
    {
        public static void Run() => Console.WriteLine($"Open orders: {new Orders.OrderQueue().Count}");
    }
}

namespace Bayview.Orders
{
    class OrderQueue { public int Count => 3; }
}

Output:

Open orders: 3

Our namespace in C# guide covers folder-to-namespace mapping and file-scoped namespaces.

Step 8: Enforce C# Class Names with .editorconfig and IDE1006

Reviewers get tired; the build doesn’t. We put these rules in the repo root .editorconfig:

root = true

[*.cs]
# Report naming problems in dotnet build, not only in the editor
dotnet_diagnostic.IDE1006.severity = warning
dotnet_diagnostic.CA1707.severity = warning
dotnet_diagnostic.CA1710.severity = warning
dotnet_diagnostic.CA1711.severity = warning
dotnet_diagnostic.CA1715.severity = warning
dotnet_code_quality.api_surface = all

# Classes, structs, enums and delegates: PascalCase
dotnet_naming_symbols.types.applicable_kinds = class, struct, enum, delegate
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_rule.types_are_pascal_case.symbols = types
dotnet_naming_rule.types_are_pascal_case.style = pascal_case
dotnet_naming_rule.types_are_pascal_case.severity = warning

# Interfaces: I + PascalCase
dotnet_naming_symbols.interfaces.applicable_kinds = interface
dotnet_naming_style.i_prefix.capitalization = pascal_case
dotnet_naming_style.i_prefix.required_prefix = I
dotnet_naming_rule.interfaces_start_with_i.symbols = interfaces
dotnet_naming_rule.interfaces_start_with_i.style = i_prefix
dotnet_naming_rule.interfaces_start_with_i.severity = warning

IDE1006 checks casing and the I prefix. CA1707, CA1710, CA1711 and CA1715 catch underscores, missing or wrong suffixes and bad type parameters. The #:property line turns on EnforceCodeStyleInBuild in a .NET 10 file-based app:

#:property EnforceCodeStyleInBuild=true

var repo = new orderRepository();
Console.WriteLine($"Orders: {repo.Count}, cache: {new Cache<string>().Size}");

public interface OrderStore { int Count { get; } }
public class orderRepository : OrderStore { public int Count => 3; }
public class clsCustomer { }
public class Shipping_Label { }
public class OrderFailure : Exception { }
public enum OrderStatusEnum { Placed, Roasted, Shipped }
public class Cache<Key> { public int Size => 0; }

dotnet build NamingCheck.cs –no-incremental -v q -nologo:

C:\DotNetDemo\ClassNamingDemo\NamingCheck.cs(9,14): warning CA1707: Remove the underscores from type name Shipping_Label (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1707)
C:\DotNetDemo\ClassNamingDemo\NamingCheck.cs(6,18): warning CA1715: Prefix interface name OrderStore with 'I' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1715)
C:\DotNetDemo\ClassNamingDemo\NamingCheck.cs(10,14): warning CA1710: Rename OrderFailure to end in 'Exception' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1710)
C:\DotNetDemo\ClassNamingDemo\NamingCheck.cs(11,13): warning CA1711: Rename type name OrderStatusEnum so that it does not end in 'Enum' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1711)
C:\DotNetDemo\ClassNamingDemo\NamingCheck.cs(12,20): warning CA1715: Prefix generic type parameter name Key with 'T' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1715)
C:\DotNetDemo\ClassNamingDemo\NamingCheck.cs(6,18): warning IDE1006: Naming rule violation: Missing prefix: 'I' (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide1006)
C:\DotNetDemo\ClassNamingDemo\NamingCheck.cs(8,14): warning IDE1006: Naming rule violation: These words must begin with upper case characters: clsCustomer (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide1006)
C:\DotNetDemo\ClassNamingDemo\NamingCheck.cs(7,14): warning IDE1006: Naming rule violation: These words must begin with upper case characters: orderRepository (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide1006)
C# class naming enforced with .editorconfig: IDE1006, CA1707, CA1710, CA1711 and CA1715 warnings for bad class, interface and type parameter names
Every bad name from the pull request now fails the review in the build

Important: pascal_case only checks the first letter, so CCustomer would still pass. That one needs a human reviewer.

C# Class Naming Conventions Cheat Sheet

What you’re namingConventionExample
Class, record, structPascalCase nounCoffeeOrder, ShippingAddress
InterfaceI + PascalCaseIOrderRepository
Exception / attributeBase type as suffixOrderNotFoundException, RoastDateAttribute
Event dataEventArgs suffixOrderPlacedEventArgs
Public abstract baseNo Base suffixPaymentProcessor
Generic type parameterT or T + nameT, TKey, TValue
Static / extension classJob name / ExtensionsSalesTaxCalculator, DecimalExtensions
ASP.NET Core controllerController suffixOrdersController

The members inside each class have their own rules: properties and methods are PascalCase, locals and parameters camelCase (see C# variable naming conventions).

Common Mistakes to Avoid in C# Class Names

MistakeWhat happensFix
clsCustomerIDE1006: must begin with upper caseCustomer
Shipping_LabelCA1707: remove the underscoresShippingLabel
OrderFailure : ExceptionCA1710: end in ‘Exception’OrderFailedException
OrderStatusEnumCA1711: don’t end in ‘Enum’OrderStatus
Cache<Key>CA1715: prefix with ‘T’Cache<TKey>
class Orders in Bayview.OrdersCS0118 wherever it’s usedOrderQueue
OrderServiceAsyncNo error, but misleadingOrderService + PlaceOrderAsync()

Next Steps

Class names are the start. Next, name the flags on those classes; our C# boolean variable naming convention guide covers the Is, Has and Can prefixes.

We’d read these next, too:

Frequently Asked Questions

What is the naming convention for classes in C#?

PascalCase nouns or noun phrases, such as CoffeeOrder or ShippingAddress, with no prefix and no underscores.

Should C# class names be singular or plural?

Singular for a class that models one thing (Order). A collection class ends in Collection, and namespaces are often plural (Bayview.Orders).

Should I prefix class names with C or cls?

No. The Framework Design Guidelines say not to prefix class names. Only interfaces get a prefix, the letter I.

Is it OK to use a Base suffix on an abstract class?

Avoid it on public library APIs. Inside an application, RepositoryBase is common and fine if your team agrees on it.

Does the file name have to match the class name in C#?

No, the compiler doesn’t require it, but one public type per file named after the type is the standard team rule.

Where are Microsoft’s official class naming rules?