Given the following Web API controller action:
// GET api/values
public IEnumerable<string> Get()
{
return new string[] { "value1", "value2" };
}
Executing the following request is not failing, even when the parameter in the query string does not exist:
http://localhost:22297/api/values?someinvalidparameter=10
Is there a way to ensure that all parameters in the query string are valid parameters for the action that's being invoked?
You can write an action filter that validates that all the query parameters are there in the action parameters and throws if not.
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
namespace My.Namespace.Filters
{
/// <summary>
/// Action filter that checks that parameters passed in the query string
/// are only those that we specified in methods signatures.
/// Otherwise returns 404 Bad Request.
/// </summary>
public class ValidateQueryParametersAttribute : ActionFilterAttribute
{
/// <summary>
/// This method runs before every WS invocation
/// </summary>
/// <param name="actionContext"></param>
public override void OnActionExecuting(HttpActionContext actionContext)
{
//check that client does not use any invalid parameter
//but just those that are required by WS methods
var parameters = actionContext.ActionDescriptor.GetParameters();
var queryParameters = actionContext.Request.GetQueryNameValuePairs();
if (queryParameters.Select(kvp => kvp.Key).Any(queryParameter => !parameters.Any(p => p.ParameterName == queryParameter)))
{
actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.BadRequest);
}
}
}
}
In order for that to work with out of the box validation support nicely, I created my own action selector which makes it possible to bind URI parameters to complex type objects without duplication.
So, you can do the following with this action selector:
public class CarsByCategoryRequestCommand {
public int CategoryId { get; set; }
public int Page { get; set; }
[Range(1, 50)]
public int Take { get; set; }
}
public class CarsByColorRequestCommand {
public int ColorId { get; set; }
public int Page { get; set; }
[Range(1, 50)]
public int Take { get; set; }
}
[InvalidModelStateFilter]
public class CarsController : ApiController {
public string[] GetCarsByCategoryId(
[FromUri]CarsByCategoryRequestCommand cmd) {
return new[] {
"Car 1",
"Car 2",
"Car 3"
};
}
public string[] GetCarsByColorId(
[FromUri]CarsByColorRequestCommand cmd) {
return new[] {
"Car 1",
"Car 2"
};
}
}
Then, you can register an action filter to validate the user inputs to terminate request and return back a "400 Bad Request" response along with the validation error messages:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class InvalidModelStateFilterAttribute : ActionFilterAttribute {
public override void OnActionExecuting(HttpActionContext actionContext) {
if (!actionContext.ModelState.IsValid) {
actionContext.Response = actionContext.Request.CreateErrorResponse(
HttpStatusCode.BadRequest, actionContext.ModelState);
}
}
}
Check out the below posts for more information about this action selector and how you can get it:
Complex Type Action Parameters with ComplexTypeAwareActionSelector in ASP.NET Web API - Part 1
Complex Type Action Parameters with ComplexTypeAwareActionSelector in ASP.NET Web API - Part 2
For .net core web-api it will be a little different:
public class ValidateQueryParametersAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext actionContext)
{
var parameters = actionContext.ActionDescriptor.Parameters.ToList();
var queryParameters = actionContext.HttpContext.Request.Query.Keys.ToList();
if (queryParameters.Any(queryParameter => !parameters.Any(p => p.Name == queryParameter)))
{
actionContext.Result = new JsonResult(new { HttpStatusCode.BadRequest });
}
}
}
Related
This's my controller code:
[HttpGet]
[Route()]
public async Task<List<GetCategoryTreeOutputDto>> GetDatasetCategoryTreeAsync([FromUri(Name = "")] GetCategoryTreeInputDto input)
{
return await _category.GetDatasetCategoryTreeAsync(input);
}
public class GetCategoryTreeInputDto
{
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
public int? ParentId { get; set; }
}
and I get an error:
When I assign a parameter value,it works,So I have to give a value to use it?But these parameters don't always need to be assigned value.
i assigned value,it works
Json response:
{
"ID": 1,
"Value": 10,
"User": null
}
I need to change the User value in each response in OnActionExecuted method.
public override void OnActionExecuted(ActionExecutedContext context)
{
var response = context.Result;
}
But unable to read the Result and Update details.
You just need to do some casting, I think. This code needs some error handling but otherwise demonstrates what you want:
public class ViewModel
{
public int ID { get; set; }
public int Value { get; set; }
public string User { get; set; }
}
[Route("api/[controller]")]
public class ValuesController : Controller
{
// GET api/values
[HttpGet]
public IActionResult Get()
{
var vm = new ViewModel()
{
ID = 1,
Value = 10
};
return Ok(vm);
}
public override void OnActionExecuted(ActionExecutedContext context)
{
base.OnActionExecuted(context);
var result = context.Result as OkObjectResult;
var vm = result.Value as ViewModel;
vm.User = "ardalis";
}
}
The result:
{"id":1,"value":10,"user":"ardalis"}
What's the best way of ensuring that a property of a model can only be set by the ASP.NET WEB.API service? To a consumer of the service, that property is read-only.
For example:
public class MyModel
{
[Required]
public string CanBeSetByConsumer { get; set; }
// Can only be set by the service
public int Id { get; set; }
}
public class MyModelController : ApiController
{
public MyModel Get(int id)
{
// get MyModel by Id
return new MyModel();
}
public MyModel Post(MyModel myData)
{
// save myData to a store and generate an ID
// return myData with ID populated with a 201 Created
}
}
In the above example, the consumer of the API can POST:
{
"CanBeSetByConsumer" : "SomeValue"
}
The consumer can also GET:
{
"Id" : 1234,
"CanBeSetByConsumer" : "SomeValue"
}
What I would like to do is return a 400 BAD REQUEST if the client POSTs:
{
"Id" : 1234,
"CanBeSetByConsumer" : "SomeValue"
}
Here is one way to do it. Note that the POST model does not contain the Id property.
public class MyGetModel
{
[Required]
public string CanBeSetByConsumer { get; set; }
public int Id { get; set; }
}
public class MyPostModel
{
[Required]
public string CanBeSetByConsumer { get; set; }
}
public class MyModelController : ApiController
{
public MyGetModel Get(int id)
{
// get MyModel by Id
return new MyGetModel();
}
public MyGetModel Post(MyPostModel myData)
{
// save myData to a store and generate an ID
// return myGetData with ID populated with a 201 Created
}
}
Then if you have a lot of shared properties, you can have both of these inherit from an abstract class MyModel.
Another way to do it could be to add an action filter to the post action. In that action filter class, you would override the OnActionExecuting method, inspect the POST values collection for a value under the Id key, and set your 400 BAD REQUEST response there.
public class PreventIdValueAttribute
: System.Web.Http.Filters.ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// check request for id value, and if present,
// set the result to a 400 bad request HttpResponseMessage
}
}
[PreventIdValue]
public MyModel Post(MyModel myData)
{
// save myData to a store and generate an ID
// return myData with ID populated with a 201 Created
}
Note that with the second option, your MyModel instance will still have an Id value in the Post action, but its value will be zero.
I have the following two classes
public interface INotification
{
int Id { get; set; }
bool IsNotificationShown { get; set; }
DateTime NotificationDate { get; set; }
}
public class Notification : INotification
{
public int Id
{
get;
set;
}
public bool IsNotificationShown
{
get;
set;
}
public DateTime NotificationDate
{
get;
set;
}
}
The WebAPI Controller has the following Action, I have hardcoded values for ease of read
public List<INotification> Get()
{
INotification notif = new Notification { Id = 1, IsNotificationShown = false, NotificationDate = DateTime.Now, NotificationDescription = "Desc", NotificationFrom = "Abcd", NotificationTo = "Abcd", NotificationTypeId = 1 } ;
return new List<INotification> { notif };
}
When I run this on the browser I get the following error
The 'ObjectContent`1' type failed to serialize the response body for content type 'application/xml; charset=utf-8'.
As soon as I change the return type from INotification to Notification then it works fine. Does anyone have any suggestions on how to fix this?
Just add the attribute KnowType to your class. Like this:
[KnownType(typeof(Notification))]
public class Notification : INotification
{
...
I have a controller action that receives a complex object as a parameter, I need the OutputCache to vary by one of the properties of this complex object. Is this possible? How?
if you have a model like
public class person{
public string Name {get;set;}
public string location {get;set;}
}
and in the (strongly typed)view you have a form
#model Person
#Html.BeginForm(){
#Html.TextBoxFor(x=>x.Name)
#Html.TextBoxFor(x=>x.location)
}
and you submit the form to an ActionResult savePerson, with varying signature like
public ActionResult savePerson(Person p){
// p.Name
// p.location
}
or
public ActionResult savePerson(string Name, string location){
}
therefore i think if you annotate the ActionResult like
[OutputCache(Duration=3600, VaryByParam="Name")]
public ActionResult savePerson(Person p)
{
//
return View();
}
it will do for you, or if you have a complex model like
public class person{
public string Name {get;set;}
public Location loc {get;set;}
}
public class Location{
public string address
}
try
[OutputCache(Duration=3600, VaryByParam="Person.Location.address")]
public ActionResult savePerson(Person p)
{
//
return View();
}
I had the same requirement as above and came up with a slightly different approach
The class
/// <summary>
/// This class is used to encapsulate search filters for monitor graphs
/// </summary>
public class DatacarMonitorSearchCriteriaModel
{
public int? SynergyCode { get; set; }
[Required]
[DataType(DataType.Date)]
public DateTime StartDate { get; set; }
[Required]
[DataType(DataType.Date)]
public DateTime EndDate { get; set; }
/// <summary>
/// Filter to apply
/// </summary>
public IEnumerable<int> Countries { get; set; }
public DatacarMonitorSearchCriteriaModel()
{
Countries = new List<int>();
}
}
OutputCacheComplexAttribute
/// <summary>
/// <para>
/// An instance of this class mimic the behaviour of OutputCacheAttribute but for complex objects.
/// </para>
/// <para>
/// It allows to cache the output of any action that takes complex objects
/// </para>
/// </summary>
public class OutputCacheComplexAttribute : OutputCacheAttribute
{
private readonly Type[] _types;
private string _cachedKey;
/// <summary>
/// Initializes a new instance of the <see cref="OutputCacheComplexAttribute"/> class.
/// </summary>
/// <param name="types">Types that this attribute will lookup for in QueryString/Form data and store values in cache.</param>
/// <exception cref="System.ArgumentOutOfRangeException">type;type cannot be null</exception>
public OutputCacheComplexAttribute(params Type[] types)
{
if (types == null)
{
throw new ArgumentOutOfRangeException("type", "type cannot be null");
}
_types = types;
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
StringBuilder sbCachedKey = new StringBuilder();
if (filterContext.HttpContext.Request.Url != null)
{
string path = filterContext.HttpContext.Request.Url.PathAndQuery;
IDictionary<string, object> parameters = filterContext.ActionParameters;
//we need to compute a cache key which will be used to store the action output for later retrieval
//The cache key scheme is
// {url}:{key 1}:{value};[{key 2}:{value 2}[; ... {key n}:{value n}]];
// where :
// - url is the url of the action that will be executed
// - key n is the name of the n-th parameter
// - value n is the value of the n-th parameter as json string.
foreach (KeyValuePair<string, object> kv in parameters)
{
var kv1 = kv;
if (kv.Value != null && _types.AtLeastOnce(t => t.IsInstanceOfType(kv1.Value)))
{
sbCachedKey = sbCachedKey.AppendFormat("{0}:{1};",kv.Key,
JsonConvert.SerializeObject(kv.Value, Formatting.None, new JsonSerializerSettings()
{
NullValueHandling = NullValueHandling.Ignore,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
}));
}
}
_cachedKey = String.Format("{0}:{1}:{2}", GetType().Name, path, sbCachedKey.ToString());
}
if (!String.IsNullOrWhiteSpace(_cachedKey) && filterContext.HttpContext.Cache[_cachedKey] != null)
{
filterContext.Result = (ActionResult)filterContext.HttpContext.Cache[_cachedKey];
}
else
{
base.OnActionExecuting(filterContext);
}
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
if (!String.IsNullOrWhiteSpace(_cachedKey))
{
filterContext.HttpContext.Cache.Add(_cachedKey, filterContext.Result, null,
DateTime.UtcNow.AddSeconds(Duration), Cache.NoSlidingExpiration,
CacheItemPriority.Default, null);
}
base.OnActionExecuted(filterContext);
}
}
Attribute usage
[OutputCacheComplex(typeof(DatacarMonitorSearchCriteriaModel), Duration = OutputCacheDurationInSeconds, Location = OutputCacheLocation.Server)]
public async Task<JsonNetResult<DatacarMonitorDetailModel>> ReadMonitorDetailsJson([DataSourceRequest] DataSourceRequest request, DatacarMonitorSearchCriteriaModel criteria)
{
//some really complicated code here
}
with this new attribute, you can specify which type[s] to use for caching and the cache key will be computed based on values of each its properties.
For object, just that work fine:
[OutputCache(VaryByParam = "*", Duration = 60)]