ASP .NET Core Change default ApiController BadResponse - c#

I am using ASP.NET Core 5.0 and I have Users Controller with Register method, which receives UserRegisterInputModel. The problem is that all responses from my API are in specific format, but the ApiController auto-validates the input model and returns BadResponse in another format.
This is my abstract response model
public abstract class ResponseModel
{
public ResponseModel(bool successfull, int statusCode)
{
this.Successfull = successfull;
this.StatusCode = statusCode;
this.ErrorMessages = new List<string>();
}
public bool Successfull { get; set; }
public int StatusCode { get; set; }
public List<string> ErrorMessages { get; set; }
public object Data { get; set; }
}
And this is my BadResponseModel
public class BadResponseModel : ResponseModel
{
public BadResponseModel()
: base(false, 400)
{
}
}
This is part of my Register method in Users Controller.
[HttpPost]
public async Task<IActionResult> Register(UserRegisterInputModel input)
{
if (!ModelState.IsValid)
{
return Json(new BadResponseModel()
{
ErrorMessages = new List<string>()
{
"Invalid register information"
}
});
}
ApiController functionality auto-validates my model and the return statement for BadResponseModel is never reached. Is there any way of stopping auto-validation or changing the default response from ApiController validation ?

You can disable automatic model state validation by using "ApiBehaviourOptions.SuppressModelStateInvalidFilter" property:
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.apibehavioroptions.suppressmodelstateinvalidfilter?view=aspnetcore-5.0
So a very basic example of usage would be like that (ConfigureServices method in Startup.cs):
services.Configure<ApiBehaviorOptions>(opt => { opt.SuppressModelStateInvalidFilter = true; });

Related

ASP.NET - API Url to List of Model Objects

Currently I am doing an API call via jQuery, my question is, is there away to do this call in C# or away to convert the results of an API call to an ASP.NET List of Model Objects?
Here is my Model
public class TeamStatsClass
{
public int id { get; set; }
public string name { get; set; }
public string league { get; set; }
public string division { get; set; }
}
And here is my current ajax call
$.ajax({
url: "https://statsapi.web.nhl.com/api/v1/teams?sportId=1",
success: function (data) {
for (var team of data.teams) {
console.log(team.name);
}
}
});
UPDATE
I changed my classes to look like so:
public class StatsTeamsClass
{
public IEnumerable<Teams> teams { get; set; }
public string copyright { get; set; }
}
public class Division
{
public int id { get; set; }
public string name { get; set; }
public string link { get; set; }
}
public class Teams
{
public int id { get; set; }
public string name { get; set; }
public string link { get; set; }
public League league { get; set; }
public Division division { get; set; }
}
and created this method which indeeds puts the results in model object:
public async System.Threading.Tasks.Task<StatsTeamsClass> GetTeams()
{
HttpClient Http = new HttpClient();
var json = await Http.GetStringAsync("https://statsapi.web.nhl.com/api/v1/teams?sportId=1");
StatsTeamsClass teams = JsonConvert.DeserializeObject<StatsTeamsClass>(json);
return teams;
}
But when I try to call this method in another controller, it just hangs there, no error, no nothing, I am assuming it will just time out after a while
public class HomeController : Controller
{
APIController webService = new APIController();
public ActionResult Index()
{
var item = webService.GetTeams().Result.teams;
return View();
}
}
(GetTeams() is inside the controller APIController)
So what would be the proper way to A. get the results of an API in object model and then call those results?
The controller action needs to be made async as well to avoid mixing async-await and blocking calls like .Result or .Wait() that could potentially cause deadlocks.
Reference Async/Await - Best Practices in Asynchronous Programming
public class HomeController : Controller {
APIController webService = new APIController();
public async Task<ActionResult> Index() {
var model = await webService.GetTeams();
var teams = model.teams;
return View();
}
}
Assuming APIController is an actual ApiContoller
public class APIController : ApiController {
//Your original code
public async Task<StatsTeamsClass> GetTeams() {
HttpClient Http = new HttpClient();
var json = await Http.GetStringAsync("https://statsapi.web.nhl.com/api/v1/teams?sportId=1");
StatsTeamsClass teams = JsonConvert.DeserializeObject<StatsTeamsClass>(json);
return teams;
}
//...
}
I would suggest not calling APIController directly like that from the HomeController and instead extract the GetTeams() method out into a reusable service
public class WebService {
static Lazy<HttpClient> http = new Lazy<HttpClient>();
public async Task<T> GetAsync<T>(string url) {
var json = await http.Value.GetStringAsync(url);
return JsonConvert.DeserializeObject<T>(json);
}
public Task<StatsTeamsClass> GetTeamsAsync() {
var url = "https://statsapi.web.nhl.com/api/v1/teams?sportId=1";
return GetAsync<StatsTeamsClass>(url);
}
}
Reference You're using HttpClient wrong
that can be properly used in HomeController
public class HomeController : Controller {
public async Task<ActionResult> Index() {
// Ideally web service should be injected but that topic
// is outside of the scope of the question at the moment.
var webService = new WebService();
var model = await webService.GetTeamsAsync();
var teams = model.teams;
//...
return View(teams);
}
}
The assumption here is that the project is a mixed Asp.Net MVC and Web Api 2+
Index.cshtml
#model IEnumerable<Teams>
#{
ViewBag.Title = "Teams";
}
#if(Model != null && Model.Count() > 0) {
#foreach (var #team in Model) {
<p>#team.name</p>
}
}
Yes, the equivalent in C# would be to use HttpClient. You're best off creating a static instance of the class that you reuse for a particular kind of repeated call:
private static readonly HttpClient Http = new HttpClient();
and then used it from an async method using Newtonsoft.Json like this:
var json = await Http.GetStringAsync("https://statsapi.web.nhl.com/api/v1/teams?sportId=1");
You can then parse this string of JSON into a model class like this:
var model = JsonConvert.DeserializeObject<TeamStatsClass>(json);
As the question is answered by #Daniel above just want to add couple of more points here The json you are getting cannot be directly casted to TeamStatsClass you might have to introduce another base class as teams is the collection in the json you are getting.
Im posting it here to get a clearer view
public class ResponseBaseClass
{
public IEnumerable<TeamStatsClass> teams { get; set; }
public string copyright { get; set; }
}
public class TeamStatsClass
{
public int id { get; set; }
public string name { get; set; }
public Division division { get; set; }
}
public class Division
{
public int id { get; set; }
public string name { get; set; }
public string nameShort { get; set; }
public string link { get; set; }
}
HttpClient Http = new HttpClient();
var json = await Http.GetStringAsync("https://statsapi.web.nhl.com/api/v1/teams?sportId=1");
var model = JsonConvert.DeserializeObject<ResponseBaseClass>(json);
var yourTeamModelObj = model.teams;

