Model null when use Route attribute - c#

In my WebAPI I have model
public class ListRequest
{
public int Skip { get; set; } = 0;
public int Take { get; set; } = 30;
}
My action is
[HttpGet]
[Route("api/users")]
public IHttpActionResult Get([FromUri] ListRequest request) {
...
}
I need to have possibility to not pass any query parameters, then default values should be used. But, when I go to http://localhost:44514/api/users the request is null. If I remove [Route("api/users")] then request is not null and has default values for parameters.
How can I reach that behavior with Route attribute?

If you want to init model using Route attributes try
Route("api/users/{*pathvalue}")]

Create your method on post request basis. Get type always receive null value.
[HttpGet]
[Route("api/users")]
public IHttpActionResult Get([FromUri] ListRequest request) {
}
Change to
[HttpPost]
[Route("api/users")]
public IHttpActionResult Get([FromUri] ListRequest request) {
...
}
Because Model (Class) type parameter does not support get type request.
Hope it will help.

Use data annotation. For more information visit Default value in mvc model using data annotation
Change
public class ListRequest
{
public int Skip { get; set; } = 0;
public int Take { get; set; } = 30;
}
To
public class ListRequest
{
[DefaultValue(0)]
public int Skip { get; set; }
[DefaultValue(30)]
public int Take { get; set; }
}
It works without removing [Route("api/users")] and request will not be null.

Related

Bind complex object from query string in post request

I have a class for holding both raw and encrypted values from query parameters:
public class SID
{
[FromQuery(Name = "sidInt")]
public int RawValue { get; set; }
[FromQuery(Name = "sid")]
public string EncryptedValue { get; set; }
public static implicit operator int(SID model)
{
return model.RawValue;
}
}
Having this I can successfully use it as a parameter for get requests:
/// GET: /{controller}/index?sid=my-encrypted-string
[HttpGet]
public IActionResult Index(SID id){
int resourceId = id;
//rest of the action code
}
Only encrypted value is provided in query string, raw integer value is automatically decrypted and added to the query string in my custom middleware. Notice that thanks to named [FromQuery] attributes I can use any parameter name in the action.
So far so good. But now I want to use the same SID class as a property in model for post request:
public class MyPostModel {
[Required]
[FromQuery]
public SID Id { get; set; }
public string Name { get; set; } // posted in body
// rest of the properties
}
/// POST: /{controller}/index?sid=my-encrypted-string + other fields in the body
[HttpPost]
public IActionResult Index(MyPostModel model){
int resourceId = model.Id;//model.Id is null here
//rest of the action code
}
But unfortunately I cannot make it working, Id in the model is not bound. I feel like I'm missing something obvious here. Is it possible? Do I need a custom model binder for this(I hope not, I want to use SID class as a property for several models and for different actions)?
Of course I can add sid parameter to the post action and reassign it in the action body explicitly but it looks wrong and too verbose:
[HttpPost]
public IActionResult Index(SID id, MyPostModel model){
model.Id = id;//id is correctly populated but model.Id is not
//rest of the action code
}
I think that your problem is that parameter is called "sid" and property in MyPostModel is "Id". Aside from that, I don't see any reason why it would not work. Can you change your MyPostModel:
public class MyPostModel {
[Required]
[FromQuery(Name = "sid")]
public SID Id { get; set; }
public string Name { get; set; } // posted in body
// rest of the properties
}

Dto in GET request never appears null

