How do I return an HttpStatus code from API methods in my ASP.NET Core 1.0 if there's a problem?
If the method is supposed to return a particular object type, when I try return an Http status code, I get an error saying I can't convert my object to status code.
[HttpPost]
public async Task<SomeObject> Post([FromBody] inputData)
{
// I detect an error and want to return BadRequest HttpStatus
if(inputData == null)
return new HttpStatusCode(400);
// All is well, so return the object
return myObject;
}
Return an IActionResult from your controller action instead:
public async Task<IActionResult> Post([FromBody] InputData inputData)
{
if(inputData == null)
{
return new HttpStatusCodeResult((int) HttpStatusCode.BadRequest);
}
//...
return Ok(myObject);
}
If you instead want to remove such null checks from the controller you could define a custom attribute:
public class CheckModelForNullAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.ActionArguments.Any(k => k.Value == null))
{
context.Result = new BadRequestObjectResult("The model cannot be null");
}
}
}
This way we dont have to bother with the model being null in the action.
[HttpPost]
[CheckModelForNull]
public async Task<SomeObject> Post([FromBody]InputData inputData)
{
// My attribute protects me from null
// ...
return myObject;
}
Related
I have a .net core 3.1 WEB Api service where I use OData (V4)
I have an endpoint where every query through GET METHOD is working fine, and with the same endpoint I can use POST METHOD and insert new records to database.
The problem is, the DELETE and PATCH METHODs are not working (or maybe they are, but if I try to use update or delete I always get a 404 Not Found error). I try to call them ( DELETE, PATCH) from POSTMAN, but I get the same 404 error, however the methods are in the controller.
MyController:
[ApiController]
[Route("[controller]")]
public class UsersController : ODataController
{
[HttpGet]
[EnableQuery()]
public IEnumerable<User> Get()
{
return new Context().Userek;
}
[HttpPatch]
[EnableQuery]
public async Task<IActionResult> Patch([FromODataUri] int id, Delta<User> user)
{
var ctx = new Context();
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var entity = await ctx.Userek.FindAsync(id);
if (entity == null)
{
return NotFound();
}
user.Patch(entity);
try
{
await ctx.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (id != 23)
{
return NotFound();
}
else
{
throw;
}
}
return Updated(entity);
}
public async Task<IActionResult> Delete([FromODataUri] int id)
{
var ctx = new Context();
var user = await ctx.Userek.FindAsync(id);
if (user == null)
{
return NotFound();
}
ctx.Userek.Remove(user);
await ctx.SaveChangesAsync();
return StatusCode(404);
}
}
Thank you for any help!
i have an problem with call to api, i get ambiguousMatchException: The request matched multiple endpoints
or page not found.
this happen after i add testApi to my controller, before all works good.
why it's happen and how to fix it? i want to add more function to this controller.
thank's!!!!!!
my code:
namespace AutomationTool.Api
{
[Route("api/[controller]")]
[ApiController]
public class TestCasesController : ControllerBase
{
private readonly ApplicationDbContext _context;
public TestCasesController(ApplicationDbContext context)
{
_context = context;
}
[HttpGet("{id:int}")] -- > here the problem
public int testApi(int id)
{
return 1;
}
// GET: api/TestCases
[HttpGet]
public async Task<ActionResult<IEnumerable<TestCase>>> GetTestCases()
{
return await _context.TestCases.ToListAsync();
}
// GET: api/TestCases/5
[HttpGet("{id}")]
public async Task<ActionResult<TestCase>> GetTestCase(int id)
{
var testCase = await _context.TestCases.FindAsync(id);
if (testCase == null)
{
return NotFound();
}
return testCase;
}
}
}
we are writing some API which required sessionId in header and some other data in body.
Is it possible to have only one class automatically parsed partially from header and from body?
Something like:
[HttpGet("messages")]
[Produces("application/json")]
[Consumes("application/json")]
[Authorize(Policy = nameof(SessionHeaderKeyHandler))]
public async Task<ActionResult<MessageData>> GetPendingClockInMessages(PendingMessagesData pendingMessagesRequest)
{
some body...
}
with request class like:
public class PendingMessagesData
{
[FromHeader]
public string SessionId { get; set; }
[FromBody]
public string OrderBy { get; set; }
}
I know, it is possible to do this, but it means, that I have to pass SessionId into the other methods as a parameter, instead of pass only one object. And we would have to do that in every API call.
public async Task<ActionResult<MessageData>> GetPendingClockInMessages(
[FromHeader] string sessionId,
[FromBody] PendingMessagesData pendingMessagesRequest)
{
some body...
}
Thank you,
Jakub
we are writing some API which required sessionId in header and some other data in body. Is it possible to have only one class automatically parsed partially from header and from body
Your GetPendingClockInMessages is annotated with a [HttpGet("messages")]. However, a HTTP GET method has no body at all. Also, it can't consume application/json. Please change it to HttpPost("messages")
Typically, SessionId is not passed in header of Session: {SessionId} like other HTTP headers. Session are encrypted via IDataProtector. In other words, you can't get it by Request.Headers["SessionId"].
Apart from the above two facts, you can create a custom model binder to do that.
Since the Session doesn't come from header directly, let's create a custom [FromSession] attribute to replace your [FromHeader]
public class FromSessionAttribute : Attribute, IBindingSourceMetadata
{
public static readonly BindingSource Instance = new BindingSource("FromSession", "FromSession Binding Source", true, true);
public BindingSource BindingSource { get { return FromSessionAttribute.Instance; } }
}
And since you're consuming application/json, let's create a binder as below:
public class MyModelBinder : IModelBinder
{
private readonly JsonOptions jsonOptions;
public MyModelBinder(IOptions<JsonOptions> jsonOptions)
{
this.jsonOptions = jsonOptions.Value;
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
var type = bindingContext.ModelType;
var pis = type.GetProperties();
var result= Activator.CreateInstance(type);
var body= bindingContext.ActionContext.HttpContext.Request.Body;
var stream = new System.IO.StreamReader(body);
var json = await stream.ReadToEndAsync();
try{
result = JsonSerializer.Deserialize(json, type, this.jsonOptions.JsonSerializerOptions);
} catch(Exception){
// in case we want to pass string directly. if you don't need this feature, remove this branch
if(pis.Count()==2){
var prop = pis
.Where(pi => pi.PropertyType == typeof(string) )
.Where(pi => !pi.GetCustomAttributesData().Any(ca => ca.AttributeType == typeof(FromSessionAttribute)))
.FirstOrDefault();
if(prop != null){
prop.SetValue( result ,json.Trim('"'));
}
} else{
bindingContext.ModelState.AddModelError("", $"cannot deserialize from body");
return;
}
}
var sessionId = bindingContext.HttpContext.Session.Id;
if (string.IsNullOrEmpty(sessionId)) {
bindingContext.ModelState.AddModelError("sessionId", $"cannot get SessionId From Session");
return;
} else {
var props = pis.Where(pi => {
var attributes = pi.GetCustomAttributesData();
return attributes.Any( ca => ca.AttributeType == typeof(FromSessionAttribute));
});
foreach(var prop in props) {
prop.SetValue(result, sessionId);
}
bindingContext.Result = ModelBindingResult.Success(result);
}
}
}
How to use
Decorate the property with a FromSession to indicate that we want to get the property via HttpContext.Sessino.Id:
public class PendingMessagesData
{
[FromBody]
public string OrderBy { get; set; } // or a complex model: `public MySub Sub{ get; set; }`
[FromSession]
public string SessionId { get; set; }
}
Finally, add a modelbinder on the action method parameter:
[HttpPost("messages")]
[Produces("application/json")]
[Consumes("application/json")]
public async Task<ActionResult> GetPendingClockInMessages([ModelBinder(typeof(MyModelBinder))]PendingMessagesData pendingMessagesRequest)
{
return Json(pendingMessagesRequest);
}
Personally, I would prefer another way, i.e, creating a FromSessionBinderProvider so that I can implement this without too much effort. :
public class FromSessionDataModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var sessionId = bindingContext.HttpContext.Session.Id;
if (string.IsNullOrEmpty(sessionId)) {
bindingContext.ModelState.AddModelError(sessionId, $"cannot get SessionId From Session");
} else {
bindingContext.Result = ModelBindingResult.Success(sessionId);
}
return Task.CompletedTask;
}
}
public class FromSessionBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null) { throw new ArgumentNullException(nameof(context)); }
var hasFromSessionAttribute = context.BindingInfo?.BindingSource == FromSessionAttribute.Instance;
return hasFromSessionAttribute ?
new BinderTypeModelBinder(typeof(FromSessionDataModelBinder)) :
null;
}
}
(if you're able to remove the [ApiController] attribute, this way is more easier).
I have implemented custom binder:
public class CustomModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var modelName = bindingContext.ModelName;
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
var modelAsString = valueProviderResult.FirstValue;
if (!string.IsNullOrEmpty(modelAsString))
{
// custom login here
}
return Task.CompletedTask;
}
}
and corresponding model binder provider:
public class CustomModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(CustomModel))
{
return new BinderTypeModelBinder(typeof(CustomModelBinder));
}
return null;
}
}
and I have added custom binder provider to ModelBinderProviders collection:
services.AddMvc(options => options.ModelBinderProviders.Insert(0, new CustomModelBinderProvider()));
Everything works except one thing. Custom binder is being called but unfortunatelly bindingContext.ModelName is always empty and don't know why. As ModelName is empty I always get ValueProviderResult.None as value provider.
var modelName = bindingContext.ModelName; //ModelName is empty here
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName); // I will get ValueProviderResult.None on this line
I don't know what am I missing in terms of ModelName and ValueProvider.
UPDATE:
CustomModel is used in controller which inherites ApiController. Method signature in controller looks like this:
[HttpPost]
public async Task<IActionResult> UpsertModel(string Id, [FromBody] CustomModel model)
I am uploading files using ng-file-upload and having some abnormal problem as the HttpContext.Current is null when using the IAuthenticationFilter. While everything working correctly when I comment the authentication filter in WebApiConfig.
Controller to Test
[HttpPost]
public IHttpActionResult Upload()
{
var current = HttpContext.Current;
if (current == null)
{
return Content(HttpStatusCode.BadRequest, Logger.Error("HttpContext.Current is null"));
}
if (current.Request != null && current.Request.Files != null)
{
var file = current.Request.Files.Count > 0 ? current.Request.Files[0] : null;
if (file != null)
{
file.SaveAs(#"C:\Temp\test.csv");
}
}
return Content(HttpStatusCode.BadRequest, Logger.Error("Should not reach here"));
}
IAuthenticationFilter
public class KeyAuthentication : Attribute, IAuthenticationFilter
{
// we only want to apply our authentication filter once on a controller or action method so return false:
public bool AllowMultiple
{
get { return false; }
}
// Authenticate the user by apiKey
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
HttpRequestMessage request = context.Request;
string apiKey = ExtractApiKey(request);
bool IsValidCustomer = await ValidateKey(apiKey);
if (IsValidCustomer)
{
var currentPrincipal = new GenericPrincipal(new GenericIdentity(apiKey), null);
context.Principal = principal;
}
else
{
context.ErrorResult = new ErrorMessageResult("Missing API Key");
}
}
// We don't want to add challange as I am using keys authenticaiton
public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
return Task.FromResult(0);
}
}
Extract API Key
public static string ExtractApiKey(HttpRequestMessage request)
{
if (!request.Headers.TryGetValues("x-api-key", out IEnumerable<string> keys))
return string.Empty;
return keys.First();
}
The solution was to include "targetFramework=4.5" in the web.config as commented by #Alfredo and more details in https://stackoverflow.com/a/32338414/3973463