Asp.net core not binding post model if invalid property values are present

I am migrating an application from legacy asp.net webapi to asp.net core mvc. I have noticed an issue. For some requests, we send partial or even invalid values in the POST body. And asp.net core is refusing to deserialize it.
E.g. post model
public class PostModel
{
public int Id { get; set; }
public Category? Category { get; set; }
}
public enum Category
{
Public,
Personal
}
action
[HttpPost]
public async Task<Response> Post([FromBody]PostModel model)
=> this.Service.Execute(model);
for the following sample request
POST /endpoint
{
id: 3,
category: "all"
}
The ModelState collection records an error - indicating that all is an invalid category, and the PostModel argument model is null. Is it possible to disable this behaviour and just attempt to bind all properties that are possible from the post body, and ignoring the ones it can't bind? This is how it was done for us in our legacy api and for now, I need to port this across.
Disabling the model validation did not help for us. The model argument is still null.
For FromBody, it will bind the request body to Model by JsonInputFormatter.
For JsonInputFormatter, it will call return InputFormatterResult.Success(model) when there is no error, and call return InputFormatterResult.Failure(); when there is any error. For return InputFormatterResult.Failure();, it will not bind the valid property.
For a solution, you could implement custom formatter to return return InputFormatterResult.Success(model).
Implement custom formatter CustomFormatter based on JsonInputFormatter.
Replace InputFormatterResult.Failure() with InputFormatterResult.Success(model).
if (!(exception is JsonException || exception is OverflowException))
{
var exceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception);
exceptionDispatchInfo.Throw();
}
return InputFormatterResult.Success(model);
Inject CustomFormatter in Startup.cs
services.AddMvc(o =>
{
var serviceProvider = services.BuildServiceProvider();
var customJsonInputFormatter = new CustomFormatter(
serviceProvider.GetRequiredService<ILoggerFactory>().CreateLogger<CustomFormatter>(),
serviceProvider.GetRequiredService<IOptions<MvcJsonOptions>>().Value.SerializerSettings,
serviceProvider.GetRequiredService<ArrayPool<char>>(),
serviceProvider.GetRequiredService<ObjectPoolProvider>(),
o,
serviceProvider.GetRequiredService<IOptions<MvcJsonOptions>>().Value
);
o.InputFormatters.Insert(0, customJsonInputFormatter);
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
Actually, your problem is related to Data Binding, not to validation, that's why disabling the model validation did not help. You can implement custom Binder and configure it to manually bind your properties, e.g.:
public class PostModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
string valueFromBody = string.Empty;
using (var sr = new StreamReader(bindingContext.HttpContext.Request.Body))
{
valueFromBody = sr.ReadToEnd();
}
if (string.IsNullOrEmpty(valueFromBody))
{
return Task.CompletedTask;
}
string idString = Convert.ToString(((JValue)JObject.Parse(valueFromBody)["id"]).Value);
string categoryString = Convert.ToString(((JValue)JObject.Parse(valueFromBody)["category"]).Value);
if (string.IsNullOrEmpty(idString) || !int.TryParse(idString, out int id))
{
return Task.CompletedTask;
}
Category? category = null;
if(Enum.TryParse(categoryString, out Category parsedCategory))
{
category = parsedCategory;
}
bindingContext.Result = ModelBindingResult.Success(new PostModel()
{
Id = id,
Category = category
});
return Task.CompletedTask;
}
}
Then you can apply this binder to your class:
[ModelBinder(BinderType = typeof(PostModelBinder))]
public class PostModel
{
public int Id { get; set; }
public Category? Category { get; set; }
}
or to action:
[HttpPost]
public async Task<Response> Post([ModelBinder(BinderType = typeof(PostModelBinder))][FromBody]PostModel model)
=> this.Service.Execute(model);
or create CustomModelBinderProvider:
public class CustomModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(PostModel))
return new PostModelBinder();
return null;
}
}
and register it in ConfigureServices methods of Startup class:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddMvc(
config => config.ModelBinderProviders.Insert(0, new CustomModelBinderProvider())
);
...
}
No you can't since the property tied to an enum. if you really want to be what you posted then change the model to be
public class PostModel
{
public int Id { get; set; }
public string Category { get; set; }
}
Then in your endpoint parse the string to enum like
Enum.TryParse("All", out Category cat);

