Is there a better way to handle default query params using [FromUri]? - c#

I have the following endpoint which has two query parameters (both optional):
[HttpGet]
[Route("")]
[ResponseType(typeof(List<ResourceResponse>))]
public IHttpActionResult GetResources([FromUri]ResourceRequest request = null)
{
request = request ?? new ResourceRequest();
var resources = ResourceService.GetResources(request.SiteId, request.ServiceId);
return Ok(resources);
}
And here is my request object:
public class ResourceRequest
{
[DefaultValue(null)]
public int? ClubId { get; set; }
[DefaultValue(null)]
[FromClubId(nameof(ClubId))]
public int? SiteId { get; private set; }
[DefaultValue(null)]
public Guid? ServiceId { get; set; }
}
This code runs fine, but I need to include request = request ?? new ResourceRequest(); as the first line to account for the case where there are no query parameters submitted.
Is there a better way to handle the no-query-parameters scenario? Or is this as good as it gets?
I need to declare my query params as the ResourceRequest class so that I can run Validation Attributes on them.
I can't use new ResourceRequest() as my default either because it is not a compile-time constant.

You could just use safe navigation operator (introduced in c# 6) like this:
public IHttpActionResult GetResources([FromUri]ResourceRequest request = null)
{
var resources = ResourceService.GetResources(request?.SiteId, request?.ServiceId);
return Ok(resources);
}
The GetResources method will received null if the request is null.

Related

Can you use ModelBinding with a GET request?

I'm curious if it's possible to bind a query string that is passed in with a GET request to a Model.
For example, if the GET url was https://localhost:1234/Users/Get?age=30&status=created
Would it be possible on the GET action to bind the query parameters to a Model like the following:
[HttpGet]
public async Task<JsonResult> Get(UserFilter filter)
{
var age = filter.age;
var status = filter.status;
}
public class UserFilter
{
public int age { get; set; }
public string status { get; set; }
}
I am currently using ASP.NET MVC and I have done quite a bit of searching but the only things I can find are related to ASP.NET Web API. They suggest using the [FromUri] attribute but that is not available in MVC.
I just tested the this, and it does work (at least in .net core 3.1)
[HttpGet("test")]
public IActionResult TestException([FromQuery]Test test)
{
return Ok();
}
public class Test
{
public int Id { get; set; }
public string Yes { get; set; }
}
You can can create an ActionFilterAttribute where you will parse the query parameters, and bind them to a model. And then you can decorate your controller method with that attribute.
For example
public class UserFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
var controller = actionContext.ControllerContext.Controller as CustomApiController;
var queryParams = actionContext.Request.GetQueryNameValuePairs();
var ageParam = queryParams.SingleOrDefault(a => a.Key == "age");
var statusParam = queryParams.SingleOrDefault(a => a.Key == "status");
controller.UserFilter = new UserFilter {
Age = int.Parse(ageParam.Value),
Status = statusParam.Value
};
}
}
The CustomApiController (inherits from your current controller) and has a UserFilter property so you can keep the value there. You can also add null checks in case some of the query parameters are not sent with the request..
Finally you can decorate your controller method
[HttpGet]
[UserFilter]
public async Task<JsonResult> Get()
{
var age = UserFilter.age;
var status = UserFilter.status;
}

Custom error objects for .Net Core 3 web api

