Skip to main content

CSharpier vs dotnet format: C# Code Formatting on .NET 10

Last updated: October 5, 2026 · Tested with CSharpier 1.3.0 (dotnet tool and CSharpier.MSBuild), dotnet format from SDK 10.0.401, Windows 11

Short answer: CSharpier is an opinionated formatter: it rewraps long lines, sorts usings and gives every file the same layout, with almost nothing to configure. dotnet format ships with the SDK and fixes spacing, indentation and code-style rules from .editorconfig, but it never wraps a long line. Use CSharpier for layout and keep dotnet format style and analyzers for code rules. Don't run dotnet format whitespace on top of CSharpier: with a common .editorconfig setting the two tools undo each other and your CI check fails either way. Below are the real before/after files, timings on 3,050 files, and the setup that worked.

Install CSharpier (1.x Commands)

Install it as a local tool so everyone on the team and the build server use the same version:

dotnet new tool-manifest
dotnet tool install csharpier
dotnet csharpier format .
dotnet csharpier check .

On SDK 10 that creates dotnet-tools.json in the current folder, with the version pinned. Commit it:

{
  "version": 1,
  "isRoot": true,
  "tools": {
    "csharpier": {
      "version": "1.3.0",
      "commands": [
        "csharpier"
      ],
      "rollForward": false
    }
  }
}

Upgrading from 0.x? Version 1.0 moved everything to subcommands. The old dotnet csharpier . and dotnet csharpier --check . now fail with exit code 1:

$ dotnet csharpier .
'.' was not matched. Did you mean one of the following?
-h
Required command was not provided.

$ dotnet csharpier --check .
'--check' was not matched. Did you mean one of the following?
check
Required command was not provided.

Replace them with dotnet csharpier format . and dotnet csharpier check . in scripts, hooks and CI.

Same File, Two Formatters

I wrote one deliberately messy class:

using System.Text;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Demo{
public class OrderService{
    private readonly Dictionary<int,decimal> _prices=new Dictionary<int, decimal>();
  public decimal CalculateTotal(IEnumerable<(int productId,int quantity)> lines,decimal discountPercentage,bool includeTax,string countryCode){
        if(lines==null) throw new ArgumentNullException(nameof(lines));
        var total=lines.Where(l=>l.quantity>0).Sum(l=>_prices.TryGetValue(l.productId,out var price)?price*l.quantity:0m);
     if (includeTax) { total = total * (countryCode == "NL" ? 1.21m : countryCode == "DE" ? 1.19m : 1.0m); }
        return Math.Round(total*(1-discountPercentage/100),2);
    }
    public string Describe(int id)    =>   new StringBuilder().Append("Order ").Append(id).ToString();
}
}

After dotnet csharpier format OrderService.cs ("Formatted 1 files in 495ms"):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Demo
{
    public class OrderService
    {
        private readonly Dictionary<int, decimal> _prices = new Dictionary<int, decimal>();

        public decimal CalculateTotal(
            IEnumerable<(int productId, int quantity)> lines,
            decimal discountPercentage,
            bool includeTax,
            string countryCode
        )
        {
            if (lines == null)
                throw new ArgumentNullException(nameof(lines));
            var total = lines
                .Where(l => l.quantity > 0)
                .Sum(l =>
                    _prices.TryGetValue(l.productId, out var price) ? price * l.quantity : 0m
                );
            if (includeTax)
            {
                total =
                    total
                    * (
                        countryCode == "NL" ? 1.21m
                        : countryCode == "DE" ? 1.19m
                        : 1.0m
                    );
            }
            return Math.Round(total * (1 - discountPercentage / 100), 2);
        }

        public string Describe(int id) =>
            new StringBuilder().Append("Order ").Append(id).ToString();
    }
}

After dotnet format whitespace on the same input:

using System.Text;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Demo
{
    public class OrderService
    {
        private readonly Dictionary<int, decimal> _prices = new Dictionary<int, decimal>();
        public decimal CalculateTotal(IEnumerable<(int productId, int quantity)> lines, decimal discountPercentage, bool includeTax, string countryCode)
        {
            if (lines == null) throw new ArgumentNullException(nameof(lines));
            var total = lines.Where(l => l.quantity > 0).Sum(l => _prices.TryGetValue(l.productId, out var price) ? price * l.quantity : 0m);
            if (includeTax) { total = total * (countryCode == "NL" ? 1.21m : countryCode == "DE" ? 1.19m : 1.0m); }
            return Math.Round(total * (1 - discountPercentage / 100), 2);
        }
        public string Describe(int id) => new StringBuilder().Append("Order ").Append(id).ToString();
    }
}