I have a GET endpoint in Asp.Net Core 2.2 where I want to return all records by default and if any query provided then I am returning a search query response. Here my query Dto never comes null even no query param provided. I want to keep Dto null when no query parameter provided.
My request Dto-
public sealed class SearchQueryDto
{
public SearchQueryDto()
{
Limit = 10;
Offset = 0;
}
public string Query { get; set; }
public string PhoneNumber { get; set; }
public string ClassificationId { get; set; }
public int Offset { get; set; }
public int Limit { get; set; }
}
My Endpoint-
[HttpGet]
public async Task<IActionResult> GetAllRecords([FromQuery] [CanBeNull] SearchQueryDto request)
{
if (request != null) // always not null
{
return Ok(await _service.Search(request));
}
return Ok(await _service.GetAll());
}
Here I am expecting request to null when no query parameter provided but it always appears initialized.
The docs lie:
Route data and query string values are used only for simple types.
This is simply not true. See also Bind query parameters to a model in ASP.NET Core, it just works.
This part of the docs is true:
When model binding occurs, the class is instantiated using the public default constructor.
So, the model class will always be instantiated. Unless it's bound from the body and the body is empty, corrupt or of the wrong content-type, but that's another story.
In your case, simply check whether the model doesn't have any properties set:
public sealed class SearchQueryDto
{
private const int DefaultLimit = 10;
private const int DefaultOffset = 0;
public string Query { get; set; }
public string PhoneNumber { get; set; }
public string ClassificationId { get; set; }
public int Offset { get; set; } = DefaultOffset;
public int Limit { get; set; } = DefaultLimit;
public bool IsDefault
{
get
{
return string.IsNullOrEmpty(Query)
&& string.IsNullOrEmpty(PhoneNumber)
&& string.IsNullOrEmpty(ClassificationId)
&& Offset == DefaultOffset
&& Limit == DefaultLimit;
}
}
}
And then in your controller:
if (!request.IsDefault)
{
// ...
}
you can set default null
[HttpGet]
public async Task<IActionResult> GetAllRecords([FromQuery] SearchQueryDto request = null)
Although you do not pass query string,the model would bind the default value,so you could not get null value.
In such case,you could fix it like below:
[HttpGet]
public async Task<IActionResult> GetAllRecords([FromQuery]SearchQueryDto request)
{
var query = Request.QueryString.HasValue;
if (query)
{
return Ok(await _service.Search(request));
}
return Ok(await _service.GetAll());
}

Swagger showing incorrect query param

I have this controller and action method:
[ApiController]
[Route("api/[controller]")]
public class AppointmentController : ControllerBase
{
[Route("{provider}/AvailableSlots")]
[HttpGet]
public Task<AvailableSlotsResponse> GetAvailableSlots(Request<AvailableSlotsRequest> request)
{
return null;
}
}
Here's the model:
public class Request<T> where T : class
{
[FromRoute]
public string Provider { get; set; }
[FromQuery(Name = "")]
public T Model { get; set; }
}
public class AvailableSlotsRequest
{
//[FromQuery(Name = "Location")] //Would prefer not to have to use this
public string Location { get; set; }
}
I need to use Location as the query param name in the URL in order to hit the endpoint, as expected.
eg. http://localhost/api/Appointment/Company/AvailableSlots?Location=SYD
However, when I view the Swagger page, the parameter is called Model.Location which is confusing for consumers of my API:
I can use [FromQuery(Name = "Location")] to force Swagger to display Location, however this feels very redundant and duplicates the property name.
Here is my Swagger set up in ConfigureServices():
services.AddSwaggerDocument(document =>
{
document.PostProcess = d =>
{
d.Info.Version = Configuration["APIVersion"];
d.Info.Title = $"{Configuration["ApplicationName"]} {Configuration["DomainName"]} API";
};
});
How can I make Swagger display Location instead of Model.Location, without having to duplicate the word "Location" in the [FromQuery] attribute?
Add to the controller parameter the attribute [FromRoute]:
public Task<AvailableSlotsResponse> GetAvailableSlots([FromRoute]Request<AvailableSlotsRequest> request)
Remove the attribute FromQuery in the Model property and uncomment the attribute FromQuery from de Location Property.
Unfortunately I had to use [FromQuery(Name = "<PropertyName>")].
However I found a better way:
[ApiController]
[Route("api/[controller]")]
public class AppointmentController : ControllerBase
{
[Route("{provider}/AvailableSlots")]
[HttpGet]
public Task<AvailableSlotsResponse> GetAvailableSlots(AvailableSlotsRequest request)
{
return null;
}
}
public class Request
{
[FromRoute]
public string ProviderName { get; set; }
}
public class AvailableSlotsRequest : Request
{
[FromQuery]
public string Location { get; set; }
}
This also means the model can use any attribute, compared to my first attempt where the T Model was decorated with [FromQuery]

How to make GET request with a complex object?