I am currently developing a web api in .NET Core 3. I currently have the following model for my error response object:
public class ErrorRo
{
public string Message { get; set; }
public int StatusCode { get; set; }
public string Endpoint { get; set; }
public string Parameters { get; set; }
public string IpAddress { get; set; }
}
This is a mandated response I need to implement, management has pushed this. It allows more verbose error messages for people hitting our API so that they know what went wrong.
At the moment I am currently populating this object manually in the methods themselves. Is there a way where I can overwrite the response methods. I.e. can I override the BadRequest of IActionResult to automatically populate these fields?
Thanks!
You can use result filters for this purpose. Add a filter which repalces result before sending it back
Model
public class CustomErroModel
{
public string Message { get; set; }
public int StatusCode { get; set; }
public string Endpoint { get; set; }
public string Parameters { get; set; }
public string IpAddress { get; set; }
}
Filter
public class BadRequestCustomErrorFilterAttribute : ResultFilterAttribute
{
public override void OnResultExecuting(ResultExecutingContext context)
{
//todo: check for BadRequestObjectResult if anything is returned for bad request
if (context.Result is BadRequestResult)
{
var result = new CustomErroModel
{
StatusCode = 200, //you status code
Endpoint = context.HttpContext.Request.GetDisplayUrl(),
Message = "some message",
IpAddress = context.HttpContext.Connection.RemoteIpAddress.ToString(), //find better implementation in case of proxy
//this returns only parameters that controller expects but not those are not defined in model
Parameters = string.Join(", ", context.ModelState.Select(v => $"{v.Key}={v.Value.AttemptedValue}"))
};
context.Result = new OkObjectResult(result); // or any other ObjectResult
}
}
}
Then apply filter per action or globally
[BadRequestCustomErrorFilter]
public IActionResult SomeAction(SomeModel model)
or
services
.AddMvc(options =>
{
options.Filters.Add<BadRequestCustomErrorFilterAttribute>();
//...
}
Well it depends on the scenario, but one possible approach could be to use a middleware using a similar strategy like the one described in this question, so that you complete the response with extra information.

How to get and post common data to each request made to the web api

I have created a simple webapi service with few get/post methods, these methods are having some input parameters that client is passing while making call to it, other than these parameter I have some common parameters that has to pass in each request made to the web api, currently I added in every web api method as input parameter that is passing by client along with other input parameters. I am looking for a way where I don'n need to add these common parameters on every webapi method, I want to get these common parameters commonly under webapi.
This is my sample api controller
public class MessageController : ApiController
{
//companyID is a common parameter that is required to pass every web api method
public IHttpActionResult GetMessage(string messageCode, int companyID)
{
Message msg = null;
MesssageManager msgManager = null;
try
{
if(string.IsNullOrEmpty(messageCode))
{
throw new Exception("Plase pass the messageCode in order to get the message.");
}
msgManager = new MesssageManager();
List<Message> messages = msgManager.GetMessages(companyID);
msg = messages.FirstOrDefault(o => o.Code.Equals(messageCode, StringComparison.InvariantCultureIgnoreCase));
return Ok(msg);
}
catch (Exception)
{
throw;
}
finally
{
msgManager = null;
}
}
public IHttpActionResult GetWarningMessage(string warningCode, int companyID)
{
//doing actual stuff to get the data
}
public IHttpActionResult GetMthod1(string param1, int companyID)
{
//doing actual stuff to get the data
}
public IHttpActionResult GetMthod2(string param1, int companyID)
{
//doing actual stuff to get the data
}
[HttpPost]
public IHttpActionResult SaveMessage(string message, int companyID)
{
//doing actual stuff to get the data
}
}
In above controller "companyID" is a common parameter that has to pass in each request.
Please suggest me implementation in web api to get the common parameters, and how to pass it from client using HttpClient.
If the companyID is some kind of indentification/authentication parameter you could add the companyId to the request headers. Implement an authenticationfilter and grab the companyId from the headers. However, you still need some kind of short term persisting mechanism (session, cache, scoped DI container etc.) where the authentication filter would store the parameter and the controller method would get the parameter from.
At the end you need to pass the parameter from the client to the server each time it is required. You need to figure out if it's less hassle to put it into the headers or pass it as a parameter to the method. If the companyId varies from request to request I'd add it to each method. If the companyId is "static" for at least a duration of a session then I'd put it into the headers and would try to make sure, that the client automatically adds the appropriate companyId to the request headers (i.e. like you would handle user tokens).
Please refer below line
https://www.codeproject.com/Tips/574576/How-to-implement-a-custom-IPrincipal-in-ASP-NET-MV
We can add additional attribute in CustomPrincipalSerializedModel like below
public interface ICustomPrincipal : System.Security.Principal.IPrincipal
{
string FirstName { get; set; }
string LastName { get; set; }
int CompanyId { get;set; }
}
public class CustomPrincipal : ICustomPrincipal
{
public IIdentity Identity { get; private set; }
public CustomPrincipal(string username)
{
this.Identity = new GenericIdentity(username);
}
public bool IsInRole(string role)
{
return Identity != null && Identity.IsAuthenticated &&
!string.IsNullOrWhiteSpace(role) && Roles.IsUserInRole(Identity.Name, role);
}
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName { get { return FirstName + " " + LastName; } }
public int CompanyId { get;set; }
}
public class CustomPrincipalSerializedModel
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public int CompanyId { get;set; }
}
https://www.codeproject.com/Tips/574576/How-to-implement-a-custom-IPrincipal-in-ASP-NET-MV
We can get from header or cookies or use Custom identity principals

