sebnilsson.com | Liquid Development Is What I Do
Seb Nilsson

dotnet-cleanup: Clean Up Solution, Project & Folder

We developers like to think of developing software as an exact science, but sometimes, you just need to wipe your source-files to solve some kinds of problems.

For .NET-developers, there are many issues on Stackoverflow which are solved by just deleting your bin and obj-folders. For people using Node.js, probably just as many answers contains the step of removing your node_modules-folder.

Those are some of the reasons why I created dotnet-cleanup, which is a .NET Core Global Tool for cleaning up solution, project or folder. This was made easy by following Nate McMaster's post on getting started with creating a .NET Core global tool package.

Deleted files and folders are first moved to a temporary folder before deletion, so you can continue working with your projects, while the tool keeps cleaning up in background.

Installation

Download the .NET Core SDK 2.1 or later. The install the dotnet-cleanup .NET Global Tool, using the command-line:

dotnet tool install -g dotnet-cleanup

Usage

Usage: cleanup [arguments] [options]

Arguments:
  PATH                  Path to the solution-file, project-file or folder to clean. Defaults to current working directory.

Options:
  -p|--paths            Defines the paths to clean. Defaults to 'bin', 'obj' and 'node_modules'.
  -y|--confirm-cleanup  Confirm prompt for file cleanup automatically.
  -nd|--no-delete       Defines if files should be deleted, after confirmation.
  -nm|--no-move         Defines if files should be moved before deletion, after confirmation.
  -t|--temp-path        Directory in which the deleted items should be moved to before being cleaned up. Defaults to system Temp-folder.
  -v|--verbosity        Sets the verbosity level of the command. Allowed levels are Minimal, Normal, Detailed and Debug.
  -?|-h|--help          Show help information

The argument PATH can point to a specific .sln-file or a project-file (.csproj, .fsharp, .vbproj). If a .sln-file is specified, all its projects will be cleaned.

If it points to a folder, the folder will be scanned for a single solution-file and then for a single project-file. If multiple files are detected an error will be shown and you need to specify the file.

If not solution or project is found, the folder will be cleaned as a project.

Example

To cleanup a typical web-project, you can specify the paths to be cleaned in the projects like this:

cleanup -p "bin" -p "obj"  -p "artifacts" -p "npm_modules"

You can find the source-code on GitHub, the latest builds on MyGet and the package on Nuget.

You can find a great list of more .NET Core Global Tools on GitHub, maintained by Nate McMaster.

LINQ Distinct-Method using Lambda Expression

If you've ever wanted to filter a collection for a distinct result you probably know about the extension-method .Distinct in LINQ. It can be useful on simple data structures containing easily comparable objects, like I collection of strings or ints, but for more complex scenarios you need to pass in a IEqualityComparer. This is not very convenient.

More convenient would be to able to pass in a Lambda-expression, specifying what field you want to do the distinction by, like this:

var distinctItems = items.Distinct(x => x.Id);

To do this, you can add the following extension-methods to your projects:

public static IQueryable<TSource> Distinct<TSource>(
    this IQueryable<TSource> source, Expression<Func<TSource, object>> predicate)
{
    // TODO: Null-check arguments
    return from item in source.GroupBy(predicate) select item.First();
}

public static IEnumerable<TSource> Distinct<TSource>(
    this IEnumerable<TSource> source, Func<TSource, object> predicate)
{
    // TODO: Null-check arguments
    return from item in source.GroupBy(predicate) select item.First();
}

The extension-method using IQueryable<T> works with ORMs like Entity Framework, while IEnumerable<T> works with all types of collections, in-memory or otherwise, depending on implementation.

Warning: Avoid using this with EF Core version 1.x or 2.0, since the .GroupBy-execution is always made in-memory. So you might get the whole content of your database loaded into memory. Only use it with EF Core 2.1 and above in production-scenarios.

API-Versioning in ASP.NET

The ASP.NET Route Versions-library was created after I was inspired by a discussion with a colleague and reading the great article Your API versioning is wrong by Troy Hunt, where he concludes that you don't need a war of preferences between different ways of versioning your API, you can actually support multiple ways in the same API.

