Home

Awesome

Route Generator for .NET

License Nuget

Tired of manually specifying route identifiers and fixing typos in the route-based navigation of your .NET app? This source generator will take away that pain.

Introduction

Route Generator is a C# source generator that generates a static Routes class for your .NET app containing all route identifiers for your string-based route navigation.

Although the sample project is using .NET MAUI, this generator can also be used with other .NET technologies since it targets .NET Standard 2.0.

Basic Usage

First, add the epj.RouteGenerator nuget package to your target project that contains the classes (i.e. pages) from which routes should be automatically generated.

Then, use the [AutoRoutes] attribute from the epj.RouteGenerator namespace on one of the classes at the root of your application. This must be within the same project and namespace containing the pages.

You must provide a suffix argument which represents the naming convention for your routes to the attribute, e.g. "Page" like above. It doesn't have to be "Page", it depends on the naming convention you use for pages in your app. If all your page classes end in "Site", then you would pass "Site" to the attribute. This suffix is used in order to identify all classes that should be included as routes in the generated Routes class based on their class name.

When using .NET MAUI, I recommend to use the attribute to decorate the MauiProgram class:

[AutoRoutes("Page")]
public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();

        // ...

        return builder.Build();
    }
}

The source generator will then pick up all pages that end in the specified suffix and generate the Routes class with these identifiers within the same root namespace as the entry point:

// <auto-generated/>
using System.Collections.ObjectModel;

namespace RouteGeneratorSample
{
    public static class Routes
    {
        public const string MainPage = "MainPage";
        public const string VolvoPage = "VolvoPage";
        public const string AudiPage = "AudiPage";
    
        private static List<string> allRoutes = new()
        {
            MainPage,
            VolvoPage,
            AudiPage,
        };
        
        public static ReadOnlyCollection<string> AllRoutes => allRoutes.AsReadOnly();

        private static Dictionary<string, Type> routeTypeMap = new()
        {
            { MainPage, typeof(RouteGeneratorSample.MainPage) },
            { VolvoPage, typeof(RouteGeneratorSample.Cars.VolvoPage) },
            { AudiPage, typeof(RouteGeneratorSample.Cars.AudiPage) },
        };
        
        public static ReadOnlyDictionary<string, Type> RouteTypeMap => routeTypeMap.AsReadOnly();
    }
}

Now, you can use these identifiers for your navigation without having to worry about typos in your string-based route navigation:

await Shell.Current.GoToAsync($"/{Routes.AudiPage}");

If the AudiPage would ever get renamed to some other class name, you would instantly notice, because the compiler wouldn't find the Routes.AudiPage symbol anymore and emit an error, letting you know that it has changed. When using verbatim strings instead of an identifier like this, you wouldn't notice this change until the app either crashes or stops behaving the way it should.

Extra Routes

There may be situations where you need to be able to specify extra routes, e.g. when a route doesn't follow the specified naming convention using the suffix or when a routes is defined in a different way (in MAUI or Xamarin.Forms this could be using a <ShellContent> element in XAML).

For situations like these, the Route Generator exposes a second attribute called [ExtraRoute] and it takes a single argument representing the name of the route. You should not pass null, empty strings, any whitespace or special characters. Duplicates will be ignored.

If an extra route is specified whose name doesn't match any existing class name, you will have to provide a type to the attribute in order to include it in the generated Routes.RouteTypeMap dictionary using typeof(SomeClass).

namespace RouteGeneratorSample;

[AutoRoutes("Page")]
[ExtraRoute("SomeFaulty!Route")] // invalid, will emit warning EXR001 and will be ignored
[ExtraRoute("YetAnotherRoute", typeof(MainPage))]
[ExtraRoute("YetAnotherRoute")] // duplicate, will emit warning EXR002 and will be ignored
[ExtraRoute("SomeOtherRoute")] // valid, but no corresponding type available, will emit warning EXR003
[ExtraRoute(null)] // will be ignored
public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        
        // ...

        return builder.Build();
    }
}

This would then result in the following Routes:

// <auto-generated/>
using System.Collections.ObjectModel;

namespace RouteGeneratorSample
{
    public static class Routes
    {
        public const string MainPage = "MainPage";
        public const string VolvoPage = "VolvoPage";
        public const string AudiPage = "AudiPage";
        public const string SomeOtherRoute = "SomeOtherRoute";
        public const string YetAnotherRoute = "YetAnotherRoute";
    
        private static List<string> allRoutes = new()
        {
            MainPage,
            VolvoPage,
            AudiPage,
            SomeOtherRoute,
            YetAnotherRoute
        };
        
        public static ReadOnlyCollection<string> AllRoutes => allRoutes.AsReadOnly();

        private static Dictionary<string, Type> routeTypeMap = new()
        {
            { MainPage, typeof(RouteGeneratorSample.MainPage) },
            { VolvoPage, typeof(RouteGeneratorSample.Cars.VolvoPage) },
            { AudiPage, typeof(RouteGeneratorSample.Cars.AudiPage) },
            { YetAnotherRoute, typeof(RouteGeneratorSample.MainPage) },
        };
        
        public static ReadOnlyDictionary<string, Type> RouteTypeMap => routeTypeMap.AsReadOnly();
    }
}

Note: If you don't provide a type to the [ExtraRoute] attribute and the specified route doesn't match any existing class name, the Routes.RouteTypeMap dictionary will not contain an entry for that route. Above, this is the case for the "SomeOtherRoute" route.

Ignore specific routes

Starting with v1.0.4, there is an [IgnoreRoute] attribute that can be used to exclude specific classes from being included in the generated Routes class. This can be useful if you have a class that matches the naming convention but should not be included as a route for some reason (e.g. non-abstract base classes):

using epj.RouteGenerator;

namespace RouteGeneratorSample;

[IgnoreRoute]
public class SomeIgnorableRoute { }

Also, abstract classes are now ignored by default:

namespace RouteGeneratorSample;

// will be ignored by default
public abstract class BaseRoute { }

// will be ignored by default
public abstract class TypedBaseRoute<T> { }

Route registration (e.g. in .NET MAUI)

Inspired by a comment by Miguel Delgado, version 1.0.1 introduced a new Routes.RouteTypeMap dictionary that maps route names to their respective Type. This can be used to register routes like this:

foreach (var route in Routes.RouteTypeMap)
{
    Routing.RegisterRoute(route.Key, route.Value);
}

Since this library is not MAUI-specific, I will not add such a utility method directly to this library. However, as mentioned below, automatic registration could be handled in a MAUI-specific layer.

Resources

The Route Generator is featured in the following resources:

Future Ideas

Remarks

C# version compatibility

This source generator only works with C# 10.0 or higher. If you are using .NET 5.0 or below, you will need to specify <LangVersion>10.0</LangVersion> in your project file.

The source generator is compatible with nullable reference types, the [ExtraRoute] attribute uses a Type? property. Please let me know if you run into problems with this.

Native AOT support

While Native AOT is still experimental in .NET 8.0 (e.g., it's not supported for Android yet and even iOS still is experiencing some hiccups), the latest version of Route Generator should technically be AOT-compatible. However, I cannot test this properly while there are still issues. Full Native AOT support will probably only be available with .NET 9.0 or higher according to this issue on GitHub.

Support

You can support this project by starring it on GitHub, sharing it with others or contributing to it. If you have any questions, feedback or ideas, feel free to open an issue or reach out to me.

Additionally, you can support me by buying me a coffee or by becoming a sponsor.

<a href="https://www.buymeacoffee.com/ewerspej" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-yellow.png" alt="Buy Me A Coffee" height="41" width="174"></a>