├── .github ├── FUNDING.yml └── workflows │ └── nuget-publish.yaml ├── .gitignore ├── LICENSE ├── README.md ├── appveyor.yml └── src ├── DotNetSortRefs.sln └── DotNetSortRefs ├── CommandBase.cs ├── DotNetSortRefs.csproj ├── Program.cs └── Sort.xsl /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | ko_fi: 'babuannamalai' 3 | custom: ['https://www.buymeacoffee.com/babuannamalai'] 4 | -------------------------------------------------------------------------------- /.github/workflows/nuget-publish.yaml: -------------------------------------------------------------------------------- 1 | name: NuGet Manual Publish 2 | 3 | on: [workflow_dispatch] 4 | 5 | env: 6 | config: Release 7 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 8 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 9 | 10 | jobs: 11 | publish_job: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | - name: Install .NET 8.0.x 19 | uses: actions/setup-dotnet@v4 20 | with: 21 | dotnet-version: 9.0.x 22 | 23 | - name: Run Pack 24 | run: dotnet pack src/DotNetSortRefs/DotNetSortRefs.csproj -c Release 25 | shell: bash 26 | 27 | - name: Publish to NuGet 28 | run: | 29 | find . -name '*.nupkg' -exec dotnet nuget push "{}" -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_API_KEY }} --skip-duplicate \; 30 | # find . -name '*.snupkg' -exec dotnet nuget push "{}" -s https://api.nuget.org/v3/index.json -k ${{ secrets.NUGET_API_KEY }} \; 31 | shell: bash 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | obj 3 | bin 4 | bin/* 5 | deploy 6 | deploy/* 7 | _ReSharper.* 8 | *.user 9 | *.suo 10 | *.cache 11 | *.Cache 12 | Thumbs.db 13 | **/packages 14 | .idea 15 | nupkg 16 | .DS_Store 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Babu Annamalai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dotnet-sort-refs 2 | [![Build status](https://ci.appveyor.com/api/projects/status/q3tr2vkto018w1l7/branch/master?svg=true)](https://ci.appveyor.com/project/BabuAnnamalai/dotnet-sort-refs/branch/master) [![NuGet Version](https://badgen.net/nuget/v/dotnet-sort-refs)](https://www.nuget.org/packages/dotnet-sort-refs/) 3 | 4 | A .NET Core global tool to alphabetically sort package references in your .NET Core and .NET Standard projects. 5 | 6 | If you have benefitted from this library and has saved you a bunch of time, please feel free to buy me a coffee!
7 | GitHub Sponsor 8 | 9 | ## Why use this tool? 10 | References and package references in a project file are the most updated parts. Sorting the references helps with the following: 11 | - Easier merges on source control (git). Without sorting the package references in the project file, you may end up with more merge conflicts to fix. 12 | - It will be easier to go through the list of package references if you manually edit the file or view changes using a diff tool. 13 | 14 | ## Installation 15 | ```bash 16 | dotnet tool install --global dotnet-sort-refs 17 | ``` 18 | 19 | ## Usage 20 | ```text 21 | dotnet sort-refs [arguments] [options] 22 | 23 | Arguments: 24 | Path The path to a .csproj, .fsproj or directory. If a directory is specified, all .csproj and .fsproj files within folder tree will be processed. If none specified, it will use the current directory. 25 | 26 | Options: 27 | --version Show version information 28 | -?|-h|--help Show help information 29 | -i|--inspect Specifies whether to inspect and return a non-zero exit code if one or more projects have non-sorted package references. 30 | ``` 31 | 32 | Note: `dotnet sort-refs` and `dotnet-sort-refs` are valid usages to run the tool. 33 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: build-{build} 2 | 3 | configuration: Release 4 | 5 | image: Ubuntu2204 6 | 7 | environment: 8 | DOTNET_CLI_TELEMETRY_OPTOUT: true 9 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 10 | NUGET_DEPLOY_KEY: 11 | secure: bfIsyEMzYM9AJCFQwxLjAC48zi8/pNgUokbWcEcEo4hzLvrGCKEN6pNYkQIx/w2J 12 | 13 | nuget: 14 | disable_publish_on_pr: true 15 | 16 | before_build: 17 | - dotnet --info 18 | - dotnet restore .\src\DotNetSortRefs.sln 19 | 20 | build_script: 21 | - dotnet build .\src\DotNetSortRefs.sln --configuration Release 22 | 23 | # after_test: 24 | # - dotnet pack .\src\DotNetSortRefs --configuration Release 25 | 26 | # artifacts: 27 | # - path: '**\dotnet-sort-refs.*.*nupkg' # find all NuGet packages recursively 28 | 29 | # deploy: 30 | # provider: NuGet 31 | # api_key: $(NUGET_DEPLOY_KEY) 32 | # artifact: /.*\.nupkg/ 33 | # on: 34 | # APPVEYOR_REPO_TAG: true 35 | 36 | -------------------------------------------------------------------------------- /src/DotNetSortRefs.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.136 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetSortRefs", "DotNetSortRefs\DotNetSortRefs.csproj", "{18F8C228-6832-4F8F-B629-79A368B23339}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {18F8C228-6832-4F8F-B629-79A368B23339}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {18F8C228-6832-4F8F-B629-79A368B23339}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {18F8C228-6832-4F8F-B629-79A368B23339}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {18F8C228-6832-4F8F-B629-79A368B23339}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {08399582-339B-4DBF-847C-2603C9AE098C} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /src/DotNetSortRefs/CommandBase.cs: -------------------------------------------------------------------------------- 1 | using McMaster.Extensions.CommandLineUtils; 2 | 3 | namespace DotNetSortRefs 4 | { 5 | [HelpOption] 6 | internal abstract class CommandBase 7 | { 8 | } 9 | } -------------------------------------------------------------------------------- /src/DotNetSortRefs/DotNetSortRefs.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | A .NET Core global tool to alphabetically sort package references in csproj or fsproj 4 | 2.1.0 5 | Babu Annamalai 6 | Exe 7 | netcoreapp3.1;net5.0;net6.0;net7.0;net8.0;net9.0 8 | dotnet-sort-refs 9 | dotnet-sort-refs 10 | ./nupkg 11 | https://github.com/mysticmind/dotnet-sort-refs 12 | MIT 13 | true 14 | true 15 | dotnet-sort-refs 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/DotNetSortRefs/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.IO.Abstractions; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Threading.Tasks; 8 | using System.Xml; 9 | using System.Xml.Linq; 10 | using System.Xml.XPath; 11 | using System.Xml.Xsl; 12 | using McMaster.Extensions.CommandLineUtils; 13 | using Microsoft.Extensions.DependencyInjection; 14 | 15 | namespace DotNetSortRefs 16 | { 17 | [Command( 18 | Name = "dotnet sort-refs", 19 | FullName = "A .NET Core global tool to alphabetically sort package references in csproj or fsproj.")] 20 | [VersionOptionFromMember(MemberName = nameof(GetVersion))] 21 | internal class Program : CommandBase 22 | { 23 | private readonly IFileSystem _fileSystem; 24 | private readonly IReporter _reporter; 25 | 26 | public Program(IFileSystem fileSystem, IReporter reporter) 27 | { 28 | _fileSystem = fileSystem; 29 | _reporter = reporter; 30 | } 31 | 32 | static int Main(string[] args) 33 | { 34 | using var services = new ServiceCollection() 35 | .AddSingleton(PhysicalConsole.Singleton) 36 | .AddSingleton(provider => new ConsoleReporter(provider.GetService()!)) 37 | .AddSingleton() 38 | .BuildServiceProvider(); 39 | var app = new CommandLineApplication 40 | { 41 | UnrecognizedArgumentHandling = UnrecognizedArgumentHandling.Throw 42 | }; 43 | 44 | app.Conventions 45 | .UseDefaultConventions() 46 | .UseConstructorInjection(services); 47 | 48 | try 49 | { 50 | return app.Execute(args); 51 | } 52 | catch (UnrecognizedCommandParsingException) 53 | { 54 | app.ShowHelp(); 55 | return 1; 56 | } 57 | } 58 | 59 | [Argument(0, Description = 60 | "The path to a .csproj, .fsproj or directory. If a directory is specified, all .csproj and .fsproj files within folder tree will be processed. If none specified, it will use the current directory.")] 61 | private string Path { get; set; } 62 | 63 | [Option(CommandOptionType.NoValue, Description = "Specifies whether to inspect and return a non-zero exit code if one or more projects have non-sorted package references.", 64 | ShortName = "i", LongName = "inspect")] 65 | private bool IsInspect { get; set; } = false; 66 | 67 | private static string GetVersion() => typeof(Program) 68 | .Assembly 69 | .GetCustomAttribute() 70 | ?.InformationalVersion; 71 | 72 | private async Task OnExecute(CommandLineApplication app, IConsole console) 73 | { 74 | try 75 | { 76 | if (string.IsNullOrEmpty(Path)) 77 | Path = _fileSystem.Directory.GetCurrentDirectory(); 78 | 79 | if (!(_fileSystem.File.Exists(Path) || _fileSystem.Directory.Exists(Path))) 80 | { 81 | _reporter.Error("Directory or file does not exist."); 82 | return 1; 83 | } 84 | 85 | var projFiles = new List(); 86 | var extensions = new[] { ".csproj", ".fsproj", ".props" }; 87 | 88 | if (_fileSystem.File.Exists(Path)) 89 | { 90 | projFiles.Add(Path); 91 | } 92 | else 93 | { 94 | projFiles = extensions 95 | .SelectMany(ext => _fileSystem.Directory.GetFiles(Path, $"*{ext}", SearchOption.AllDirectories)) 96 | .ToList(); 97 | } 98 | 99 | if (projFiles.Count == 0) 100 | { 101 | _reporter.Error($"no '{string.Join(", ", extensions)}'' files found."); 102 | return 1; 103 | } 104 | 105 | var projFilesWithNonSortedReferences = await Inspect(projFiles); 106 | 107 | if (IsInspect) 108 | { 109 | Console.WriteLine("Running inspection..."); 110 | PrintInspectionResults(projFiles, projFilesWithNonSortedReferences); 111 | return projFilesWithNonSortedReferences.Count > 0 ? 1 : 0; 112 | } 113 | else 114 | { 115 | Console.WriteLine("Running sort package references..."); 116 | return await SortReferences(projFilesWithNonSortedReferences); 117 | } 118 | } 119 | catch (Exception e) 120 | { 121 | _reporter.Error(e.StackTrace!); 122 | return 1; 123 | } 124 | } 125 | 126 | private static async Task> Inspect(IEnumerable projFiles) 127 | { 128 | var projFilesWithNonSortedReferences = new List(); 129 | 130 | foreach (var proj in projFiles) 131 | { 132 | using (var sw = new StringWriter()) 133 | { 134 | var doc = XDocument.Parse(System.IO.File.ReadAllText(proj)); 135 | 136 | const string elementTypes = "PackageReference|Reference|PackageVersion"; 137 | var itemGroups = doc.XPathSelectElements($"//ItemGroup[{elementTypes}]"); 138 | 139 | foreach (var itemGroup in itemGroups) 140 | { 141 | var references = itemGroup.XPathSelectElements(elementTypes) 142 | .Select(x => x.Attribute("Include")?.Value.ToLowerInvariant()).ToList(); 143 | 144 | if (references.Count <= 1) continue; 145 | 146 | var sortedReferences = references.OrderBy(x => x).ToList(); 147 | 148 | var result = references.SequenceEqual(sortedReferences); 149 | 150 | if (!result && !projFilesWithNonSortedReferences.Contains(proj)) 151 | { 152 | projFilesWithNonSortedReferences.Add(proj); 153 | } 154 | } 155 | } 156 | } 157 | 158 | return await Task.FromResult(projFilesWithNonSortedReferences); 159 | } 160 | 161 | private void PrintInspectionResults(IEnumerable projFiles, 162 | ICollection projFilesWithNonSortedReferences) 163 | { 164 | foreach (var proj in projFiles) 165 | { 166 | if (projFilesWithNonSortedReferences.Contains(proj)) 167 | { 168 | _reporter.Error($"» {proj} X"); 169 | } 170 | else 171 | { 172 | _reporter.Output($"» {proj} ✓"); 173 | } 174 | } 175 | } 176 | 177 | private async Task SortReferences(IEnumerable projFiles) 178 | { 179 | var xslt = GetXslTransform(); 180 | 181 | foreach (var proj in projFiles) 182 | { 183 | _reporter.Output($"» {proj}"); 184 | 185 | await using var sw = new StringWriter(); 186 | var doc = XDocument.Parse(await System.IO.File.ReadAllTextAsync(proj)); 187 | xslt.Transform(doc.CreateNavigator(), null, sw); 188 | await File.WriteAllTextAsync(proj, sw.ToString()); 189 | } 190 | 191 | return await Task.FromResult(0); 192 | } 193 | 194 | private static XslCompiledTransform GetXslTransform() 195 | { 196 | var assembly = Assembly.GetExecutingAssembly(); 197 | using var stream = assembly.GetManifestResourceStream("DotNetSortRefs.Sort.xsl"); 198 | using var reader = XmlReader.Create(stream!); 199 | var xslt = new XslCompiledTransform(); 200 | xslt.Load(reader); 201 | return xslt; 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/DotNetSortRefs/Sort.xsl: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | --------------------------------------------------------------------------------