In his article, Troy lists 3 ways (to do it wrong), which I have implemented for ASP.NET Core, and added support for one more way, which is URL versioning. This library supports the following ways to version your API:

  • URL versioning
  • Query string versioning
  • Custom request header
  • Content type

URL versioning:

HTTP GET:
https://my-web-app.com/api/v2/customers

Query string versioning:

HTTP GET:
https://my-web-app.com/api/customers?api-version=2

Custom request header:

HTTP GET:
https://my-web-app.com/api/customers
api-version: 2

Content type:

HTTP GET:
https://my-web-app.com/api/customers
Accept: application/vnd.api-version.v2+json

[RouteVersion]-attribute

All you need to do is use the [RouteVersion]-attribute on the Controller-Actions you want to version and provide the route-version as an argument:

[Route("api/v{api-version}/[controller]")]
[Route("api/[controller]")]
[ApiController]
public class CustomersController : ControllerBase
{
    [HttpGet]
    [RouteVersion(1)]
    public ActionResult<string> GetV1()
    {
        return "Get Customers Version 1";
    }

    [HttpGet]
    [RouteVersion(2)]
    public ActionResult<string> GetV2()
    {
        return "Get Customers Version 2";
    }

    [HttpPost]
    [RouteVersion(1)]
    public ActionResult<string> PostV1()
    {
        return "Post Customers Version 1";
    }

    [HttpPost]
    [RouteVersion(2)]
    public ActionResult<string> PostV2()
    {
        return "Post Customers Version 2";
    }
}

The attribute will only resolve versioning between Controller-Actions, everything else is handled by the regular ASP.NET Core routing, and behave as you're used to.

Configuration

In your Startup.cs you can configure what ways of API-versioning you want to support (all activated by default). You can also change the keys of the routing, query string, custom header and content type.

public void ConfigureServices(IServiceCollection services)
{
    services.ConfigureRouteVersions(options =>
    {        
        options.UseRoute = true;
        options.UseQuery = true;
        options.UseCustomHeader = true;
        options.UseAcceptHeader = true;

        // Set route-name in template used. For example: "api/v{version}/[controller]"
        // Default: "api-version"
        options.RouteKey = "version";

        // Set query string-key used. For example: "/api/customers?v=1"
        // Default: "api-version"
        options.QueryKey = "v"; // To use: '/api/customers?v=1'

        // Set custom version header used. For example: "my-app-api-version"
        // Default: "api-version"
        options.CustomHeaderKey = "my-app-api-version";

        // Set Accept-header vendor used. For example: "application/vnd.my-custom-api-header.v1+json"
        // Default: "application/vnd.api-version.v1+json"
        options.SetAcceptHeader("my-custom-api-header");

        // Set Accept-header regex-pattern. For example: "application/pre.my-custom-vendor-api.v1+json"
        options.AcceptRegexPattern = @"application\/pre\.my-custom-vendor-api\.v([\d]+)\+json";
    });

    services.AddMvc();
}

Default version

If you know that your new version of an API-endpoint is compatible with previous version, and if you want to support it, you can use the IsDefault-parameter with the [RouteVersion]-attribute. For example, if you've just added new fields to the next version and you find that is compatible enough to be the default version of the API-endpoint:

[Route("api/[controller]")]
[ApiController]
public class CustomersController : ControllerBase
{
    [HttpGet]
    [RouteVersion(1)]
    public ActionResult<string> GetV1()
    {
        return "Get Customers Version 1";
    }

    [HttpGet]
    [RouteVersion(2, IsDefault = true)]
    public ActionResult<string> GetV2()
    {
        return "Get Customers Version 2";
    }
}

Then you can make a call to the URL for the API-endpoint without specifying the version and get the default version, which in this example is v2:

HTTP GET:
https://my-web-app.com/api/customers/
> "Get Customers Version 2"

Contributing

You can find the source code on GitHub, the newest unstable build on MyGet and the latest version of the library on NuGet