Routes with different controllers but same action name fails to produce wanted urls

Viewed 25175

I am trying to set up a API for my MVC web app that will have a lot of routes but much of the same part for each one. Basically a CRUD for each area. I am also setting it up to be version-able. I have set up two controllers each with a simple action to get started and receive a conflict right off the bat. The error I get is

I am after these urls

The MVC will let you have a

So I am looking for a away around needlessly naming things like contacts_delete and locations_delete producing URLs like

I may as well just do https://foo.bar/aim/v1/contacts_delete/11111 but that seems so senseless to me. If the MVC can do it, i have to believe that there is a way to make this happen.

The error I get is:

Attribute routes with the same name 'delete' must have the same template:

Action: 'rest.fais.foo.edu.Controllers.aimContactsController.delete (rest.fais.foo.edu)' - Template: 'aim/v1/contacts/delete/{id}'

Action: 'rest.fais.foo.edu.Controllers.aimLocationsController.delete (rest.fais.foo.edu)' - Template: 'aim/v1/locations/delete/{id}'

For the controllers I have set up

[EnableCors("SubDomains")]
[ResponseCache(NoStore = true, Duration = 0)]
[Produces("application/json")]
[Route("aim/v1/contacts/[action]")]
[ProducesResponseType(typeof(errorJson), 500)]
public class aimContactsController : Controller
{
    private readonly IHostingEnvironment _appEnvironment;
    private readonly AimDbContext _aim_context;
    private readonly UserManager<ApplicationUser> _userManager;

    public aimContactsController(IHostingEnvironment appEnvironment,
        AimDbContext aim_context,
        UserManager<ApplicationUser> userManager)
    {
        _appEnvironment = appEnvironment;
        _userManager = userManager;
        _aim_context = aim_context;
    }



    [HttpPost("{id}", Name = "delete")]
    public IActionResult delete(string id)
    {

        return Json(new
        {
            results = "deleted"
        });
    }

}


[EnableCors("SubDomains")]
[ResponseCache(NoStore = true, Duration = 0)]
[Produces("application/json")]
[Route("aim/v1/locations/[action]")]
[ProducesResponseType(typeof(errorJson), 500)]
public class aimLocationsController : Controller
{
    private readonly IHostingEnvironment _appEnvironment;
    private readonly AimDbContext _aim_context;
    private readonly UserManager<ApplicationUser> _userManager;

    public aimLocationsController(IHostingEnvironment appEnvironment,
        AimDbContext aim_context,
        UserManager<ApplicationUser> userManager)
    {
        _appEnvironment = appEnvironment;
        _userManager = userManager;
        _aim_context = aim_context;
    }



    [HttpPost("{id}", Name = "delete")]
    public IActionResult delete(string id)
    {

        return Json(new
        {
            results = "deleted"
        });
    }

}

For the Startup.cs I have

    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole(Configuration.GetSection("Logging"));
        loggerFactory.AddDebug();
        //RolesData.SeedRoles(app.ApplicationServices).Wait();

        app.UseApplicationInsightsRequestTelemetry();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
            app.UseBrowserLink();
        }
        else
        {
            //app.UseExceptionHandler("/Home/Error");
        }

        app.UseIdentity();
        app.UseDefaultFiles();
        app.UseStaticFiles();

        //app.UseResponseCompression();

        // Add external authentication middleware below. To configure them please see http://go.microsoft.com/fwlink/?LinkID=532715
        app.UseSession();

        // custom Authentication Middleware
        app.UseWhen(x => (x.Request.Path.StartsWithSegments("/aim_write", StringComparison.OrdinalIgnoreCase) || x.Request.Path.StartsWithSegments("/rms", StringComparison.OrdinalIgnoreCase)),
        builder =>
        {
            builder.UseMiddleware<AuthenticationMiddleware>();
        });

        // Enable middleware to serve generated Swagger as a JSON endpoint.
        app.UseSwagger();

        // Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint.
        app.UseSwaggerUI(c =>
        {
            c.RoutePrefix = "docs";
            //c.SwaggerEndpoint("/docs/v1/wsu_restful.json", "v1.0.0");swagger
            c.SwaggerEndpoint("/swagger/v1/swagger.json", "v1.0.0");
        });


        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "javascript",
                template: "javascript/{action}.js",
                defaults: new { controller = "mainline" });
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
            routes.MapRoute(
                name: "AIMApi",
                template: "aim/v1/{action}/{id?}",
                defaults: new { controller = "aim" });
            routes.MapRoute(
                name: "AIMContactsApi",
                template: "aim/v1/contacts/{action}/{id?}",
                defaults: new { controller = "aimContactsController" }
            );
            routes.MapRoute(
                name: "AIMLocationsApi",
                template: "aim/v1/locations/{action}/{id?}",
                defaults: new { controller = "aimLocationsController" }
            );
            routes.MapRoute(
                name: "RMSApi",
                template: "{controller=rms}/v1/{action}/{id?}");
        });
    }
}

What I can't seem to google out the answer how to work around this. I want to fix the immediate issue, but any suggests are welcomed.

3 Answers

Another possible solution is:

// other solution
[Route("aim/v1/contacts/[Action]")]
public class aimContactsController : Controller
{
    [HttpPost("{id}")]
    public IActionResult delete(string id)
    {
        // omitted ...
    }
}

Example with Action name dynamically:

[Route("api/{lang}/[controller]/[Action]")]
    [ApiController]
    public class SpatiaController : ControllerBase
    {
        private readonly ISpatialService _service;
        private readonly ILogger<SpatialController> _logger;

        public SpatialUnitsIGMEController(ILogger<SpatialController> logger, ISpatialService service) 
        {
            _service = service;
            _logger = logger;
        }


        [HttpGet]
        public async Task<ActionResult<IQueryable<ItemDto>>> Get50k(string lang)
        {
            var result = await _service.GetAll50k(lang);
            return Ok(result);
        }

        [HttpGet("{name}")]
        public async Task<ActionResult<ItemDto>> Get50k(string lang, string name)
        {
            var result = await _service.Get50k(lang, name);
            return Ok(result);
        }
    }

To call these endpoints the following calls would be used:

https://example.domain.com/api/en/spatial/get50k --> we get all the data in English

https://example.domain.com/api/en/spatial/get50k/madrid --> we obtain the data of madrid in english

https://example.domain.com/api/es/spatial/get50k/madrid --> we obtain the data of madrid in spanish

(using the action and language in the route)

Related