I try to make GET request via WebApi with complex object.
Request is like this:
[HttpGet("{param1}/{param2}")]
public async Task<IActionResult> GetRequest(string param1, int param2, [FromBody] CustomObject[] obj)
{
throw new NotImplementException();
}
Where CustomObject is:
[DataContract]
public class CustomeObject
{
[DataMember]
public string Name { get; set; }
[DataMember]
public string Email { get; set; }
}
How do I compose a valid GET request?
[FromBody] CustomObject[] obj ... GET request has no message body and thus you should change it to FromUri.
Sure, take a look at Documentation
public class GeoPoint
{
public double Latitude { get; set; }
public double Longitude { get; set; }
}
public ValuesController : ApiController
{
public HttpResponseMessage Get([FromUri] GeoPoint location) { ... }
}
Request would be like below, essentially you pass the entire object data as query string
http://localhost/api/values/?Latitude=47.678558&Longitude=-122.130989
An array of object example can be found in another post pass array of an object to webapi
If your complex object is defined by the server, you can model bind to it through the URI and dot notate the properties in the routing template. My advice is to keep this model to one level of properties. You can bind to more complex objects, but you'll quickly find yourself having to write your own model binder.
Note that your argument decorator will need to be changed to [FromUri] to bind a complex object through the Uri. Servers are not required to support GET bodies and most do not.
public class CustomObject
{
public string Name { get; set; }
public string Email { get; set; }
}
[HttpGet]
[Route("{foo.Name}/{foo.Email}")]
public HttpResponseMessage Get([FromUri]CustomObject foo)
{
//...body
return Request.CreateResponse(HttpStatus.OK, foo);
}
You can pass it as a stringified json or use the request body via post instead of get.

Multiple actions were found that match the request: WebAPI 2

I've read a few SO posts and none of them quite cover my scenario so I'm going to post here.
Given the following route config registration:
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
and these controller actions in a controller that inherits from ApiController:
public GetDocumentsResponse Post([FromBody]GetDocumentsRequest request)
{
}
public FinishDocumentsResponse Post([FromBody] FinishDocumentsRequest request)
{
}
public class GetDocumentsRequest
{
public string CorrelationId { get; set; }
public int Id { get; set; }
public string ObjectId { get; set; }
public string BusinessArea { get; set; }
public string UserId { get; set; }
public string SystemName { get; set; }
public string SystemToken { get; set; }
public Letter LetterDetails { get; set; }
public List<KeyValuePair<string, string>> KeyValue { get; set; }
}
public class FinishDocumentsRequest
{
public string CorrelationId { get; set; }
public string[] Documents { get; set; }
}
I thought doing it this way would be enough disambiguation for the IHttpActionSelector to correctly choose the route, but unfortunately it is not.
So my questions is "Is there a way to make this code work correctly, and keep it in the same controller?"
Thank you,
Stephen
You could use attribute routing for this.
Define the route as a string in the Route attribute ontop of the methods as this
[Route("api/controller/Post1")]
[HttpPost]
public GetDocumentsResponse Post([FromBody]GetDocumentsRequest request)
{
}
[Route("api/controller/Post2")]
[HttpPost]
public FinishDocumentsResponse Post([FromBody] FinishDocumentsRequest request)
{
}
The request routing pipeline isn't smart enough to determine if the body of the request matches the parameter type (aka overloading). (The compiler is smart enough, which is why this compiles and you have runtime issues.)
You have a couple of different options.
You can either add an [Route(<ActionName>)] attribute on both of your posts.
Make two controllers, one for GetDocuments and one for FinishDocuments
Make one Post method that is ambiguous. (I'd avoid this)
If you choose option 1, your API uri will have to be .../api/MyController/MyActionName rather than .../api/MyController/. It's also advisable to add [HttpGet] and [HttpPost] attributes on your methods.
Sample:
public class DocumentController : ApiController
{
// POST /api/Document/GetDocuments
[HttpPost]
[Route("GetDocuments")]
public GetDocumentsResponse Post([FromBody]GetDocumentsRequest request) { ... }
// POST /api/Document/FinishDocuments
[HttpPost]
[Route("FinishDocuments")]
public FinishDocumentsResponse Post([FromBody] FinishDocumentsRequest request){ ...}
}
If you choose option 2, you have to maintain an additional code file.
public class GetDocumentsController : ApiController
{
// POST /api/GetDocuments
[HttpPost]
public GetDocumentsResponse Post([FromBody]GetDocumentsRequest request) { ... }
}
public class FinishDocumentsController : ApiController
{
// POST /api/FinishDocuments/
[HttpPost]
public FinishDocumentsResponse Post([FromBody] FinishDocumentsRequest request){ ...}
}
If you choose option 3, may God have mercy on your soul you're going to have a bad time maintaining it.
Add the Route attribute decoration to your web api functions and that will assit the selector to choose the route:
[Route("Post1")]
public GetDocumentsResponse Post([FromBody]GetDocumentsRequest request)
{
}
[Route("Post2")]
public FinishDocumentsResponse Post([FromBody] FinishDocumentsRequest request)
{
}
I also recommend adding the http method decoration such as [HttpPost] or [HttpGet]

Categories

Resources