How to filter SQL Server DateTime using .NET Core WebAPI with OData v4 - asp.net-core

I am running a simple .NET Core WebApi application with OData Query v4 and SQL Server 2012.
This works, but it's extremely slow:
GET /api-endpoint?$filter=date(MyDateTimeField) ge 2018-01-01&$top=100
SQL Query generated by the URL above:
SELECT TOP 100 * FROM MyTable WHERE ((((DATEPART(year, [MyDateTimeField]) * 10000) + (DATEPART(month, [MyDateTimeField]) * 100)) + DATEPART(day, [MyDateTimeField])) >= (((2018 * 10000) + (1 * 100)) + 1))
When I try to do this:
GET /api-endpoint?$filter=MyDateTimeField ge 2018-01-01T00:00:00.00Z&$top=100
It generates the following SQL query:
SELECT TOP 100 * FROM MyTable WHERE [MyDateTimeField] > '2018-01-01T00:00:00.0000000'
Which returns this error:
Conversion failed when converting date and/or time from character
string.
What would the OData Query syntax be to generate a SQL query similiar to this?
SELECT TOP 100 * FROM MyTable WHERE [MyDateTimeField] > '2018-01-01'

Assuming that the field of MyDateTimeField is datetime instead of datatime2, decorate the MyDateTimeField with a column annotation [Column(TypeName="datetime")] firstly :
public class MyTable
{
// ... other props
[Column(TypeName="datetime")]
public DateTime MyDateTimeField {get;set;}
}
To query with datetime, cast it into DateTimeOffset:
?$filter=MyDateTimeField ge cast(2018-01-01T00:00:00.00Z,Edm.DateTimeOffset)
The generated sql is something like :
SELECT ..., [$it].[MyDateTimeField],
FROM [MyTable] AS [$it]
WHERE [$it].[MyDateTimeField] >= '2018-01-01T08:00:00.000'
Note the datetime above is 2018-01-01T08:00:00.000 instead of 2018-01-01T00:00:00.0000000.
A screenshot of Demo:

After drowning incredulous in my own frustration I finally found a solution which would not force my api consumers to cast the DateTime string in an, ugly, verbose, disturbing expression.
I also want my model to transparently look like using DateTimeOffset instead of DateTime, this will allow me in the future to refactor the database and finally use DateTimeOffset even if so far I don't need to handle time zones.
My limitation is that I cannot update my legacy database and so here is the solution.
Is this solution for you?
This solution is useful only if:
You cannot (or want to) update your db column type (if you can you should and solve the problem)
You use or can change you entities to use DateTimeOffset instead of DateTime (If it is a new api consider doing this to comply with odata standard and allowing you to refactor in the future the underlay database)
You don't need to handle different time zones (this can be done but you need to work on the solution to do it)
You use EF Core >= 2.1
Solution
Update your entity using DateTimeOffset
public class MyEntity {
[...]
public DateTimeOffset CreatedDateTime { get; set; }
public DateTimeOffset ModifiedDateTime { get; set; }
[...]
}
Create a ValueConverter
public static class ValueConvertes
{
public static ValueConverter<DateTimeOffset, DateTime> DateTimeToDateTimeOffset =
new ValueConverter<DateTimeOffset, DateTime>(
model => model.DateTime,
store => DateTime.SpecifyKind(store, DateTimeKind.UTC));
}
Update your model mapping
public void Configure(EntityTypeBuilder<QuestionQML> builder)
{
builder.ToTable("MyEntityTable");
builder.Property(e => e.CreatedDateTime)
.HasColumnName("CreatedDateTime") // On DB datetime Type
.HasConversion(ValueConvertes.DateTimeToDateTimeOffset);
builder.Property(e => e.ModifiedDateTime)
.HasColumnName("ModifiedDateTime") // On DB datetime Type
.HasConversion(ValueConvertes.DateTimeToDateTimeOffset);
[...]
}
This allow you to filter in the following ways:
?$filter=CreatedDateTime gt 2010-01-25T02:13:40Z
?$filter=CreatedDateTime gt 2010-01-25T02:13:40.01234Z
?$filter=CreatedDateTime gt 2010-01-25
Special thanks to chris-clark
EDIT:
Corrected code DateTimeKind.UTC can be used when the datetime stored in the database is in UTC, if you store in a different timezone you need to set the Kind to the timezone you use, but this changes the way your datetimes will be shown in the results, showing for UK Timezone, for example, Z(GMT time) or +01:00 (BST time.

Related

Odata filter DateTimeOffset less that date

I am trying to retrieve all records before specific date as follow:
?$filter=CreatedDate lt '2020-06-04T14:27:12.38'
but i keep receiving this error
"message": "The query specified in the URI is not valid. A binary operator with incompatible types was detected. Found operand types 'Edm.DateTimeOffset' and 'Edm.String' for operator kind 'GreaterThan'.",
I tried to cast date
?$filter=CreatedDate lt cast('2020-06-04T14:27:12.38', Edm.DateTimeOffset))
but still the same .
also tried
?$filter=CreatedDate lt datetime'2020-06-04T14:27:12.38'
and received
The query specified in the URI is not valid. Unrecognized 'Edm.String' literal 'datetime'1995-09-01T00:00:00'' at '21' in 'CreatedDate gt datetime'1995-09-01T00:00:00''.
is there anyway to achieve that ?
You have to query date without quotation marks. An example from my application:
http://localhost/EDS4OData/EmployeeHoursVacationRequests?$filter=(Username eq 'jsmith' and DeleteInd eq false and DayOffType eq 2 and StartDate ge 2020-10-01T00:00:00.000Z and StartDate le 2020-12-31T23:59:59.999Z)&$count=true
In my C# model for the controller:
public DateTime StartDate { get; set; }
A quick googling lead me to this answer,
so try
?$filter=CreatedDate lt datetime'2020-06-04T14:27:12.38'
I managed to get it working like this:
?$filter=CreatedDate lt 2021-03-19T12:50:54.219Z
Check this answer: https://stackoverflow.com/a/27775965/3264998
if you are using .NET sdk to generate Storage Table query try using the TableQuery.GenerateFilterConditionForDate version
var timestampToFilter1 = TableQuery.GenerateFilterConditionForDate("Timestamp", QueryComparisons.GreaterThanOrEqual,
DateTimeOffset.Parse("2022-06-15T04:32:07.000Z") );
var timestampToFilter2 = TableQuery.GenerateFilterConditionForDate("Timestamp", QueryComparisons.LessThanOrEqual,
DateTimeOffset.Parse("2022-06-15T12:32:07.000Z"));
var timestampToFilter = TableQuery.CombineFilters(timestampToFilter1, TableOperators.And, timestampToFilter2);

