2

I want to place a REST [ApiController] with multiple actions and sub routes in a class library that I plan to reuse across projects. I would like to register the controller route via endpoint routing and specify the main route name in the appsettings.json of each project.

So in project1 I have

GET /someRoute/stuff/1
DELETE /someRoute/stuff/1
POST /someRoute/stuff/1/otherStuff

and in project2

GET /otherRoute/stuff/1
DELETE /otherRoute/stuff/1
POST /otherRoute/stuff/1/otherStuff

The problem is [ApiController] requires a [Route] attribute that takes a static string, so I cannot set the route from the app using the library. Alternatively I have tried to register each individual action via MapControllerRoute (maybe a stupid approach), but not sure how to proceed when I have multiple actions (get/delete/post) on the same route (eg: /otherName/stuff/1). I am using .NetCore3.

I've read the documentation but can't seem to figure this one out so any input is appreciated.

EDIT: I would also like to have different authorization requirements per project. For example in project1 the controller might be restricted to admins, in project2 it should satisfy a policy and finally let's say in project3 it should be open to everybody (no authorization required).

notes404
  • 33
  • 4
  • 1
    Please share the code you have tried so far? – Noah Stahl Oct 31 '20 at 20:05
  • 1
    @NoahStahl the code is actually of no value in this question. The OP described the problem sufficiently well, imho – CoolBots Oct 31 '20 at 20:15
  • 1
    I would make `ApiController`s in the library project `abstract`, and inherit from them in each actual Web API or MVC project you need them in. It's a bit more boilerplate, but minimal, and you can then easily configure routing per project. The routing for each method would be relative to the controller `[Route]`, so you'd only need to specify that single attribute in each inherited `ApiController` – CoolBots Oct 31 '20 at 20:18
  • 1
    Once C#9 and .NET 5 are fully out, this sounds like an awesome use case for Source Generators: https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/ – CoolBots Oct 31 '20 at 20:23
  • 1
    @CoolBots I think your approach might be the most straightforward as it provides a lot of flexibility when managing things like authorization, whether or not to include it in the api explorer, etc. Thanks for the bonus of C# source generators as it definitely a very interesting read. – notes404 Nov 01 '20 at 14:05

1 Answers1

2

Really dirty way to do this, but I am not sure if you have shorter and better options.

Library project I am going to create custom attribute that would override RouteAttribute's template with value from config. Firstly, since we don't have a way to use dependency injection with attributes, we need static resolver, taken from here.

using System;

namespace ClassLibrary1
{
    public class AppDependencyResolver
    {
        private static AppDependencyResolver _resolver;

        public static AppDependencyResolver Current
        {
            get
            {
                if (_resolver == null)
                    throw new Exception("AppDependencyResolver not initialized. You should initialize it in Startup class");
                return _resolver;
            }
        }

        public static void Init(IServiceProvider services)
        {
            _resolver = new AppDependencyResolver(services);
        }

        private readonly IServiceProvider _serviceProvider;

        public object GetService(Type serviceType)
        {
            return _serviceProvider.GetService(serviceType);
        }

        public T GetService<T>()
        {
            return (T)_serviceProvider.GetService(typeof(T));
        }

        private AppDependencyResolver(IServiceProvider serviceProvider)
        {
            _serviceProvider = serviceProvider;
        }
    }
}

Next thing, custom attribute that inherits from RouteAttribute

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;

namespace ClassLibrary1
{
    public class DynamicRouteAttribute : RouteAttribute
    {
        public DynamicRouteAttribute(string template) : base(ModifyTemplate(template))
        {
        }

        private static string ModifyTemplate(string template)
        {
            var config = AppDependencyResolver.Current.GetService<IConfiguration>();
            var routePrefix = config.GetValue<string>("route");

            return routePrefix + "/" + template;
        }
    }
}

And controller itself

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;

namespace ClassLibrary1
{
    [DynamicRoute("stuff")]
    [ApiController]
    public class LibraryController : ControllerBase
    {
        [Route("")]
        [HttpGet]
        public void Get(int id)
        { }

        [Route("")]
        [HttpDelete]
        public void Delete(int id)
        { }

        [Route("")]
        [HttpPost]
        public void Post(int id)
        { }
    }
}

Parent (Web) project. Register your static resolver

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    AppDependencyResolver.Init(app.ApplicationServices);
    ...
}

And put your prefix value to appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "route": "someRoute"
}

Using this solution we can check all available routes. For the second web projects steps will be the same, just different value in config file.

enter image description here

Yehor Androsov
  • 4,885
  • 2
  • 23
  • 40
  • Thank you very much for taking the time to write this (there are some cool things in the links too). Instead of getting IConfiguration from the resolver, I could get the library settings class via IOptions for a cleaner approach. Your code covers my issues, however I fail to see how it would deal with authorization (at the controller level, not on individual actions). I know I did not mention it initially in my post. – notes404 Nov 01 '20 at 13:57
  • @notes404 I think [Authorize] attribute should work, but if not feel free to adjust your question – Yehor Androsov Nov 01 '20 at 14:01
  • I updated my question. Let me know if anything is unclear. Thank you in advance. – notes404 Nov 01 '20 at 14:10
  • 1
    I think it is possible too, with breaking policy attribute like I did with route, making anonymous users have fake authentication to make them have policy too and other stuff. but probably this is too much work, and you better just inherit controllers from library per each project as CoolBots suggested – Yehor Androsov Nov 01 '20 at 14:31