RouteValueDictionary ignores empty values - asp.net

I have few empty route values I want in the query string:
var routeValues = new RouteValueDictionary();
routeValues.Add("one", "");
routeValues.Add("two", null);
routeValues.Add("three", string.Empty);
If I then pass it to UrlHelper.RouteUrl() it ignores all the values and the generated query string is empty. However, urls like /?one=&two=&three= are perfectly valid. How can I do that?

This behavior is built into the default Route class. It calls into the ParsedRoute.Bind() method where it does this check:
if (IsRoutePartNonEmpty(obj2))
{
acceptedValues.Add(key, obj2);
}
Which does this:
private static bool IsRoutePartNonEmpty(object routePart)
{
string str = routePart as string;
if (str != null)
{
return (str.Length > 0);
}
return (routePart != null);
}
This effectively prevents any query string values from being output if they are empty. Everything it does is private, static, internal, and otherwise impossible to override. So, there are really only 2 options to override this behavior.
Subclass Route and override GetVirtualPath(), replacing ParsedRoute.Bind() with a custom implementation.
Subclass RouteBase and create a custom route implementation.
I guess the 3rd option is to leave it alone, as others have pointed out this isn't really a problem since having an empty parameter in the query string has little or no value.

Related

How can I use a default value/model on WebAPI EmptyBody?

I have dotnet WebAPI and I'm trying to get a specific behaviour but am constantly getting 415 responses.
I have reproduced this by starting a new webapi project using dotnet new webapi on the command line. From there, I added two things: a new controller, and a model class. In my real project the model class is obviously a bit more complex, with inheritance and methods etc...
Here they are:
[HttpGet("/data")]
public async Task<IActionResult> GetModel(BodyParams input)
{
var response = new { Message = "Hello", value = input.valueOne };
return Ok(response);
}
public class BodyParams {
public bool valueOne { get; set; } = true;
}
My goal is that the user can call https://localhost:7222/data with no headers or body needed at all, and will get the response - BodyParams will be used with the default value of true. Currently, from postman, or from the browser, I get a 415 response.
I've worked through several suggestions on stack and git but nothing seems to be working for me. Specifically, I have tried:
Adding [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] into the controller, but this makes no difference unless I provide an empty {} json object in the body. This is not what I want.
Making BodyParams nullable - again, no change.
Adding .AddControllers(opt => opt.AllowEmptyInputInBodyModelBinding = true)... again, no change.
I Implemented the solution suggested here using the attribute modification in the comment by #HappyGoLucky. Again, this did not give the desired outcome, but it did change the response to : 400 - "The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true."
I tried modifying the solution in (4) to manually set context.HttpContext.Request.Body to an empty json object... but I can't figure out the syntax for this because it need to be a byte array and at that point I feel like I am way over complicating this.
How can I get the controller to use BodyParams with default values in the case that the user provides no body and no headers at all?
You can achieve that using a Minimal API.
app.MapGet("/data",
async (HttpRequest httpRequest) =>
{
var value = true;
if (Equals(httpRequest.GetTypedHeaders().ContentType, MediaTypeHeaderValue.Parse("application/json")))
{
var bodyParams = await httpRequest.ReadFromJsonAsync<BodyParams>();
if (bodyParams is not null) value = bodyParams.ValueOne;
}
var response = new {Message = "Hello", value};
return Results.Ok(response);
});
So, as there doesn't seem to be a more straightforward answer, I have currently gone with the approach number 5) from the OP, and just tweaking the code from there very slightly.
All this does is act as an action which checks the if the user has passed in any body json. If not, then it adds in an empty anonymous type. The behaviour then is to use the default True value from the BodyParams class.
The full code for the action class is:
internal class AllowMissingContentTypeForEmptyBodyConvention : Attribute, IActionModelConvention
{
public void Apply(ActionModel action)
{
action.Filters.Add(new AllowMissingContentTypeForEmptyBodyFilter());
}
private class AllowMissingContentTypeForEmptyBodyFilter : IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
if (!context.HttpContext.Request.HasJsonContentType()
&& (context.HttpContext.Request.ContentLength == default
|| context.HttpContext.Request.ContentLength == 0))
{
context.HttpContext.Request.ContentType = "application/json";
var str = new { };
//convert string to jsontype
var json = JsonConvert.SerializeObject(str);
//modified stream
var requestData = Encoding.UTF8.GetBytes(json);
context.HttpContext.Request.Body = new MemoryStream(requestData);
}
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
// Do nothing
}
}
}
Then you can add this to any of your controllers using [AllowMissingContentTypeForEmptyBodyConvention]