How to compare string data to time

I have data type in string and the time is like 06:00A, 09:00P, etc. I would like to query data from 6am to 12pm, how do I convert the string data to time format and query it in linq to sql?
Use DateTime.ParseExact or DateTime.TryParseExact to convert the string to a date. If you can't guarantee that the string version of your time is always going to be correct, stick to the TryParseExact version.
Once you have it converted to date, query as normal.
Example at: https://dotnetfiddle.net/MDnERt
Edited after response:
If you are using the code as written against EntityFramework then no, this will not work. (Please also note that there is a big difference between Linq To SQL and Entity Framework, but the same concepts apply, to some degree)
ORMs that support LINQ are actually converting your where clauses into an Expression which is then translated by the ORM into SQL. You will get a NotSupported exception, or something similar.
Is there some reason why the table in question is using that time format? Why would you not just use a datetime in the table? There is also the option of using the time datatype in sql server (assuming you are targetting sql server) which is mapped to the TimeSpan type in .net.
You would define your table in Sql server like:
create table log ( data varchar(20), logtime time )
and the LINQ expression would look something like:
from x in Logs
where x.Logtime >= new TimeSpan(6,0,0) && x.Logtime <= new TimeSpan(12,0,0)
select x
Now we are getting into actual design questions, though, which is off topic. :)
I'd suggest writing own parser and represent times as TimeSpan:
TimeSpan? ToTimeSpan(string str)
{
// get A or P at the end
var amPm = str.Last();
int hrs, mins;
try
{
hrs = int.Parse(str.Substring(0, 2));
mins = int.Parse(str.Substring(3, 2));
}
catch
{
return null;
}
switch (amPm)
{
case 'P': hrs += 12; break;
case 'A': break;
default: return null;
}
return new TimeSpan(hrs, mins, 0);
}

DbContext.DbSet.FromSql() not accepting parameters

