2

I have two entities in a database which hold a DateTime in two different formats; SuperLogs.EndTime is a normal DateTime whilst SuperLogs.Logs holds the DateTime as separated numeric components of Year (int), Month (int), Day (int) and Time (double). What I want to do in my query is get the only SuperLogs entity which latest Log has a DateTime greater than the EndTime of superLogX.

I have a C# function (GetLogDateTime) to convert the DateTime on the Logs into an actual DateTime but when passed into the following query, "System.InvalidOperationException: Internal .NET Framework Data Provider error 1025." is thrown.

var superLogNewerThanSuperLogX = await MyDbContext.SuperLogs.SingleOrDefaultAsync
(
    superLog => superLog.Logs.Max<Log, DateTime>(GetLogDateTime) > superLogX.EndTime
);

GetLogDateTime is implemented as follows:

private static DateTime GetLogDateTime(Log log)
{
    var time = log.Time;
    var hasTime = log.Time.HasValue;
    var hours = hasTime ? Math.Floor(time.Value / 10000) : 0;
    var minutes = hasTime ? Math.Floor(time.Value / 100) - hours * 100 : 0;

    return new DateTime
    (
        log.Year,
        log.Month,
        log.Day,
        (int)hours,
        (int)minutes,
        hasTime ? (int)(time.Value - (hours * 10000 + minutes * 100)) : 0,
        hasTime ? (int)((time.Value - Math.Truncate(time.Value)) * 1000) : 0
    );
}

My guess is part of the problem is that GetLogDateTime only converts into a Func<Log, DateTime> but needs to convert into an Expression<Func<Log, DateTime>> somehow. I'm not sure how to do this and if it's not possible I also have a Database-implemented Scalar Function which performs the same task as this method.

CREATE FUNCTION dbo.CreateDateTime
(
    @year SMALLINT,
    @month SMALLINT,
    @day SMALLINT,
    @time FLOAT
)
RETURNS DATETIME AS
BEGIN
    DECLARE @paddedYear VARCHAR(4) = REPLACE(STR(@year, 4), SPACE(1), '0')
    DECLARE @paddedMonth VARCHAR(2) = REPLACE(STR(@month, 2), SPACE(1), '0')
    DECLARE @paddedDay VARCHAR(2) = REPLACE(STR(@day, 2), SPACE(1), '0')
    DECLARE @hours FLOAT = ROUND(@time / 10000, 0, 1)
    DECLARE @hoursAndMinutes FLOAT = ROUND(@time / 100, 0, 1)
    DECLARE @hoursMinutesAndSeconds FLOAT = ROUND(@time, 0, 1)
    --DATETIME only supports 3 decimal places on seconds so VARCHAR 5 is the max needed for the string 0.xxx
    DECLARE @milliseconds VARCHAR(5) = CAST(@time - ROUND(@time, 0, 1) AS VARCHAR(5))
    RETURN IIF
    (
        @year IS NULL
        OR @month IS NULL
        OR @day IS NULL,
        NULL,
        CAST
        (
            IIF
            (
                @time IS NULL,          
                @paddedYear
                + @paddedMonth
                + @paddedDay,
                @paddedYear
                + @paddedMonth
                + @paddedDay
                + ' '
                --In ROUND, the final argument of 1 forces ROUND to always round down (desired here).
                + REPLACE(STR(@hours, 2), SPACE(1), '0')
                + ':'
                + REPLACE(STR(@hoursAndMinutes - @hours * 100, 2), SPACE(1), '0')
                + ':'
                + REPLACE(STR(@hoursMinutesAndSeconds - @hoursAndMinutes * 100, 2), SPACE(1), '0')
                + IIF
                (
                    @time <> @hoursMinutesAndSeconds,
                    '.'
                    + SUBSTRING(@milliseconds, 3, LEN(@milliseconds) - 2),
                    ''
                )
            )
            AS DATETIME
        )
    )
END

However, this doesn't particularly help either as I can't seem to get Entity Framework to allow me to call it! At the moment the only viable option I can see is to try to move my entire query (much bigger than shown here) into the database as a Stored Procedure. Is there another way or is this what I'm going to be forced to do?