ASP.NET Core 2 IFormCollection returning StringValues

Recently the IFormCollection in the platform I'm building started returning values of the type Microsoft.Extensions.Primitives.StringValues. when it used to return strings.
The controllers were made with strings in mind and now that are a lot of forms that are not working.
Is there any explanation to this, or a way to revert it?
As far as I'm aware ASP.NET Core's IFormCollection has always been a collection of StringValues. The reason is simple: multiple values can be posted for any particular key, making it potentially impossible to set the value if the type was merely string. There is no way to "revert" this. Change your code accordingly.
Or, better yet, stop using IFormCollection. Bind to strongly-typed models. That's always the best way.
For others coming here also confused by seeing IFormCollection giving StringValues. You may be familiar with .NET Frameworks FormCollection class, which gives strings. The reason for the change is valid and explained by #Chris Pratt in his answer here.
To make IFormCollection and StringValues feel familiar again consider any of these simple extensions:
// Example: var name = collection["name"].FirstOrNull();
// Equal to (.NET Framework): var name = collection["name"];
public static string FirstOrNull(this StringValues values)
{
if (values.Count > 0)
{
return values[0];
}
return null;
}
// Example: var name = collection["name"].FirstOr("John Doe");
// Equal to (.NET Framework): var name = collection["name"] ?? "John Doe";
public static string FirstOr(this StringValues values, string fallback)
{
if (values.Count > 0)
{
return values[0];
}
return fallback;
}
// Example: var name = collection.ValueOrFallback("name", "John Doe");
// Equal to (.NET Framework): var name = collection["name"] ?? "John Doe";
public static string ValueOrFallback(this IFormCollection collection, string key, string fallback)
{
if (collection[key].Count > 0)
{
return collection[key][0];
}
return fallback;
}
Also consider the built-in TryGetValue:
if (collection.TryGetValue("name", out var name))
{
// at least one name did exist
}
Alt.
var name = collection.TryGetValue("name", out var names) ? names[0] : "John Doe";

Setting a default page size for a WebAPI 2 OData service

I have an ODataController with an endpoint like this:
[EnableQuery]
public IQueryable<Customer> Get()
{
return _context.Customers;
}
And this setting in the Register method of the WebApiConfig:
config.Count().Filter().OrderBy().Expand().Select().MaxTop(100);
There are a lot of Customer entries and I don't want the client requesting too many at once as the query will take a very long time. Luckily this setting means if they do a request like this:
http://domain.com/api/Customers?$top=1000
It'll prevent them from retrieving them as it's higher than 100.
However, if they do a request like this:
http://domain.com/api/Customers
This then attempts to retrieve all customers, which I don't want.
I know I can set the page size like this:
[EnableQuery(PageSize = 10)]
public IQueryable<Customer> Get()
{
return _context.Customers;
}
And this will only return 10 results, however I still want the user to be able to specify their own $top and $skip values for paging (and deciding how many results they want per page). What I want is for there to be a maximum of 100 and a default of 10.
How can I achieve this?
EDIT
I tried the following but it doesn't work properly when using Expand clauses. Any other ideas?
[EnableQuery(MaxTop = 100)]
public IQueryable<Customer> Get(ODataQueryOptions<Customer> queryOptions)
{
IQueryable<Customer> query = _context.Customers;
int? top = queryOptions?.Top?.Value;
if (top == null)
{
query = query.Take(10);
}
return query;
}
Define class:
public class EnableQueryWithDefaultPageSizeAttribute : EnableQueryAttribute
{
const int pageSizeDefault = 10;
public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
{
int? top = queryOptions?.Top?.Value;
if (top == null)
{
return queryOptions.ApplyTo(queryable, new ODataQuerySettings { PageSize = this.PageSize == 0 ? pageSizeDefault : this.PageSize });
}
return queryOptions.ApplyTo(queryable); ;
}
}
and use it instance of EnableQuery. See this.
I was facing the same thing, but I was able to get it working. It looks like the order of parameters matters. You'll have to forgive me, I am very new at this and learning as I go, so I hope this actually helps.
My API controller looks like this:
[EnableQuery(PageSize=100)]
public IQueryable<APPLICATION_LOGS> Get()
{
return db.APPLICATION_LOGS;
}
In my client, I am building a request and capturing the response:
string entity = "/AuditApplicationLog";
string expandOptions = "&$expand=APPLICATION, APPLICATIONS_SUBSYSTEMS, FILE";
string parameters = "?$top=100&$count=true";
string url = serviceUri + entity + parameters + expandOptions;
WebRequest request = WebRequest.Create(url);
WebResponse response = request.GetResponse();
This is all a practice project that I am just using to teach myself a few concepts.The page size in my API controller is set to 100 but you'll notice in the parameters variable I am setting a top value. If I set this value to 25, it will return 25. If I set it to 50, it will return 50 etc. Apparently the order I set the parameters matters. If I set the top value before the expand parameters, it works. If I attempt to set it after the expand parameters it returns the default 100, regardless of the value I set top at. It does appear to leave out the "nextPage" link, but that can be accounted for. I hope this helps.