I have a database with two tables, and wrote a relatively simple select statement that combines the data and returns me the fields I want. In my SQL developer software it executes just fine.
Now to execute it in my C# .NET Core application, I created a "fake" DbSet in my DbContext that doesn't have a proper table on the database. The Type of the DbSet is a Model that represents the resulting data structure of the select statement. I use the DbSet field to access the method FromSql() and execute the select like so:
List<ProjectSearchModel> results = _ejContext.ProjectSearch.FromSql(
#"SELECT combined.Caption, combined.Number FROM
(SELECT p.Caption, p.Number, CreateDate FROM dbo.Project AS p
UNION
SELECT j.Caption, j.Number, CreateDate FROM dbo.Job AS j) AS combined
WHERE combined.Caption LIKE '{0}%' OR combined.Number LIKE '{0}%'
ORDER BY combined.CreateDate DESC
OFFSET 0 ROWS
FETCH FIRST 30 ROWS ONLY", term)
.ToList();
The SQL does properly return data, I've tested that. But the result variable holds 0 entries after executing. In the documentation for FromSql() I found that with MS SQL Servers you have to use OFFSET 0 when using ORDER BY so that's what I did.
I have no idea what I'm doing wrong.
You need to use DbQuery<T> instead:
public DbQuery<Project> ProjectSearch { get; set; }
Then, you can issue arbitrary queries using FromSql on that. With DbSet, the query must be composable, which means it can only be a standard select, it must correspond to a real table, and must include all properties on the entity - none of which apply to the query you're trying to perform.
As #ChrisPratt said, one of my mistakes was using the DbSet class instead of the DbQuery class. But also, what drove me crazy is that my parameters didn't work. My mistake was putting them inside a string, which results in them not being recognized as parameters. So my SQL string should be
...
WHERE combined.Caption LIKE {0} + '%' OR combined.Number LIKE {0} + '%'
...
instead of
...
WHERE combined.Caption LIKE '{0}%' OR combined.Number LIKE '{0}%'
...

how to translate "where (r.value1 - r.value2 > 0.01)" to NHibernate

I'm converting an MSSQL query to NHibernate. In essence, this is my SQL:
SELECT * FROM MyTable as T
WHERE (T.Value1 - T.Value2 > 0.01)
And this is my C# code:
var query = QueryOver.Of<MyType> ()
.Where(r => (r.Value1 - r.Value2 > 0.01));
and it's giving me an exception:
Could not determine member from (r.Value1 - r.Value2)
I'm sure there's a way to let the database do the calculation. Does anyone know?
Predicates within QueryOver.Where() can generally only be very simple comparison expressions. QueryOver dos not support arithmetic operations within delegates (r.Value1 - r.Value2). As #Rippo has pointed out You need to fallback to ICriterion.
.QueryOver<MyType>()
.Where(
Expression.Gt(
Projections.SqlProjection("{{alias}}.rValue1 - {{alias}}.rValue2", null, null),
0.01
)
)
In sql query you use "MyTable" but in QueryOver: "MyType"
reading back on this, I wanted to share the solution that ended up in production: mapping a readonly formula in FluentNHibernate.
the model needs to have a Property like this:
public virtual decimal Difference { get { return Value2 - Value1; } protected set; }
and this property should be mapped as follows:
Map(x => x.Difference).Formula("(Value2 - Value1)").Readonly();
the string parameter of the Formula method ends up in the SQL Select query, similar to this :
Select Id, Value1, Value2, (Value2 - Value1) from MyTable...
Note that the calculated property needs to have a protected setter or NHibernate will throw an exception saying that no setter is available.

nHibernate 3 - QueryOver with DateTime

I'm trying to write a query to select using the DateTime. Year as a where parameter, but I'm receiving this error from nunit:
NHibernate.QueryException : could not resolve property: Register.Year of: Estudantino.Domain.Events
In class Events I've a property named Register as a DateTime type.
public virtual DateTime Registada { get; set; }
This is the method that is returning the error:
using (ISession session = NHibernateHelper.OpenSession())
{
return session.QueryOver<Evento>()
.Where(x => x.Register.Year == year)
.List();
}
The variable year is of type int, that is been passed to the method.
Does anyone have an idea of what i'm doing wrong? My database server is SQL Server 2005 Express.
QueryOver does not resolve things like DateTime.Year.
Use LINQ instead:
return session.Query<Evento>()
.Where(x => x.Register.Year == year)
.ToList();
In QueryOver, you can just say:
dateTimeColumn.YearPart() == 1999
There is also other extension methods: MonthPart(), DayPart(), etc.
You may need to use HQL here instead. Basically NHibernate does not know really what to do with year. I suspect you need to use the RegisterFunction to register a YEAR function.
Please read this article in its entirety to fully understand what it is you are trying to do.
Create a custom dialect in code
Create a function in SQL server called MyYearFunction that returns a year for a date
Then use HQL (not sure if QueryOver can do this) to get the date where dbo.MyYearFunction(:date) = 12" ...
.Where(x => x.Register >= new DateTime(year, 1, 1) && x.Register <
new DateTime(year + 1, 1, 1))