Imagine we have asynchronous action defined in controller:
public class PortalController : AsyncController {
public void NewsAsync(string city) {
AsyncManager.OutstandingOperations.Increment();
NewsService newsService = new NewsService();
newsService.GetHeadlinesCompleted += (sender, e) =>
{
AsyncManager.Parameters["headlines"] = e.Value;
AsyncManager.OutstandingOperations.Decrement();
};
newsService.GetHeadlinesAsync(city);
}
public ActionResult NewsCompleted(string[] headlines) {
return View("News", new ViewStringModel
{
NewsHeadlines = headlines
});
}
When I call it from browser - everything works. But I want to call this action (News) from another synchronous controller. Is there any way to call action and get the result?
public class PortalController: Controller {
public ActionResult News(string city) {
NewsService newsService = new NewsService();
ViewStringModel headlines = newsService.GetHeadlines(city);
return View(headlines);
}
}
you should have looked at the rest of the code you got From MSDN you can't have a view without an ActionResult of some sort to get the data.
Related
I have a controller that has multiple routes.
I am trying to call an endpoint stated as
GET: api/lookupent/2020-03-17T13:28:37.627691
but this results in this error
Microsoft.AspNetCore.Routing.Matching.AmbiguousMatchException: The request matched multiple endpoints. Matches:
Controllers.RecordController.Get (API)
Controllers.RecordController.GetRecordRegisteredAt (API)
but I am not sure I understand why this makes sense since this code
// GET: api/{RecordName}/{id}
[HttpGet("{RecordName}/{id}", Name = "GetRecord")]
public ActionResult Get(string RecordName, long id)
// GET: api/{RecordName}/{timestamp}
[HttpGet("{RecordName}/{timestamp}", Name = "GetRecordRegisteredAt")]
public ActionResult GetRecordRegisteredAt(string RecordName, string timestamp)
why does the input match with these endpoints?
You can fix this using route constraints.
Take a look at https://learn.microsoft.com/en-us/aspnet/web-api/overview/web-api-routing-and-actions/attribute-routing-in-web-api-2
Here's their example:
[Route("users/{id:int}")]
public User GetUserById(int id) { ... }
[Route("users/{name}")]
public User GetUserByName(string name) { ... }
The problem you have is that your controller has the same routing for 2 different methods receiving different parameters.
Let me illustrate it with a similar example, you can have the 2 methods like this:
Get(string entityName, long id)
Get(string entityname, string timestamp)
So far this is valid, at least C# is not giving you an error because it is an overload of parameters. But with the controller, you have a problem, when aspnet receives the extra parameter it doesn't know where to redirect your request.
You can change the routing which is one solution.
This solution gives you the ability to map your input to a complex type as well, otherwise use Route constraint for simple types
Normally I prefer to keep the same names and wrap the parameters on a DtoClass, IntDto and StringDto for example
public class IntDto
{
public int i { get; set; }
}
public class StringDto
{
public string i { get; set; }
}
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
[HttpGet]
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
}
but still, you have the error. In order to bind your input to the specific type on your methods, I create a ModelBinder, for this scenario, it is below(see that I am trying to parse the parameter from the query string but I am using a discriminator header which is used normally for content negotiation between the client and the server(Content negotiation):
public class MyModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
dynamic model = null;
string contentType = bindingContext.HttpContext.Request.Headers.FirstOrDefault(x => x.Key == HeaderNames.Accept).Value;
var val = bindingContext.HttpContext.Request.QueryString.Value.Trim('?').Split('=')[1];
if (contentType == "application/myContentType.json")
{
model = new StringDto{i = val};
}
else model = new IntDto{ i = int.Parse(val)};
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
Then you need to create a ModelBinderProvider (see that if I am receiving trying to bind one of these types, then I use MyModelBinder)
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(IntDto) || context.Metadata.ModelType == typeof(StringDto))
return new MyModelBinder();
return null;
}
and register it into the container
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers(options =>
{
options.ModelBinderProviders.Insert(0, new MyModelBinderProvider());
});
}
So far you didn't resolve the issue you have but we are close. In order to hit the controller actions now, you need to pass a header type on the request: application/json or application/myContentType.json. But in order to support conditional logic to determine whether or not an associated action method is valid or not to be selected for a given request, you can create your own ActionConstraint. Basically the idea here is to decorate your ActionMethod with this attribute to restrict the user to hit that action if he doesn't pass the correct media type. See below the code and how to use it
[AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
public class RequestHeaderMatchesMediaTypeAttribute : Attribute, IActionConstraint
{
private readonly string[] _mediaTypes;
private readonly string _requestHeaderToMatch;
public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch,
string[] mediaTypes)
{
_requestHeaderToMatch = requestHeaderToMatch;
_mediaTypes = mediaTypes;
}
public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch,
string[] mediaTypes, int order)
{
_requestHeaderToMatch = requestHeaderToMatch;
_mediaTypes = mediaTypes;
Order = order;
}
public int Order { get; set; }
public bool Accept(ActionConstraintContext context)
{
var requestHeaders = context.RouteContext.HttpContext.Request.Headers;
if (!requestHeaders.ContainsKey(_requestHeaderToMatch))
{
return false;
}
// if one of the media types matches, return true
foreach (var mediaType in _mediaTypes)
{
var mediaTypeMatches = string.Equals(requestHeaders[_requestHeaderToMatch].ToString(),
mediaType, StringComparison.OrdinalIgnoreCase);
if (mediaTypeMatches)
{
return true;
}
}
return false;
}
}
Here is your final change:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
[RequestHeaderMatchesMediaTypeAttribute("Accept", new[] { "application/json" })]
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
[RequestHeaderMatchesMediaTypeAttribute("Accept", new[] { "application/myContentType.json" })]
[HttpGet]
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
}
Now the error is gone if you run your app. But how you pass the parameters?:
This one is going to hit this method:
public IActionResult Get(StringDto i)
{
return new JsonResult(i);
}
And this one the other one:
public IActionResult Get(IntDto a)
{
return new JsonResult(a);
}
Run it and let me know
I had the same issue for these two methods:
[HttpPost]
public async Task<IActionResult> PostFoos(IEnumerable<FooModelPostDTO> requests)
[HttpPost]
public async Task<IActionResult> GetFoos(GetRequestDTO request)
The first one is for getting entities (using Post) and the second one is for posting new entities in DB (again using Post).
One possible solution is to distinguish between them by their's method names (../[action]) with the Route attribute:
[Route("api/[controller]/[action]")]
[ApiController]
public class FoosController : ControllerBase
I'm trying to get a REST service up and running (I followed this tutorial), and was trying to extend it with a simple method to mark one of the ToDoItem as "Complete"; literally to pass an ID into a method which should mark it as "Complete".
However, I'm struggling to understand how the routing works.
This is the method provided by default, which works correctly via https://localhost:44388/api/values
If I add another GET operation, even with different [Route] attribute, then I end up with "AmbiguousActionException: Multiple actions matched"
[Route("api/values")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
I tried to specify a route prefix using the method below, so that I could add doesn't work; I get a 404 on https://localhost:44388/api/values and https://localhost:44388/api/values/getbyname
[RoutePrefix("api/values")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[Route("getbyname")]
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
I might be trying the wrong method, so I'm happy to take any advice. I just want to be able to create new REST calls and have them the appropriate actions. Do I need to create other controllers? Am I limited to one GET/POST/PUT etc per controller?
Edit: didn't provide enough info, here's more code:
[Route("api/ToDo")]
[ApiController]
public class ToDoController : ControllerBase
{
private readonly ToDoContext _context;
public ToDoController(ToDoContext toDoContext)
{
_context = toDoContext;
if (_context.ToDoItems.Count() == 0)
{
//collection is empty, so add a new item
ToDoItem item1 = new ToDoItem(1, "example 1");
ToDoItem item2 = new ToDoItem(2, "example 2");
_context.ToDoItems.Add(item1);
_context.ToDoItems.Add(item2);
_context.SaveChanges();
}
}
//GET: api/todo
[HttpGet]
public async Task<ActionResult<IEnumerable<ToDoItem>>> GetToDoItems()
{
return await _context.ToDoItems.ToListAsync();
}
//GET: api/todo/5
//[HttpGet(Name = "Get a ToDoItem")]
//[Route("get{id}")]
[HttpGet("{id}")]
public async Task<ActionResult<ToDoItem>> GetToDoItem(long id)
{
var todoitem = await _context.ToDoItems.FindAsync(id);
if (todoitem == null)
{
return NotFound();
}
return todoitem;
}
//POST: api/Todo
[HttpPost]
public async Task<ActionResult<ToDoItem>> PostToDoItem(ToDoItem todoItem)
{
_context.ToDoItems.Add(todoItem);
await _context.SaveChangesAsync();
//calls the "GetToDoItem" method above!
return CreatedAtAction("GetToDoItem", new { id = todoItem.ID }, todoItem);
}
//DELETE: api/todo/5
[HttpDelete("{id}")]
public async Task<ActionResult<ToDoItem>> DeleteToDoItem(long id)
{
var todoItem = await _context.ToDoItems.FindAsync(id);
if(todoItem == null)
{
return NotFound();
}
_context.ToDoItems.Remove(todoItem);
await _context.SaveChangesAsync();
return todoItem;
}
//* -. space to create a "MarkAsComplete" method
//GET: api/todo/5
[HttpGet(Name = "{name}")]
public async Task<ActionResult<ToDoItem>> MarkAsComplete(long id)
{
var todoitem = await _context.ToDoItems.FindAsync(id);
if (todoitem == null)
{
return NotFound();
}
else
{
todoitem.IsComplete = true;
}
return todoitem;
}
//*/
}
Mixing up different versions of the attributes. RoutePrefix is from a previous version.
Routes need to be unique per action to avoid route conflicts.
For example.
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase {
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get() {
return new string[] { "value1", "value2" };
}
// GET api/values/some_name
[HttpGet("{name}")]
public IActionResult GetByName(string name) {
return Ok();
}
}
Reference Routing to controller actions in ASP.NET Core
When building a REST API, it's rare that you will want to use [Route(...)] on an action method. It's better to use the more specific Http*Verb*Attributes to be precise about what your API supports. Clients of REST APIs are expected to know what paths and HTTP verbs map to specific logical operations.
Based on the additional details provided, that MarkAsComplete action should use HTTP PUT so signify that the model is being edited/updated.
For example
//* -. space to create a "MarkAsComplete" method
//PUT: api/todo/5
[HttpPut("{id:long}")]
public async Task<ActionResult<ToDoItem>> MarkAsComplete(long id) {
var todoitem = await _context.ToDoItems.FindAsync(id);
if (todoitem == null) {
return NotFound();
} else {
todoitem.IsComplete = true;
}
return todoitem;
}
//*/
I'm getting an array returned from an API call. The array looks something like this https://i.imgur.com/Rq8GfBI.png.
I get the array, then call my controller method using this
this.http.post<Maps[]>(this.baseUrl + "api/Map/InsertMap/", beatmaps[0]).subscribe();
Maps being an interface
interface Maps {
Id: number;
Name: string;
Artist: string;
Creator: string;
}
Now I just have a basic insert controller method
[Route("api/[controller]/InsertMap/")]
[HttpPost("[action]")]
public async Task<IActionResult> AsyncCreateMap(MapModel model)
{
await _mapService.AsyncInsert(model);
return Ok();
}
It takes in the Model as a parameter and then inserts it using Entity Framework. It doesn't work. I have no idea how to actually transfer the array I get to an object I can use in my controller.
Here is my whole controller class
[Route("api/[controller]")]
public class MapController : Controller
{
private readonly MapService _mapService;
public MapController(MapService mapService)
{
_mapService = mapService;
}
[Route("api/[controller]/Maps")]
[HttpGet("[action]")]
public async Task<IActionResult> AsyncMaps()
{
var data = await _mapService.AsyncGetMaps(0, 10);
return Ok(data);
}
[HttpPost]
public async Task<IActionResult> AsyncCreateMap([FromBody]MapModel model)
{
await _mapService.AsyncInsert(model);
return Ok();
}
}
you should try use [FromBody].
Example:
public class ModelDTO
{
public string Id{get; set;}
public List<string> Childs {get; set;}
}
[HttpPost]
[Route("api/nice/Save")]
public bool Save([FromBody] ModelDTO model)
{ ...
in the angular side, you should use httpClient.post..
save(data: IData): Observable<ISaveCompleted> {
const options = this.createPostOptions();
const saveCompleted = this.http
.post(options.url, data, options)
.map((res: Response) => <ISaveCompleted>res.json());
return saveCompleted;
}
I have two controllers:
public class AController : Controller
{
public ActionResult AControllerAction()
{
if (// BControllerAction reported an error somehow )
{
ModelState.AddModelError("error-key", "error-value");
}
...
}
}
public class BController : Controller
{
public ActionResult BControllerAction()
{
try{Something();}
catch(SomethingExceprion)
{
// here I need to add error info data,
// pass it to AController and redirect to
// AControllerAction where this error will be added
// to model state
}
}
}
I think I can do something like:
public ActionResult BControllerAction()
{
try{Something();}
catch(SomethingException)
{
var controller = new AController();
controller.ModelState.AddModelError("error-key", "error-value");
controller.AControllerAction();
}
}
But I suggest it will be architecture breaking approach, and I don't want to do like that. Is there some simpler and safer way, except passing model object?
Depending on what details of the exception you need to pass back to Controller A, I would do something along the lines of
public ActionResult BControllerAction()
{
try{Something();}
catch(SomethingException ex)
{
return RedirectToAction("AControllerAction", "AController", new { errorMessage = ex.Message() })
}
}
And then change the signature of the called method to
public ActionResult AControllerAction(string errorMessage)
{
if (!String.IsNullOrEmpty(errorMessage))
{
//do something with the message
}
...
}
You can return a redirect to AControllerAction. You can use the TempData dictionary (similar to ViewData) to share data across such a call (data stored this way will persist to the next request in the same session, as explained in this blog post).
Example:
public class AController : Controller
{
public ActionResult AControllerAction()
{
if (TempData["BError"] != null)
{
ModelState.AddModelError("error-key", "error-value");
}
...
}
}
public class BController : Controller
{
public ActionResult BControllerAction()
{
try{Something();}
catch(SomethingExceprion)
{
TempData["BError"] = true;
return RedircetToAction("AControllerAction", "AController");
}
}
}
I just want everyones feedback about the following Async Controller using the Web Api HttpClient. This looks very messy is there a way to make it cleaner? Does anyone have a good wrapper around chaining multiple async tasks together?
public class HomeController : AsyncController
{
public void IndexAsync()
{
var uri = "http://localhost:3018/service";
var httpClient = new HttpClient(uri);
AsyncManager.OutstandingOperations.Increment(2);
httpClient.GetAsync(uri).ContinueWith(r =>
{
r.Result.Content.ReadAsAsync<List<string>>().ContinueWith(b =>
{
AsyncManager.Parameters["items"] = b.Result;
AsyncManager.OutstandingOperations.Decrement();
});
AsyncManager.OutstandingOperations.Decrement();
});
}
public ActionResult IndexCompleted(List<string> items)
{
return View(items);
}
}
You seem to be using a bit to many async calls and AsyncManager.OutstandingOperations.Decrement(). The following code is enough to load the Flickr photo information asynchronously using YQL.
public class HomeController : AsyncController
{
public void IndexAsync()
{
var uri = "http://query.yahooapis.com/v1/public/yql?q=select%20*%20from%20flickr.photos.recent";
var httpClient = new HttpClient(uri);
AsyncManager.OutstandingOperations.Increment();
httpClient.GetAsync("").ContinueWith(r =>
{
var xml = XElement.Load(r.Result.Content.ContentReadStream);
var owners = from el in xml.Descendants("photo")
select (string)el.Attribute("owner");
AsyncManager.Parameters["owners"] = owners;
AsyncManager.OutstandingOperations.Decrement();
});
}
public ActionResult IndexCompleted(IEnumerable<string> owners)
{
return View(owners);
}
}
You may take a look at http://pfelix.wordpress.com/2011/08/05/wcf-web-api-handling-requests-asynchronously/.
It contains an example based on the task iterator technique ( http://blogs.msdn.com/b/pfxteam/archive/2009/06/30/9809774.aspx ) for chaining async operations.