Passing Complex Object to .netcore1.1 Webapi though Query string

I'm trying to pass a complex object though the query string but for some reason its not working. I have a complex object that looks like this:
public class QueryOptions
{
public QueryParameter[] Parameters = new QueryParameter[0];
}
And I've tried to sent it a few ways but nothing is working:
My webapi method looks like this:
[HttpGet]
[AllowAnonymous]
public async Task<TDTO[]> GetList([FromQuery] QueryOptions queryOptions)
{
return await this._service.GetList(queryOptions);
}
I've tried with and with out the FromQuery Attribute neither are working.
The url queries looks like this :
/api/users?Parameters[0].PropertyName=FirstName&Parameters[0].Value=GTitzy&Parameters[0].FilterCondition=0
I've also tried with the name of the object appened to the beginning. The request gets sent but the queryOptions always have no parameters.
How can I pass this complex object through the query string?
Assuming
public class QueryParameter {
public string PropertyName { get; set; }
public string Value { get; set; }
public string FilterCondition { get; set; }
}
You need to update your model to expose public properties for [FromQuery] to know what to bind to.
public class QueryOptions {
public QueryParameter[] Parameters { get; set; }
}
You should also consider reading Model Binding: Customize model binding behavior with attributes

Optional query string parameters in ASP.NET Web API

I need to implement the following WebAPI method:
/api/books?author=XXX&title=XXX&isbn=XXX&somethingelse=XXX&date=XXX
All of the query string parameters can be null. That is, the caller can specify from 0 to all of the 5 parameters.
In MVC4 beta I used to do the following:
public class BooksController : ApiController
{
// GET /api/books?author=tolk&title=lord&isbn=91&somethingelse=ABC&date=1970-01-01
public string GetFindBooks(string author, string title, string isbn, string somethingelse, DateTime? date)
{
// ...
}
}
MVC4 RC doesn't behave like this anymore. If I specify fewer than 5 parameters, it replies with a 404 saying:
No action was found on the controller 'Books' that matches the request.
What is the correct method signature to make it behave like it used to, without having to specify the optional parameter in the URL routing?
This issue has been fixed in the regular release of MVC4.
Now you can do:
public string GetFindBooks(string author="", string title="", string isbn="", string somethingelse="", DateTime? date= null)
{
// ...
}
and everything will work out of the box.
It's possible to pass multiple parameters as a single model as vijay suggested. This works for GET when you use the FromUri parameter attribute. This tells WebAPI to fill the model from the query parameters.
The result is a cleaner controller action with just a single parameter. For more information see: http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api
public class BooksController : ApiController
{
// GET /api/books?author=tolk&title=lord&isbn=91&somethingelse=ABC&date=1970-01-01
public string GetFindBooks([FromUri]BookQuery query)
{
// ...
}
}
public class BookQuery
{
public string Author { get; set; }
public string Title { get; set; }
public string ISBN { get; set; }
public string SomethingElse { get; set; }
public DateTime? Date { get; set; }
}
It even supports multiple parameters, as long as the properties don't conflict.
// GET /api/books?author=tolk&title=lord&isbn=91&somethingelse=ABC&date=1970-01-01
public string GetFindBooks([FromUri]BookQuery query, [FromUri]Paging paging)
{
// ...
}
public class Paging
{
public string Sort { get; set; }
public int Skip { get; set; }
public int Take { get; set; }
}
Update:
In order to ensure the values are optional make sure to use reference types or nullables (ex. int?) for the models properties.
Use initial default values for all parameters like below
public string GetFindBooks(string author="", string title="", string isbn="", string somethingelse="", DateTime? date= null)
{
// ...
}
if you want to pass multiple parameters then you can create model instead of passing multiple parameters.
in case you dont want to pass any parameter then you can skip as well in it, and your code will look neat and clean.
Default values cannot be supplied for parameters that are not declared 'optional'
Function GetFindBooks(id As Integer, ByVal pid As Integer, Optional sort As String = "DESC", Optional limit As Integer = 99)
In your WebApiConfig
config.Routes.MapHttpRoute( _
name:="books", _
routeTemplate:="api/{controller}/{action}/{id}/{pid}/{sort}/{limit}", _
defaults:=New With {.id = RouteParameter.Optional, .pid = RouteParameter.Optional, .sort = UrlParameter.Optional, .limit = UrlParameter.Optional} _
)

Categories