Trouble sending a specific request with null parameters to an API - asp.net-web-api

This is an example of the controller I've built in .Net Core 2.1.
[Route("api/TestApi")]
public class TestApiController: Controller
{
[HttpGet("{param1?}/{param2?}/{param3?}")]
//[HttpGet]
public ActionResult Get(int? param1 = null, DateTime? param2 = null, bool? param3 = null)
{
//Get data ...
}
}
My issue with this set up is that I cannot call this function with a null parameter;
This kind of call will not work: http://localhost:9000/api/TestApi/null/null/true. It will give me an error stating that null is not a valid input for param1.
Now I cannot rely on reading parameters from a query string (company constraints)
but I would like to point out that it does work if I use [HttpGet] instead and use the url parameters.
Any ideas? suggestions? questions?
All would be welcome.
Thanks in advance.

I found sort of a patch to use.
It involves adding a middle ware in Startup.cs file.
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = false;
options.InvalidModelStateResponseFactory = actionContext =>
{
if (
!string.IsNullOrEmpty(actionContext.ActionDescriptor.AttributeRouteInfo.Template)
&& actionContext.ActionDescriptor.AttributeRouteInfo.Template.Contains("?"))
{
//Hard coded removed bad error if template has int?/double?/datetime?
return null;
}
else
{
var errors = actionContext.ModelState
.Where(e => e.Value.Errors.Count > 0)
.Select(e => new Error
{
Name = e.Key,
Message = e.Value.Errors.First().ErrorMessage
}).ToArray();
return new BadRequestObjectResult(errors);
}
};
});
I have managed to finally use the URL I specified in the question;
http://localhost:9000/api/TestApi/null/null/true
Again, it's not a solid solution, just a workaround.

Related

TempData not kept between postback

I need some advice on how to proceed with the mvc app I'm building. On my page I type out who is logged in to the page. This I first did by creating a base class where I created a user class which contained the users name and a image representing the user. Then I passed this class on to my views. But I also need to pass other models to my views depending on what view I'm in. Sure I could build a class that contain all different models I need to use on each page but there should be a easy way to pass name and image values across the pages and be persistant? I tried TempData together with TempData.Keep() but that was not persistant. How can I pass theses values between pages?
public ActionResult Validate(AccountModels.LoginModel model)
{
if (ModelState.IsValid)
{
if (Membership.ValidateUser(model.UserName, model.Password))
{
var mu = _repo.GetUser(Membership.GetUser().ProviderUserKey.ToString());
TempData["Name"] = mu.Name;
TempData["Image"] = mu.Image;
TempData.Keep();
FormsAuthentication.RedirectFromLoginPage(model.UserName, model.RememberMe);
}
}
return View("Index");
}
As #Jyoti said, you could use of Keep() method.
To make it easy to work with TempData, I wrote these methods in my BaseController, and I use it in every controller when I need to transfer data between actions or between view and controller.
protected TReturnType GetTempDataValue<TReturnType>(PsmConstants.TempDataKey sessionName, bool peekData =false )
{
object value = peekData ? TempData.Peek(sessionName.ToString()) : TempData[sessionName.ToString()];
return (TReturnType) value;
}
protected void RemoveTempData(PsmConstants.TempDataKey sessionName)
{
if (TempData.ContainsKey(sessionName.ToString()) && TempData[sessionName.ToString()] == null) return;
TempData[sessionName.ToString()] = null;
}
protected void SetTempDataValue(PsmConstants.TempDataKey sessionName, object value)
{
if(TempData.ContainsKey(sessionName.ToString()))
TempData[sessionName.ToString()]=null;
TempData[sessionName.ToString()] = value;
}
protected void KeepTempDataValue(PsmConstants.TempDataKey sessionName)
{
if (TempData.ContainsKey(sessionName.ToString()))
TempData.Keep(sessionName.ToString());
}
And this is the Keys enumeration :
public enum TempDataKey
{
PageError = 1,
PageInfo = 2
}
And this is, the usage of these methods(Set value and Get value from TempData):
SetTempDataValue(PsmConstants.TempDataKey.PageError , 'your error message' );
var originalValues = GetTempDataValue<MyModel>(PsmConstants.TempDataKey.Info, true);
Use session instead of Temp if it is not working.but i think it should work.
TempData["Name"] = mu.Name;TempData["Image"] = mu.Image;TempData.Keep();
How you are passing this into other models,Please share the source code so that it will easy to identify.