HttpPost Action Method In WebAPI Controller Not Working In .NETCore Project

I'm trying to insert into a DB using WebAPI for a .Net Core project but it's not working -
[Route("api/IMTWebAPI")]
public class IMTWebAPIController : BaseController
{
[HttpPost("Create")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([FromBody] InterMemberTransfer interMemberTransfer)
{
var testController = new CRUDForIMT(_context);
var response = await testController.Create(interMemberTransfer);
return Json(response);
}
}
CRUDForIMT -
private readonly ARMStocktradeV2Context _context;
public CRUDForIMT(ARMStocktradeV2Context context)
{
_context = context;
}
public async Task<int> Create([Bind("Id,Date,EmailAddress,PhoneNumber,ResidentBroker")] InterMemberTransfer interMemberTransfer)
{
_context.Add(interMemberTransfer);
var res = await _context.SaveChangesAsync();
return res;
}
Anytime I test in postman, i get a 400 bad request and no response.
My Model -
public partial class InterMemberTransfer
{
public long Id { get; set; }
public DateTime Date { get; set; }
public string EmailAddress { get; set; }
public long PhoneNumber { get; set; }
public string ResidentBroker { get; set; }
}
I added a breakpoint in the Create action method but it's not even getting to the breakpoint.
Remove [ValidateAntiForgeryToken] from your Create Method.
Otherwise you'll never get into this Method at all. At least not with Postman.

ASP.NET Core MVC Mixed Route/FromBody Model Binding & Validation

I am using ASP.NET Core 1.1 MVC to build an JSON API. Given the following model and action method:
public class TestModel
{
public int Id { get; set; }
[Range(100, 999)]
public int RootId { get; set; }
[Required, MaxLength(200)]
public string Name { get; set; }
public string Description { get; set; }
}
[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho([FromBody] TestModel data)
{
return Json(new
{
data.Id,
data.RootId,
data.Name,
data.Description,
Errors = ModelState.IsValid ? null : ModelState.SelectMany(x => x.Value.Errors)
});
}
The [FromBody] on my action method parameter is causing the model to be bound from the JSON payload that is posted to the endpoint, however it also prevents the Id and RootId properties from being bound via the route parameters.
I could break this up into to separate models, one bound from the route and one from the body or I could also force any clients to send the id & rootId as part of the payload, but both of those solutions seem to complicate things more than I'd like and don't allow me to keep the validation logic in a single place. Is there any way to get this situation working where the model can be bound properly and I can keep my model & validation logic together?
After researching I came up with a solution of creating new model binder + binding source + attribute which combines functionality of BodyModelBinder and ComplexTypeModelBinder. It firstly uses BodyModelBinder to read from body and then ComplexModelBinder fills other fields. Code here:
public class BodyAndRouteBindingSource : BindingSource
{
public static readonly BindingSource BodyAndRoute = new BodyAndRouteBindingSource(
"BodyAndRoute",
"BodyAndRoute",
true,
true
);
public BodyAndRouteBindingSource(string id, string displayName, bool isGreedy, bool isFromRequest) : base(id, displayName, isGreedy, isFromRequest)
{
}
public override bool CanAcceptDataFrom(BindingSource bindingSource)
{
return bindingSource == Body || bindingSource == this;
}
}
[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class FromBodyAndRouteAttribute : Attribute, IBindingSourceMetadata
{
public BindingSource BindingSource => BodyAndRouteBindingSource.BodyAndRoute;
}
public class BodyAndRouteModelBinder : IModelBinder
{
private readonly IModelBinder _bodyBinder;
private readonly IModelBinder _complexBinder;
public BodyAndRouteModelBinder(IModelBinder bodyBinder, IModelBinder complexBinder)
{
_bodyBinder = bodyBinder;
_complexBinder = complexBinder;
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
await _bodyBinder.BindModelAsync(bindingContext);
if (bindingContext.Result.IsModelSet)
{
bindingContext.Model = bindingContext.Result.Model;
}
await _complexBinder.BindModelAsync(bindingContext);
}
}
public class BodyAndRouteModelBinderProvider : IModelBinderProvider
{
private BodyModelBinderProvider _bodyModelBinderProvider;
private ComplexTypeModelBinderProvider _complexTypeModelBinderProvider;
public BodyAndRouteModelBinderProvider(BodyModelBinderProvider bodyModelBinderProvider, ComplexTypeModelBinderProvider complexTypeModelBinderProvider)
{
_bodyModelBinderProvider = bodyModelBinderProvider;
_complexTypeModelBinderProvider = complexTypeModelBinderProvider;
}
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
var bodyBinder = _bodyModelBinderProvider.GetBinder(context);
var complexBinder = _complexTypeModelBinderProvider.GetBinder(context);
if (context.BindingInfo.BindingSource != null
&& context.BindingInfo.BindingSource.CanAcceptDataFrom(BodyAndRouteBindingSource.BodyAndRoute))
{
return new BodyAndRouteModelBinder(bodyBinder, complexBinder);
}
else
{
return null;
}
}
}
public static class BodyAndRouteModelBinderProviderSetup
{
public static void InsertBodyAndRouteBinding(this IList<IModelBinderProvider> providers)
{
var bodyProvider = providers.Single(provider => provider.GetType() == typeof(BodyModelBinderProvider)) as BodyModelBinderProvider;
var complexProvider = providers.Single(provider => provider.GetType() == typeof(ComplexTypeModelBinderProvider)) as ComplexTypeModelBinderProvider;
var bodyAndRouteProvider = new BodyAndRouteModelBinderProvider(bodyProvider, complexProvider);
providers.Insert(0, bodyAndRouteProvider);
}
}
Install-Package HybridModelBinding
Add to Statrup:
services.AddMvc()
.AddHybridModelBinder();
Model:
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
public string FavoriteColor { get; set; }
}
Controller:
[HttpPost]
[Route("people/{id}")]
public IActionResult Post([FromHybrid]Person model)
{ }
Request:
curl -X POST -H "Accept: application/json" -H "Content-Type:application/json" -d '{
"id": 999,
"name": "Bill Boga",
"favoriteColor": "Blue"
}' "https://localhost/people/123?name=William%20Boga"
Result:
{
"Id": 123,
"Name": "William Boga",
"FavoriteColor": "Blue"
}
There are other advanced features.
You can remove the [FromBody] decorator on your input and let MVC binding map the properties:
[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho(TestModel data)
{
return Json(new
{
data.Id,
data.RootId,
data.Name,
data.Description,
Errors = ModelState.IsValid ? null : ModelState.SelectMany(x => x.Value.Errors)
});
}
More info:
Model binding in ASP.NET Core MVC
UPDATE
Testing
UPDATE 2
#heavyd, you are right in that JSON data requires [FromBody] attribute to bind your model. So what I said above will work on form data but not with JSON data.
As alternative, you can create a custom model binder that binds the Id and RootId properties from the url, whilst it binds the rest of the properties from the request body.
public class TestModelBinder : IModelBinder
{
private BodyModelBinder defaultBinder;
public TestModelBinder(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory) // : base(formatters, readerFactory)
{
defaultBinder = new BodyModelBinder(formatters, readerFactory);
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
// callinng the default body binder
await defaultBinder.BindModelAsync(bindingContext);
if (bindingContext.Result.IsModelSet)
{
var data = bindingContext.Result.Model as TestModel;
if (data != null)
{
var value = bindingContext.ValueProvider.GetValue("Id").FirstValue;
int intValue = 0;
if (int.TryParse(value, out intValue))
{
// Override the Id property
data.Id = intValue;
}
value = bindingContext.ValueProvider.GetValue("RootId").FirstValue;
if (int.TryParse(value, out intValue))
{
// Override the RootId property
data.RootId = intValue;
}
bindingContext.Result = ModelBindingResult.Success(data);
}
}
}
}
Create a binder provider:
public class TestModelBinderProvider : IModelBinderProvider
{
private readonly IList<IInputFormatter> formatters;
private readonly IHttpRequestStreamReaderFactory readerFactory;
public TestModelBinderProvider(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory)
{
this.formatters = formatters;
this.readerFactory = readerFactory;
}
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(TestModel))
return new TestModelBinder(formatters, readerFactory);
return null;
}
}
And tell MVC to use it:
services.AddMvc()
.AddMvcOptions(options =>
{
IHttpRequestStreamReaderFactory readerFactory = services.BuildServiceProvider().GetRequiredService<IHttpRequestStreamReaderFactory>();
options.ModelBinderProviders.Insert(0, new TestModelBinderProvider(options.InputFormatters, readerFactory));
});
Then your controller has:
[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho(TestModel data)
{...}
Testing
You can add an Id and RootId to your JSON but they will be ignored as we are overwriting them in our model binder.
UPDATE 3
The above allows you to use your data model annotations for validating Id and RootId. But I think it may confuse other developers who would look at your API code. I would suggest to just simplify the API signature to accept a different model to use with [FromBody] and separate the other two properties that come from the uri.
[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho(int id, int rootId, [FromBody]TestModelNameAndAddress testModelNameAndAddress)
And you could just write a validator for all your input, like:
// This would return a list of tuples of property and error message.
var errors = validator.Validate(id, rootId, testModelNameAndAddress);
if (errors.Count() > 0)
{
foreach (var error in errors)
{
ModelState.AddModelError(error.Property, error.Message);
}
}
I have not tried this for your example but it should work as asp.net core support model binding like this.
You can create model like this.
public class TestModel
{
[FromRoute]
public int Id { get; set; }
[FromRoute]
[Range(100, 999)]
public int RootId { get; set; }
[FromBody]
[Required, MaxLength(200)]
public string Name { get; set; }
[FromBody]
public string Description { get; set; }
}
Update 1: Above will not work in case when stream is not rewindable. Mainly in your case when you post json data.
Custom Model binder is solution but if you still don't want to create that one and just want to manage with Model then you can create two Model.
public class TestModel
{
[FromRoute]
public int Id { get; set; }
[FromRoute]
[Range(100, 999)]
public int RootId { get; set; }
[FromBody]
public ChildModel OtherData { get; set; }
}
public class ChildModel
{
[Required, MaxLength(200)]
public string Name { get; set; }
public string Description { get; set; }
}
Note : This works perfectly with application/json binding as it is working bit differently then other content-type.
What I ended up doing (translated to your case) was:
Model
public class TestModel
{
public int Id { get; set; }
[Range(100, 999)]
public int RootId { get; set; }
[Required, MaxLength(200)]
public string Name { get; set; }
public string Description { get; set; }
}
Controller
[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho(int rootId, int id, TestModel data)
{
data.RootId = rootId;
data.Id = id;
return Json(new
{
data.Id,
data.RootId,
data.Name,
data.Description,
Errors = ModelState.IsValid ? null : ModelState.SelectMany(x => x.Value.Errors)
});
}
It might not be the same signature on the controller method. It may not look as elegant as only having the model in the signature. It was - however - easy, as it doesn't require any external packages to be downloaded and only requires small changes to your controller method (one extra line and declared parameter per added route parameter).

Asp.net Web Api. Post a complex model to Controller and received model incorrect

I have a problem with Post a complex model object from client to Web Api controller.
My model structure is:
public class PaymentModel
{
public Credit Crediter { get; set; }
}
public class Credit : ICredit
{
public int BankInformationId { get; set; }
}
public interface ICredit
{
int BankInformationId { get; set; }
}
public sealed class CrediterEmployee:Credit
{
public int EnployeeId { get; set; }
}
I tried to create a model to post to API controller:
var param = new PaymentModel
{
Crediter = new CrediterEmployee
{
BankInformationId = 4928,
EnployeeId = 7013
},
}
In API controller I received a model object, but for Crediter I cannot cast to CreditEmployee. It is null when I tried to cast.
How can I cast Crediter to the CreditEmployee?
Have you tried something like this? ... And have you tried to do the POST maybe with tool like POSTMAN? (to be sure you're not sending an empty body)
[HttpPost]
[ResponseType(typeof(Credit))]
public async Task<IHttpActionResult> Post([FromBody] PaymentModel payment)
{
try
{
if (payment == null || payment.Crediter == null) return BadRequest();
var response = new Credit
{
BankInformationId = payment.Crediter.BankInformationId
};
return Ok(response);
}
catch (Exception ex)
{
return InternalServerError(ex);
}
}

Categories

Resources