Web Performance Testing context paramters

I used the MSDN guide on creating Custom Extraction Rule, which presents this example (Extract method):
public override void Extract(object sender, ExtractionEventArgs e)
{
if (e.Response.HtmlDocument != null)
{
foreach (HtmlTag tag in e.Response.HtmlDocument.GetFilteredHtmlTags(new string[] { "input" }))
{
if (String.Equals(tag.GetAttributeValueAsString("name"), Name, StringComparison.InvariantCultureIgnoreCase))
{
string formFieldValue = tag.GetAttributeValueAsString("value");
if (formFieldValue == null)
{
formFieldValue = String.Empty;
}
// add the extracted value to the web performance test context
e.WebTest.Context.Add("someNameHere", formFieldValue);
e.Success = true;
return;
}
}
}
// If the extraction fails, set the error text that the user sees
e.Success = false;
e.Message = String.Format(CultureInfo.CurrentCulture, "Not Found: {0}", Name);
}
However, I just don't know how to use access the someNameHere in the Web Test and add it to the QueryString as a parameter.
Any help would be greatly appreciated.
Right-click on the request in the web test and select "Add URL query string parameter". Alter the name as needed and into the value field enter {{someNameHere}}. The doubled curly braces call for a context parameter value to be inserted. The doubled curly braces can be used to insert the value of a context parameter into many other places in a web test. Note that strings such as text{{someNameHere}}moretext can be used to join context values to other strings.

Caching Patterns in ASP.NET

So I just fixed a bug in a framework I'm developing. The pseudo-pseudocode looks like this:
myoldObject = new MyObject { someValue = "old value" };
cache.Insert("myObjectKey", myoldObject);
myNewObject = cache.Get("myObjectKey");
myNewObject.someValue = "new value";
if(myObject.someValue != cache.Get("myObjectKey").someValue)
myObject.SaveToDatabase();
So, essentially, I was getting an object from the cache, and then later on comparing the original object to the cached object to see if I need to save it to the database in case it's changed. The problem arose because the original object is a reference...so changing someValue also changed the referenced cached object, so it'd never save back to the database. I fixed it by cloning the object off of the cached version, severing the reference and allowing me to compare the new object against the cached one.
My question is: is there a better way to do this, some pattern, that you could recommend? I can't be the only person that's done this before :)
Dirty tracking is the normal way to handle this, I think. Something like:
class MyObject {
public string SomeValue {
get { return _someValue; }
set {
if (value != SomeValue) {
IsDirty = true;
_someValue = value;
}
}
public bool IsDirty {
get;
private set;
}
void SaveToDatabase() {
base.SaveToDatabase();
IsDirty = false;
}
}
myoldObject = new MyObject { someValue = "old value" };
cache.Insert("myObjectKey", myoldObject);
myNewObject = cache.Get("myObjectKey");
myNewObject.someValue = "new value";
if(myNewObject.IsDirty)
myNewObject.SaveToDatabase();
I've done similar things, but I got around it by cloning too. The difference is that I had the cache do the cloning. When you put an object into the cache, the cache will clone the object first and store the cloned version (so you can mutate the original object without poisoning the cache). When you get an object from the cache, the cache returns a clone of the object instead of the stored object (again so that the caller can mutate the object without effecting the cached/canonical object).
I think that this is perfectly acceptable as long as the data you're storing/duping is small.
A little improvement on Marks anwser when using linq:
When using Linq, fetching entities from DB will mark every object as IsDirty.
I made a workaround for this, by not setting IsDirty when the value is not set; for this instance: when null. For ints, I sat the orig-value to -1, and then checked for that. This will not work, however, if the saved value is the same as the uninitialized value (null in my example).
private string _name;
[Column]
public string Name
{
get { return _name; }
set
{
if (value != _name)
{
if (_name != null)
{
IsDirty = true;
}
_name = value;
}
}
}
Could probably be improved further by setting IsDirty after initialization somehow.

Resources