What the comparison shows:

  • Long lines: CSharpier wrapped the parameter list, the LINQ chain and the nested ternary at its 100-character default. dotnet format left a 152-character line exactly as it was; it has no line-length rule.
  • Usings: CSharpier sorted them (System.Text moved to the end). dotnet format whitespace did not touch the order.
  • Blank lines: CSharpier added one between members and after the usings. dotnet format only fixed indentation and spaces around operators.
  • Code style: neither added braces to if (lines == null) throw .... That is a code-style rule (IDE0011), which is dotnet format style's job, not a formatter's.

Do They Conflict?

With no .editorconfig, no: dotnet format --verify-no-changes passed on CSharpier's output. Then I added an .editorconfig that I see in a lot of repositories:

root = true

[*.cs]
indent_style = space
indent_size = 4
max_line_length = 80
csharp_new_line_before_open_brace = none

CSharpier honored max_line_length = 80 (the longest line dropped to 73 characters) and ignored csharp_new_line_before_open_brace = none, because brace placement is not configurable in CSharpier. dotnet format reads that setting, so it rejected CSharpier's output:

$ dotnet format whitespace demo.csproj --verify-no-changes
OrderService.cs(6,15): error WHITESPACE: Fix whitespace formatting. Replace 1 characters with '\s'.
OrderService.cs(8,30): error WHITESPACE: Fix whitespace formatting. Replace 5 characters with '\s'.
OrderService.cs(18,10): error WHITESPACE: Fix whitespace formatting. Replace 9 characters with '\s'.
OrderService.cs(29,28): error WHITESPACE: Fix whitespace formatting. Replace 13 characters with '\s'.

And after letting dotnet format move the braces, CSharpier rejected that:

