How to build Dynamic Linq from Request.QueryString? - asp.net-mvc

I'm currently working on feature to implement/build a filter through Request.QueryString. The idea here is the filter can be on any property with in the Model For ex.,
public class Alert{
public string Name;
public string Status;
public Datetime StartDate;
public Datetime EndDate;
public bool IsActive;
}
so the calling client wants to pass something like this in the query string startdate >=2013-10-1&Name=John&IsActive=false. I'm using System.Linq.Dynamic from Scottgu to build the where clause which takes a string but the format to build is kind of killing me. I need some pointers on the format and I'm doing the type checking through reflection before executing this and also I'm filtering this against the data that came back from DB and not passing this into the db.Any help is really appreciated!

Don't reinvent the wheel if you don't need to :P
Have a look at Odata, and the .net web.api
That should do what you need.

Related

Web Api OData Query for custom type

I've defined my own struct that represents DateTime with TimeZoneInfo so I can work with UTC time while keeping the information about timezone.
I would like to get these objects with OData query, but it fails when I try to use $orderby on this properties of this type. I was able to get results when I queried $orderBy=Timestamp/Value/UniversalTime but I would like to use just $orderBy=Timestamp
Is there any possibility to order collection with this type?
public struct DateTimeWithZone : IComparable<DateTime>, IComparable<DateTimeWithZone>, IFormattable
{
private readonly DateTime _utcDateTime;
private readonly TimeZoneInfo _timeZone;
public DateTimeWithZone(DateTime dateTime, TimeZoneInfo timeZone)
{
if (timeZone == null)
{
throw new NoNullAllowedException(nameof(timeZone));
}
_utcDateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc);
_timeZone = timeZone;
}
...
}
With model defined like this:
public class ClientViewModel
{
public string Name { get; set; }
public DateTimeWithZone? Timestamp { get; set; }
}
And this is how it is used:
public IHttpActionResult GetAll(ODataQueryOptions<ClientViewModel> options)
{
var fromService = _clientsClient.GetAllClients().MapTo<ClientViewModel>(MappingStrategy).AsQueryable();
var totalCount = fromService.Count();
var results = options.ApplyTo(fromService); // <-- Fails here
return Ok(new PageResult<ClientViewModel>(
results as IEnumerable<ClientViewModel>,
Request.ODataProperties().NextLink,
totalCount));
}
Fails with The $orderby expression must evaluate to a single value of primitive type.
we had some similar issue with complex type ordering. Maybe this can be of assistance in your scenario as well. In our case (which is not 100% identical) we use a two phase approach:
Rewriting ODataQueryOptions
separating the extneral model (ODATA) and the internal model (EntityFramework in our case)
Rewriting ODataQueryOptions
You mention that the format $orderBy=Timestamp/Value/UniversalTime is accepted and is processed properly by ODATA. So you can rewrite the value basically by extracting the $orderby value and reinserting it with in your working format.
I described two ways on how to do this in my post Modifying ODataQueryOptions on the fly (full code included), which take existing options recreate new options by constructing a new Uri. In your case you would extract Timestamp from $orderBy=Timestamp and reinsert as with $orderBy=Timestamp/Value/UniversalTime.
Separating External and Internal Model
In addition, we used two models for the public facing API and the internal / persistence layer. On the internal side we used different properties which we grouped into a navigation property (which only exists on the public side). With this approach the user is able to specify an option via an $expand=VirtualNavigationProperty/TimeZoneInfo and $orderby=.... Internally you do not have to use the complex data type, but keep using DateTimeOffset which already holds that information. I described this separation and mapping of virtual navigation properties in the following post:
Separating your ODATA Models from the Persistence Layer with AutoMapper
More Fun with your ODATA Models and AutoMapper
According to your question it should be sufficient to rewrite the query options in the controller as you did mention that the (little bit longer) $orderby format is already working as expected and you only wanted a more convenient query syntax.
Regards, Ronald

Getting timestamp() value using C# client