ASP.NET core HttpGet single Web API

Good Morning,
I’m having difficulty setting up my HTTPGETs and then testing the solution in Postman.
I’m trying to return a single result on both occasions however when I input the parameters nothing loads. So I'm clearly missing something which i need some help on please.
I have 1 parameter {id} in my CashMovementController and if I navigate to localhost/api/cashmovements/{id} it loads however if pass the {id} parameter in postman it fails.
Then in my BondCreditRatingsController I have 2 parameters {ISIN} & {Date} and again I'm not sure how to approach this.
Love to hear some advice/help on this please
Thanks GWS
Startup.cs
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
CashMovementsController.cs
[Route("api/[controller]")]
public class CashMovementsController : Controller
{
private ICashMovementRepository _cashmovementRepository;
[HttpGet("{id}", Name = "GetCashMovement")]
public IActionResult Get(int id)
{
CashMovement _cashmovement = _cashmovementRepository.GetSingle(u => u.CashMovementId == id);
if (_cashmovement != null)
{
CashMovementViewModel _cashmovementVM = Mapper.Map<CashMovement, CashMovementViewModel>(_cashmovement);
return new OkObjectResult(_cashmovementVM);
}
else
{
return NotFound();
}
}
}
BondCreditRatingsController.cs
[Route("api/[controller]")]
public class BondCreditRatingsController : Controller
{
private IBondCreditRatingRepository _bondcreditratingRepository;
public BondCreditRatingsController(IBondCreditRatingRepository bondcreditratingRepository)
{
_bondcreditratingRepository = bondcreditratingRepository;
}
[HttpGet("{id}", Name = "GetBondCreditRating")]
public IActionResult Get(string id, DateTime efffectivedate)
{
BondCreditRating _bondcreditrating = _bondcreditratingRepository.GetSingle(u => u.ISIN == id, u => u.EffectiveDate == efffectivedate);
if (_bondcreditrating != null)
{
BondCreditRatingViewModel _bondcreditratingVM = Mapper.Map<BondCreditRating, BondCreditRatingViewModel>(_bondcreditrating);
return new OkObjectResult(_bondcreditratingVM);
}
else
{
return NotFound();
}
}
If you want to map it to api/Controller/method/id you would need to use the code below because you want to map parameter order (no other identifier) to a specific parameter name in the action.
[HttpGet("GetCashMovement/{id}")]
Your current code should work with below since you are using named parameters and because the request can't be mapped to any other template.
/api/CashMovements/GetCashMovement?id=1
But that attribute syntax will also (possibly unintentionally) trigger:
/api/CashMovements/1
Since a sum of your defined template for that action is:
[Route("api/[controller]/{id}")]
Reason to why /api/ApiTest/GetCashMovement maps GetCashMovement.Get(int i) is because id is defined as optional in startup
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/**{id?}**");
A question mark (?) after the route parameter name defines an optional
parameter.
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-3.0#create-routes

OData V4 modify $filter on server side

I would like to be able to modify the filter inside the controller and then return the data based on the altered filter.
So for I have an ODataQueryOptions parameter on the server side that I can use to look at the FilterQueryOption.
Let's assume the filter is something like this "$filter=ID eq -1" but on the server side if I see "-1" for an ID this tells me that the user wants to select all records.
I tried to change the "$filter=ID eq -1" to "$filter=ID ne -1" which would give me all by setting the Filter.RawValue but this is read only.
I tried to create a new FilterQueryOption but this requires a ODataQueryContext and a ODataQueryOptionParser which I can't figure out how to create.
I then tried to set the Filter = Null and then us the ApplyTo which seems to work when I set a break point in the controller and check this on the immediate window but once it leaves the GET method on the controller then it "reverts" back to what was passed in the URL.
This article talks about doing something very similar "The best way to modify a WebAPI OData QueryOptions.Filter" but once it leaves the controller GET method then it reverts back to the URL query filter.
UPDATE WITH SAMPLE CODE
[EnableQuery]
[HttpGet]
public IQueryable<Product> GetProducts(ODataQueryOptions<Product> queryOptions)
{
if (queryOptions.Filter != null)
{
var url = queryOptions.Request.RequestUri.AbsoluteUri;
string filter = queryOptions.Filter.RawValue;
url = url.Replace("$filter=ID%20eq%201", "$filter=ID%20eq%202");
var req = new HttpRequestMessage(HttpMethod.Get, url);
queryOptions = new ODataQueryOptions<Product>(queryOptions.Context, req);
}
IQueryable query = queryOptions.ApplyTo(db.Products.AsQueryable());
return query as IQueryable<Product>;
}
Running this code will not return any product this is because the original query in the URL wanted product 1 and I swapped the ID filter of product 1 with product 2.
Now if I run SQL Profiler, I can see that it added something like "Select * from Product WHERE ID = 1 AND ID = 2".
BUT if I try the same thing by replacing the $top then it works fine.
[EnableQuery]
[HttpGet]
public IQueryable<Product> GetProducts(ODataQueryOptions<Product> queryOptions)
{
if (queryOptions.Top != null)
{
var url = queryOptions.Request.RequestUri.AbsoluteUri;
string filter = queryOptions.Top.RawValue;
url = url.Replace("$top=2", "$top=1");
var req = new HttpRequestMessage(HttpMethod.Get, url);
queryOptions = new ODataQueryOptions<Product>(queryOptions.Context, req);
}
IQueryable query = queryOptions.ApplyTo(db.Products.AsQueryable());
return query as IQueryable<Product>;
}
END RESULT
With Microsoft's help. Here is the final output that supports filter, count, and paging.
using System.Net.Http;
using System.Web.OData;
using System.Web.OData.Extensions;
using System.Web.OData.Query;
/// <summary>
/// Used to create custom filters, selects, groupings, ordering, etc...
/// </summary>
public class CustomEnableQueryAttribute : EnableQueryAttribute
{
public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
{
IQueryable result = default(IQueryable);
// get the original request before the alterations
HttpRequestMessage originalRequest = queryOptions.Request;
// get the original URL before the alterations
string url = originalRequest.RequestUri.AbsoluteUri;
// rebuild the URL if it contains a specific filter for "ID = 0" to select all records
if (queryOptions.Filter != null && url.Contains("$filter=ID%20eq%200"))
{
// apply the new filter
url = url.Replace("$filter=ID%20eq%200", "$filter=ID%20ne%200");
// build a new request for the filter
HttpRequestMessage req = new HttpRequestMessage(HttpMethod.Get, url);
// reset the query options with the new request
queryOptions = new ODataQueryOptions(queryOptions.Context, req);
}
// set a top filter if one was not supplied
if (queryOptions.Top == null)
{
// apply the query options with the new top filter
result = queryOptions.ApplyTo(queryable, new ODataQuerySettings { PageSize = 100 });
}
else
{
// apply any pending information that was not previously applied
result = queryOptions.ApplyTo(queryable);
}
// add the NextLink if one exists
if (queryOptions.Request.ODataProperties().NextLink != null)
{
originalRequest.ODataProperties().NextLink = queryOptions.Request.ODataProperties().NextLink;
}
// add the TotalCount if one exists
if (queryOptions.Request.ODataProperties().TotalCount != null)
{
originalRequest.ODataProperties().TotalCount = queryOptions.Request.ODataProperties().TotalCount;
}
// return all results
return result;
}
}
Remove [EnableQuery] attribute, your scenario should work, because after using this attribute, OData/WebApi will apply your original query option after you return data in controller, if you already apply in your controller method, then you shouldn't use that attribute.
But if your query option contains $select, those code are not working because the result's type is not Product, we use a wrapper to represent the result of $select, so I suggest you use try this:
Make a customized EnableQueryAttribute
public class MyEnableQueryAttribute : EnableQueryAttribute
{
public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
{
if (queryOptions.Filter != null)
{
queryOptions.ApplyTo(queryable);
var url = queryOptions.Request.RequestUri.AbsoluteUri;
url = url.Replace("$filter=Id%20eq%201", "$filter=Id%20eq%202");
var req = new HttpRequestMessage(HttpMethod.Get, url);
queryOptions = new ODataQueryOptions(queryOptions.Context, req);
}
return queryOptions.ApplyTo(queryable);
}
}
Use this attribute in your controller method
[MyEnableQueryAttribute]
public IHttpActionResult Get()
{
return Ok(_products);
}
Hope this can solve your problem, thanks!
Fan.
In response of #Chris Schaller I post my own solution as below:
public class CustomEnableQueryAttribute : EnableQueryAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
var url = actionContext.Request.RequestUri.OriginalString;
//change something in original url,
//for example change all A charaters to B charaters,
//consider decoding url using WebUtility.UrlDecode() if necessary
var newUrl = ModifyUrl(url);
actionContext.Request.RequestUri = new Uri(newUrl);
base.OnActionExecuting(actionContext);
}
}

ServiceStack caching strategy

I'm learning ServiceStack and have a question about how to use the [Route] tag with caching. Here's my code:
[Route("/applicationusers")]
[Route("/applicationusers/{Id}")]
public class ApplicationUsers : IReturn<ApplicationUserResponse>
{
public int Id { get; set; }
}
public object Get(ApplicationUsers request)
{
//var cacheKey = UrnId.Create<ApplicationUsers>("users");
//return RequestContext.ToOptimizedResultUsingCache(base.Cache, cacheKey, () =>
return new ApplicationUserResponse
{
ApplicationUsers = (request.Id == 0)
? Db.Select<ApplicationUser>()
: Db.Select<ApplicationUser>("Id = {0}", request.Id)
};
}
What I want is for the "ApplicationUsers" collection to be cached, and the times when I pass in an Id, for it to use the main cached collection to get the individual object out.
If I uncomment the code above, the main collection is cached under the "users" key, but any specific query I submit hits the Db again. Am I just thinking about the cache wrong?
Thanks in advance,
Mike
this line
var cacheKey = UrnId.Create<ApplicationUsers>("users");
is creating the same cache key for all the requests, you must use some of the request parameters to make a "unique key" for each different response.
var cacheKey = UrnId.Create<ApplicationUsers>(request.Id.ToString());
this will give you the "urn:ApplicationUsers:0" key for the get all and the "urn:ApplicationUsers:9" for the request with Id = 9
now you can use the extension method in this way.
return RequestContext.ToOptimizedResultUsingCache(Cache, cacheKey, () => {
if(request.Id == 0) return GetAll();
else return GetOne(request.Id);
});
I hope this helps, regards.

How to have ASP.Net MVC 3.0 Checkboxfor as checked by default?

I want mt view to have the check box checked by default,
I tried something like this.
#Html.CheckBoxFor(model=>model.GenericsOK, new { id = ViewBag.GenericsOK, #checked = true })
and also
#Html.CheckBoxFor(model=>model.GenericsOK, new { id = ViewBag.GenericsOK, #checked = "checked"})
in both cased it give the below error.
String was not recognized as a valid Boolean.
My property is defined as this.
private bool _deafaultchecked = true;
[Display(Name = "Generics Ok")]
public bool GenericsOK
{
get { return _deafaultchecked; }
set { _deafaultchecked = value; }
}
any suggestions please?
Since i could not find a solution or this.
i got this done like this.
#Html.CheckBox("GenericsOK", true, new {id=ViewBag.GenericsOK, name="GenericsOK" })
this works for my requirement.
thanks for all who helped me.
In your controller's Create method (I presume), have you tried this?
public ActionResult Create()
{
return View(new YourModelClass { GenericsOk = true });
}
In the controller action where you create the model just set that field value to true.
For example
return View(new DriverCsvModel{SendEmails = true});
You should be using the state of the model, rather than forcing the UI into a checked state.
You will want to remove the #checked="checked" portion of the HTML attributes. If the viewmodel property is a boolean then it is unnecessary when you use the CheckBoxFor
In the default constructor for your model class, you can set the "GenericsOK" property to "True"

Resources