$ dotnet csharpier check OrderService.cs
Error .\fight/OrderService.cs - Was not formatted.
  ----------------------------- Expected: Around Line 6 -----------------------------

  namespace Demo
  {
  ----------------------------- Actual: Around Line 6 -----------------------------

  namespace Demo {
      public class OrderService {

Run both whitespace checks in CI and one of them always fails. The fix that passed for me: let CSharpier own layout and run only the other two parts of dotnet format. On the CSharpier-formatted file, with the same .editorconfig, both of these exited with 0:

dotnet format style --verify-no-changes
dotnet format analyzers --verify-no-changes

Also delete layout settings that CSharpier ignores (brace and new-line options) from .editorconfig, so the IDE doesn't reformat code differently when someone presses Format Document.

Configuration: What You Can Change

Very little, on purpose. The settings that matter are print width, indentation and line endings. CSharpier reads them from .csharpierrc.json or from .editorconfig:

{
  "printWidth": 140
}

I tested which one wins with the same messy file:

Config presentLongest line after formatting
None (default print width 100)93
.editorconfig with max_line_length = 8073
Both, .csharpierrc.json with printWidth: 140101 (CSharpier's own file wins)

If you keep both files, set the width in one place only.

Keep Hand-Aligned Code with csharpier-ignore

CSharpier collapses manual alignment. A // csharpier-ignore comment protects the next statement or member:

namespace Demo;

public static class Rates
{
    // csharpier-ignore
    public static readonly (string Country, decimal Vat)[] Aligned =
    [
        ("NL",   0.21m),
        ("DE",   0.19m),
        ("LU",   0.17m),
    ];

    public static readonly (string Country, decimal Vat)[] NotIgnored =
    [
        ("NL", 0.21m),
        ("DE", 0.19m),
        ("LU", 0.17m),
    ];
}

That is the file after formatting: the ignored table kept its spacing, and the second one, which had the same spacing before, was collapsed to single spaces. To skip whole files (generated code, migrations), list them in a .csharpierignore file, which uses .gitignore syntax. A .csharpierignore containing Migrations/ left a deliberately messy migration file untouched while the rest of the folder was formatted.

Format on Build with CSharpier.MSBuild

dotnet add package CSharpier.MSBuild

With the package referenced, a normal dotnet build formatted my messy file in place (the usings came out sorted) and the build succeeded. With -p:CSharpier_Check=true it checks instead of writing, and fails the build:

$ dotnet build -p:CSharpier_Check=true
D:\...\msb\OrderService.cs : error : Was not formatted.
Build FAILED.
    0 Warning(s)
    1 Error(s)

The file was left unchanged in check mode. One gotcha: the command-line tool refuses to run when a project references a different CSharpier.MSBuild version. With the tool at 1.3.0 and the package at 1.2.0:

$ dotnet csharpier format msb
Error The csproj at D:\...\msb\msb.csproj uses version 1.2.0 of CSharpier.MsBuild which is a mismatch with version 1.3.0

The exit code is 1, so a pre-commit hook or CI step stops there. Upgrade both together; --no-msbuild-check skips the check if you really need to.

Speed on 3,050 Files

I copied the C# files from my test projects 50 times (3,050 files, 171,400 lines, most of them already tidy) and timed each command twice from a fresh copy:

CommandTime
dotnet csharpier format --no-cache8.6–9.8 s
dotnet csharpier format again, cache populated0.8 s
dotnet csharpier check5.2–5.6 s
dotnet format whitespace --folder10.4–11.0 s
dotnet format whitespace --folder --verify-no-changes9.1–9.5 s

CSharpier keeps a cache of files it has already formatted, so repeated local runs only touch changed files. The first cached run after a --no-cache run still took the full 5 seconds; the cache is written by normal runs. In CI, where the cache is empty, check is the number that counts, and it was about twice as fast as dotnet format --verify-no-changes, while doing more (line wrapping and using order).

A CI Step That Works

These are the two commands I ran from a clean folder with only the tool manifest and an unformatted file. check printed the diff and exited with 1:

dotnet tool restore
dotnet csharpier check .

In GitHub Actions they go in a normal run step after actions/setup-dotnet. If you also run dotnet format style --verify-no-changes, put it in a separate step so the log shows which tool failed. The GitHub Actions guide for .NET covers the rest of the pipeline.

Which Should You Use?

SituationChoice
New project or team tired of style debates in reviewsCSharpier for layout + dotnet format style/analyzers for code rules
Large existing codebase, can't accept a one-time diff of every filedotnet format only, or CSharpier introduced folder by folder
Team needs a specific brace or wrapping styledotnet format with .editorconfig; CSharpier won't do it
No extra tools alloweddotnet format (ships with the SDK)

The first CSharpier run on an existing repository touches almost every file. Do it in one commit with nothing else in it and add that commit to .git-blame-ignore-revs, so git blame still points at real changes. The C# code review checklist and Git practices for .NET teams cover what to review once formatting is no longer a topic.

FAQ

Does CSharpier remove unused usings? No. A file with unused System.IO and System.Text usings came back with both still there, just sorted. Unused usings are an analyzer rule (IDE0005), so dotnet format style or the IDE handles them.

Can CSharpier put braces on the same line? No. Brace placement is not an option, and csharp_new_line_before_open_brace in .editorconfig is ignored.

Is formatting the same in Visual Studio and Rider? Only if the IDE runs CSharpier on save (there are official extensions); the IDE's own Format Document follows .editorconfig and can produce different output.

This article is part of the C# tutorials guide (language features, patterns, performance and testing).

References: CSharpier documentation · CSharpier configuration · CSharpier ignore rules · CSharpier.MSBuild on NuGet · Microsoft: dotnet format

Comments

Popular posts from this blog

.NET MAUI Tutorial 2026: Build Cross-Platform Apps in C#

Learn .NET MAUI in 2026 to build iOS, Android, Windows & Mac apps from one C# codebase. Start this cross-platform tutorial with code examples today. .NET MAUI (Multi-platform App UI) is Microsoft's framework for building native iOS, Android, Windows, and macOS apps from a single C# codebase . If you've ever wanted to ship a mobile app without learning Swift, Kotlin, and Win32 separately, this .NET MAUI tutorial for 2026 is your starting point. In this guide you'll learn what .NET MAUI is, why it matters for cross-platform app development in C#, and how to build your first working app — with runnable code examples and the best practices senior engineers actually use in production. What Is .NET MAUI and Why Use It in 2026? .NET MAUI is the evolution of Xamarin.Forms, fully integrated into the modern .NET runtime. With one project and one language — C# — you target four platforms. The framework compiles to native UI controls on each device, so a button on iOS...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...