Just a quick one, I have a node that gets a value set using the value of timestamp(), when I query for the node using
public List<Thing> ListThings()
{
return client.Cypher
.Match("(thing:Thing)")
.Return<Thing>("thing").Results.ToList();
}
I have a class called Thing that looks like this:
public class Thing{
public string name {get; set;}
public int id {get;set;}
public DateTimeOffset timestamp{get;set}
}
We create the 'thing' ahead of time by using:
Create(thing:Thing{id:6,name:'thing1', timestamp:timestamp()}) return thing
I get every value back apart from timestamp when calling from my script, which is a little annoying, any ideas? I get all the values back using the query in Neo4j browser, so I was wondering if I was actually doing something wrong?
Thanks
Tatham is right, the timestamp() returns a long that's not convertible into DateTimeOffset or DateTime. One of the problems is that the returned value is Milliseconds since 1/1/1970, so you need to calculate that, unfortunately, JSON.Net can't do that automagically.
If you can change your Thing class you could have a property that isn't serialized but is calculated by the Timestamp property, something like this:
public class Thing
{
private static DateTime _epoch = new DateTime(1970, 1, 1);
public string id { get; set; }
public string name { get; set; }
private long _ts;
public long timestamp
{
get { return _ts; }
set
{
_ts = value;
TimestampAsDateTime = _epoch.AddMilliseconds(_ts);
}
}
[JsonIgnore]
public DateTime TimestampAsDateTime { get; set; }
}
The TimestampAsDateTime property would only ever be available in your code.
If you go to the Neo4j console and just run RETURN timestamp(), you'll see the result is a number like 1440468541547. That can't be de-serialized to a DateTimeOffset, especially as there's no offset component. You'll need to use a different type in C#: probably long, or maybe DateTime.
Thank you for your excellent answers, you solved the problem, but I am using a slightly overlooked (on my part) solution.
As the data type for timestamp() is long I am just going to return that from the service. We have the ability to handle that perfectly using the clients in-built Date object.
It was a mistake on my part, I wrongly assumed that timestamp would have been DateTime, but then I got an error about it and was advised to use DateTimeOffset, which I did, never really considered long as an option.
Please remember I am from a MSSQL background and some things, although easier to do, take a different perspective to grasp.
Thanks to you both though, appreciate the time taken to assist.

Web Api: Pass an empty parameter in a query string

I'm looking for a proper way to handle empty parameters in a query string. Web Api does not accept query strings as "?id=1&category=", which seems reasonable, but I need to handle this case.
The quick and dirty solution is to use a custom value which will be interpreted on the server side (say "(empty)" for example) but I'm not satisfied with it...
Any suggestion ?
Thanks.
One way I have dealt with this in the past is to make a class to hold your paramaters and then use to ModelBinder attribute to bind your query parameters to the class properties.
So your class would look something like this:
public class QueryParams
{
public string Category {get; set;}
public int Id {get; set;}
}
And the method in your api controller would look like this:
public objectToReturn Get([ModelBinder] QueryParams)
{
//code here
}
This way if one of the parameters in the query string has no value it will simply be ignored.
You can use this attribute to achieve what you want.
[DisplayFormat(ConvertEmptyStringToNull = false)]
If "category" is missing then it will be null.
Otherwise, if "category=" or "category= " then will be an empty string or whitespace.

MVC post not populating the model

I cannot figure out why my model is not being populated. All the data posted is in the Request.Form, but the model is actually turning out to be null.
According to this answer on model with collection not populating on postback
In other words, (...) If any required fields are missing, or if the values are
submitted in such a way that they cannot be converted to the type of a required field, then the entire object will be left null
I have changed several value types, but I cannot get it to work.
Here is my model:
public class AddModel
{
//Get properties
public Vehicle vehicle;
//Post properties
[Required(ErrorMessage = "Please enter a start date")]
public DateTime StartDate;
public int? StatusCode;
public SelectList StatusCodes()
{
...
}
}
Can you think of why it's not being populated?
Making AddModel members Properties - adding get; set; to fields should solve your problem
As per #archil's response, you should make your public variables properties. Although this may work with types you will run into problems as soon as you add complexity. Certainly for classes but possible for nullable types too.
The model binders use reflection to parse the form fields into the model, and reflection works differently on properties to public variables - in the case of these models, the differences will likely be causing the failure here.
Hope that helps - and hat tip to #archil for answering this (probably) correctly, much sooner than me!