Matt Arnold
  • 668
  • 2
  • 8
  • 21
  • 1
    EF6 or EF Core? If EF6, are you allowed to use (free) 3rd party packages? Like [EntityFramework.Functions](https://github.com/Dixin/EntityFramework.Functions) which allows you to map a database scalar function and use it inside a LINQ to Entities query. – Ivan Stoev Feb 14 '19 at 11:50
  • It's EF6 I'm using; using free 3rd party packages shouldn't be a problem so I'll give that one a try, thanks. – Matt Arnold Feb 14 '19 at 12:01
  • To get the expression, you may need to use `AsQueryable()`. See [this answer](https://stackoverflow.com/a/22048912/3390788), as well as the other explanations provided there. – Frank Alvaro Feb 14 '19 at 12:15
  • 1
    @IvanStoev I installed the NuGet package for EntityFramework.Functions and subsequently was able to import CreateDateTime using "Update Model from Database..." but it doesn't appear in the Model Browser under Function Imports. If you could post an answer of how to use it I'd be happy to upvote it. – Matt Arnold Feb 14 '19 at 12:19
  • 1
    Ah, sorry - this package is for Code First. I'm not quite familiar with Database First (edmx), I guess it should be possible, just don't know how. – Ivan Stoev Feb 14 '19 at 13:07
  • I'm looking at using `[ComposableScalarValuedFunction("CreateDateTime", Schema = "dbo")]` on a `CreateDateTime` method I've put in a partial class of the `DbContext` - hopefully this will map to the database but I'm not sure how it will be composable in the EF query in the way I'm using it. I'll post an answer if it works. – Matt Arnold Feb 14 '19 at 13:19
  • @IvanStoev Don't suppose you've got an example of an EF method call that successfully calls a database scalar function? All I keep getting is: "System.NotSupportedException: LINQ to Entities does not recognize the method 'System.DateTime CreateDateTime(Int16, Int16, Int16, Single)' method, and this method cannot be translated into a store expression." whenever I call it. I thinking this should be the same regardless of whether it's for Code-First or Database-First EF. – Matt Arnold Feb 14 '19 at 14:13
  • Storing datetime in the proper data type (Which is DateTime2 since SQL Server 2008 version) would save you all these trouble... why didn't you do that in the first place? – Zohar Peled Feb 14 '19 at 15:54
  • Are you referring to the DateTime data in `Logs` or `SuperLogs`? If `Logs`, the reason is that it's come from some hardware which holds the data in this format and, for a business reason (which makes no sense to me either!), it must remain in this format once in the database. If `SuperLogs`, how would this help the situation as the DateTime data from `Logs` would still need to be converted into a `DateTime2` to be compared with the `EndTime` wouldn't it? I would like to change the database structure but if I do, it has implications on several platforms which depend on it. – Matt Arnold Feb 14 '19 at 16:19
  • @MattArnold Absolutely. The latest was here [Bit-Bool on a Entity Framework Scalar Function Throws 'cannot be translated' Exception](https://stackoverflow.com/questions/54579740/bit-bool-on-a-entity-framework-scalar-function-throws-cannot-be-translated-exc/54651622#54651622). They must be marked as [ComposableScalarValuedFunction](https://weblogs.asp.net/Dixin/EntityFramework.Functions#Scalar-valued_function,_composable) in order to be usable in L2E queries. – Ivan Stoev Feb 14 '19 at 17:17
  • The difference between datetime and datetime2 is not that big - datetime2 has a lower storage cost and also handles odbc datetime string format (`yyyy-mm-dd hh:mm:ss`) better than datetime (the later depends on current culture when casting to date date type). I'm talking about the `SuperLogs.Logs`. Assuming sql server version 2012 or higher, you should be able to use some math and datetimefromparts to convert the year, month, day, time values to datetime. – Zohar Peled Feb 15 '19 at 13:46
  • When you say about converting the values to datetime, do you mean on read-back or as a script to run against the existing data? If the latter, it's something I'm not authorized to do and would also result in a lot of refactoring of our Data Access Layer to accommodate the legacy format and the new one. If the former, is there a way I can call it from a LINQ-to-Entities Expression (as otherwise I'm back to the same problem of not being able to call `GetLogDateTime`)? – Matt Arnold Feb 15 '19 at 14:38

0 Answers0