Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swagger error Ambiguous HTTP method for action Actions require explicit HttpMethod binding

I am trying to generate swagger docs for my API. Every time I navigate to the swagger docs I get this error:

Ambiguous HTTP method for action - [ControllerName]Controller.DownloadFile. Actions require an explicit HttpMethod binding for Swagger/OpenAPI 3.0

Here is the "offending" code it is complaining about in my controller:

    using Models;
using Utilities;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;


namespace Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class LibrarianController : ControllerBase
    {
        private IFileUploadUtilties fileUtilities;
        private IConfiguration config;
        private ApiContext context;
        private IFileRetrieval fileRetrieval;
        public LibrarianController(IFileUploadUtilties utilities, IConfiguration config, FinwiseApiContext context, IFileRetrieval fileRetrieval)
        {
            fileUtilities = utilities;
            this.config = config;
            this.context = context;
            this.fileRetrieval = fileRetrieval;
        }

       [HttpGet]
        public IActionResult Get()
        {
            return Ok("Ok");
        }

        // GET api/<LibrarianController>/5
        [HttpGet("/api/[controller]/{Id}")]
        public async Task<IActionResult> Get(int id)
        {
            try
            {
                return Ok(await fileRetrieval.GetFileForPartnerItemById(id));
            }
            catch(Exception ex)
            {
                return NotFound();
            }
            
        }
        [HttpGet ("/api/[controller]/[action]/{fileId}")]
        public async Task<IActionResult> DownloadFile(int fileId)
        {
            if (fileId == 0)
                return Content("File Id missing");

            var fileDownload = await fileRetrieval.GetFileForPartnerItemById(fileId);
            var contentType = await fileUtilities.GetFileType(Path.GetExtension(fileDownload.NewFileName));

            var path = Path.Combine(config.GetSection("StoredFilePath").Value, fileDownload.NewFileName);

            var memory = new MemoryStream();
            using (var stream = new FileStream(path, FileMode.Open))
            {
                await stream.CopyToAsync(memory);
            }
            memory.Position = 0;
            return File(memory, contentType, Path.GetFileName(path));
        }

        [HttpGet("api/[controller]/{PartnerId}/{ItemId}")]
        public async Task<List<FileUploads>> GetFiles(string PartnerId, string ItemId)
        {
            var getFiles =  await fileRetrieval.GetAllFilesForPartnerItem(PartnerId, ItemId);
            return getFiles;
        }
        
        
        // POST api/<LibrarianController>
        [HttpPost]
        public async Task<IActionResult> Post([FromForm] FileInformation fileInfo)
        {
            int newFileVersion = 1;

            if (fileInfo == null || fileInfo.Files == null || fileInfo.Files.Count == 0)
                return BadRequest("File(s) not found");

            try
            {
                foreach (var locFile in fileInfo.Files)
                {
                    //check for file extension, if not there, return an error
                    var fileExtension = Path.GetExtension(locFile.FileName);
                    if (string.IsNullOrEmpty(fileExtension))
                        return BadRequest("Files must include file extension");


                    var valid = await fileUtilities.IsFileValid(locFile);

                    var newFileName = string.Concat(Guid.NewGuid().ToString(),valid.fileExtension);

                    var newFileLocation = Path.Combine(config.GetSection("StoredFilePath").Value, newFileName);
                   

                    if (!valid.FileExtensionFound)
                    {
                        return BadRequest($"Error {valid.FileExtensionFoundError}");
                    }
                    if (!valid.FileSizeAllowed)
                    {
                        return BadRequest($"Error: {valid.FileSizeAllowedError}");
                    }


                    //check for an existing file in the database.  If there is one, increment the file version before the save
                    var currentFile = await fileUtilities.FileExists(fileInfo, locFile);

                    if (currentFile != null)
                    {
                        newFileVersion = currentFile.Version + 1;
                    }
              
                    //save to the file system
                    using (var stream = new FileStream(newFileLocation, FileMode.OpenOrCreate, FileAccess.ReadWrite))
                    {
                        await locFile.CopyToAsync(stream);
                    }

                    //save to the db.  Check to see if the file exists first.  If it does, do an insert, if not, return an error
                    if (System.IO.File.Exists(newFileLocation))
                    {
                        FileUploads upload = new FileUploads
                        {
                            EntityId = fileInfo.EntityId,
                            FileName = locFile.FileName,
                            ItemId = fileInfo.ItemId.ToString(),
                            NewFileName = newFileName,
                            ValidFile = true,
                            Version = newFileVersion
                        };
                        context.FileUploads.Add(upload);
                        context.SaveChanges();
                        //TODO: fire event the file has been saved provide Id key to find the record
                        //upload.Id;
                    }
                    else
                    {
                        return BadRequest("Error: File Could not be saved");
                    }

                }
            }
            catch (Exception ex)
            {
                return BadRequest("Failure to upload files.");
            }
            return Ok("File Uploaded");
        }

        // PUT api/<LibrarianController>/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody] string value)
        {
        }

        // DELETE api/<LibrarianController>/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}

This end point works fine when I test it. I set the route and it is decorated with the HTTP so I don't understand what it is complaining about. I looked around for a solution, but from what I could see it is stating there is a public method not decorated in the controller, however there are not undecorated methods in this controller. What is the problem here? If I remove the routing info from the HttpGet the method is not reachable, so I need to have both the Route and the HttpGet decorators to reach this method (unless I did that wrong too). How can I fix this?

like image 427
john Avatar asked Jan 24 '23 21:01

john


2 Answers

I solved my problem by including the [NonAction] signature in the header of functions that were not endpoints

Example

[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class RequestController : ControllerBase {

    [NonAction]
    public ObjectResult SetError( Exception e)
    {
        return StatusCode(500, e.Message);
    }
}
like image 156
César Augusto Avatar answered Jan 29 '23 08:01

César Augusto


Why Ambiguous

Here is your routing for DownloadFile():

[HttpGet ("/api/[controller]/[action]/{fileId}")]
public async Task<IActionResult> DownloadFile(int fileId)

And this is the one for GetFiles():

[HttpGet("api/[controller]/{PartnerId}/{ItemId}")]
public async Task<List<FileUploads>> GetFiles(string PartnerId, string ItemId)

Consider a GET request /api/Librarian/DownloadFile/62959061. This url fits BOTH actions:

  • For DownloadFile(), DownloadFile is [action] and 62959061 is fileId.
  • For GetFiles(), DownloadFile is PartnerId and 62959061 is ItemId. (.NET will treat 62959061 as a string when doing model binding.)

That's why you have the Ambiguous error.

Suggestions

Assign each action a unique name and a predictable route.

1. Method name duplication

Instead of having these:

public IActionResult Get() { /*...*/ }
public async Task<IActionResult> Get(int id) { /*...*/ }

Rename one of the method to avoid same method name:

public IActionResult Get() { /*...*/ }
public async Task<IActionResult> GetById(int id) { /*...*/ }

2. Defining a unique route

Form the controller you defined, I suggest you to use [RoutePrefix] at controller level and use [Route] or [Http{Method}] at action level. Here is the related discussion on what they are.

Defining the route using the same pattern can avoid creating ambiguous route accidentally. Below is my attempt to redefine the routings:

[RoutePrefix("api/[controller]")] // api/Librarian
[ApiController]
public class LibrarianController : ControllerBase
{
    [HttpGet("")] // GET api/Librarian/
    public IActionResult Get() { /*...*/ }

    [HttpGet("{Id}")] // GET api/Librarian/62959061
    public async Task<IActionResult> GetById(int id) { /*...*/ }

    [HttpGet("download/{fileId}")] // GET api/Librarian/download/62959061
    public async Task<IActionResult> DownloadFile(int fileId) { /*...*/ }

    [HttpGet("getfiles/{PartnerId}/{ItemId}")] // GET api/Librarian/getfiles/partnerid/itemid
    public async Task<List<FileUploads>> GetFiles(string PartnerId, string ItemId) { /*...*/ }

    [HttpPost("")] // POST api/Librarian/
    public async Task<IActionResult> Post([FromForm] FileInformation fileInfo) { /*...*/ }

    [HttpPut("{id}")] // PUT api/Librarian/62959061
    public void Put(int id, [FromBody] string value) { /*...*/ }

    [HttpDelete("{id}")] // DELETE api/Librarian/62959061
    public void Delete(int id) { /*...*/ }
}

By adding a path for DownloadFile() and GetFiles() the app should be able to identify the route correctly as the app knows all routes with /download must go to DownloadFile() and all routes with /getfiles must go to GetFiles().

like image 43
Bemn Avatar answered Jan 29 '23 07:01

Bemn