Creating an ActionLink with a DateTime in the query string in ASP.NET MVC

I have a search form with a DateTime search criterion, plus some other criteria:
<form method="get" action="/app/search">
<input type="text" value="13/01/2010" name="BeginDate"/>
<input type="text" value="blah" name="SomeOtherCriterion"/>
<form>
So I have a Search controller with a default Action (let's call it Index) and with a SearchCriteria parameter.
public class SearchController
{
public ActionResult Index(SearchCriteria searchCriteria) {//blah }
}
public class SearchCriteria
{
public DateTime BeginDate {get; set;}
public string SomeOtherCriterion {get; set;}
}
Now if I want to create an ActionLink, passing in a SearchCriteria value, thus:
Html.ActionLink("Search", "Index", searchCriteria)
I get the BeginDate query string parameter in US format. Looking on Google and poking around in System.Web.Routing using Reflector it seems to be because it uses the InvariantCulture, so there's nothing I can do about it.
It seems like noone has asked this question before so I guess I'm doing something very stupid.... Please help!
EDIT: Pass in SearchCriteria to ActionLink rather than anonymous object to show why I can't just do the custom ToString() myself.
Given that the framework appears to be hard-coded to handle this piece of data using InvariantCulture, I don't think there's much you can do to make it work transparently.
There is one ugly option - download the MVC source and rip out the code for all the offending classes from Route down to ParsedRoute to create your own RouteBase implementation that does what you need.
If I absolutely had to keep the DateTime declaration on the SearchCriteria class, then that's the route (sorry for the pun) I would choose.
However, a far easier solution would be to change your SearchCriteria class to use a slightly different declaration for the DateTime field, based on a type like this:
public class MyDateTime
{
public DateTime Value { get; set; }
//for passing MyDateTime in place of a DateTime without casting
public static implicit operator DateTime(MyDateTime instance) { return instance.Value; }
//so you can assign a MyDateTime from a DateTime without a cast
//- e.g. MyDateTime dt = DateTime.Now
public static implicit operator MyDateTime(DateTime instance) { return new MyDateTime() { Value = instance }; }
//override ToString so that CultureInfo.CurrentCulture is used correctly.
public override string ToString()
{
return Value.ToString(CultureInfo.CurrentUICulture);
}
}
In theory you should be able to roll out this change without too much fuss.
The big work could be if you have a lot of code that uses members (e.g. .Days etc) of the DateTime instance in SearchCriteria: you either have to reproduce those members on MyDateTime, wrapping around the inner DateTime Value or change all the code to use .Value.Member.
To avoid issues related to Regional Settings and "Culture",
I treat date and time as separate unbound fields and then
assemble them into DateTime in my Controller.
Example:
Year [ ] Month [ ] Day [ ]
I always present separate textboxes for year, month, and day, in that order so that there can be no confusion between U.S. format (month/day/year) and more or less the rest of the world's format (day/month/year).
Can you provide a formatted date in your ActionLink? Try this:
Html.ActionLink("Search",
"Index",
new {BeginDate =
DateTime.Now.ToString("d", new CultureInfo("pt-BR");})
Of course this changes BeginDate to a string instead of a DateTime... but maybe that will work for you?
We use ISO ("s" in a format string -- YYYY-MM-DDTHH:MM:SS) format for this. It works correctly, and JavaScript can handle it as well.
Perhaps you could use a Model Binder to format and parse the date? Just re-read the article and noticed that it does not format the date...Probably not going to work out. I'll leave the answer though in case it provides any unintentional inspiration :)
poking around in System.Web.Routing using Reflector it
seems to be because it uses the
InvariantCulture
Are you realy shure about this? The parts of Modelbinding and UrlBuilding I checked used CurrentCulture. Can you check what happens if you set the CurrentCulture before rendering the link?
Get the ASP.NET MVC 1.0 book written by Scott Hanselman, Scott Guthrie, Phil Haack, and Rob Conery. They actually do this exact scenario in the book. They use a specific route. I am looking at it right now on page 216.
They do it by breaking up day, month, and year. Then it is your responsibility to use those values as they come back.

Resources