Thursday, March 10, 2022

Custom EF Core Function to Use Transact-SQL AT TIME ZONE

In this post, let's see how we can write a Custom EF Core Function to Use Transact-SQL AT TIME ZONE.

Consider the below example. I have the following Order entity in my DbContext.

public class Order
{
    public int Id { getset; }
 
    public DateTimeOffset OrderDate { getset; }
}

Now I have the following Extension method to Convert a given DateTimeOffset to a given TimeZone.

public static class DateTimeExtensions
{
    public static DateTimeOffset ConvertToTimeZone(this DateTimeOffset dateTimeOffsetstring timeZone)
    {
        TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
        DateTime datetime = TimeZoneInfo.ConvertTimeFromUtc(dateTimeOffset.UtcDateTime, timeZoneInfo);
 
        return new DateTimeOffset(datetime, timeZoneInfo.GetUtcOffset(datetime));
    }
}

Now consider the below query.

var timeZone = "Eastern Standard Time";
 
var orders = await context.Orders
    .Select(x => new
    {
        OrderDate = x.OrderDate,
        ConvertedOrderDate = x.OrderDate.ConvertToTimeZone(timeZone)
    })
    .ToListAsync();

Note for ConvertedOrderDate, I am using the above CLR extension method. 

And when I run this query, the generated SQL statement is going to look like below.

SELECT [o].[OrderDate]
FROM [Orders] AS [o]

And with the below result.

OrderDate                     | ConvertedOrderDate
------------------------------| ------------------------------
20/02/2022 12:00:00 am +00:00 | 19/02/2022 7:00:00 pm -05:00

In the SQL statements EF generated, you can see it doesn't contain anything on ConvertedOrderDate or TimeZone conversion. That's because EF can't translate my CLR extension method into SQL statements. And if we have used this extension method in a Where statement (instead of Select), EF is going to loudly throw an error saying "Translation of method 'DateTimeExtensions.ConvertToTimeZone' failed". So here basically the TimeZone conversion was done In Memory.

And for these kinds of scenarios, we can use this feature in EF, where we can say how a method should get translated into SQL.

First, I am creating a static class with the following method.

public static class EfFunctions
{
    public static DateTimeOffset ConvertToTimeZone(DateTimeOffset dateTimeOffset, [NotParameterized] string timeZone)
        => throw new NotImplementedException("This method should be implemented in EF.");
}

We don't provide any implementation here.

Now we need to instruct EF on how to translate this method.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    MethodInfo methodInfo = typeof(EfFunctions).GetMethod(nameof(EfFunctions.ConvertToTimeZone));
    modelBuilder
        .HasDbFunction(methodInfo)
        .HasTranslation(x =>
        {
            ColumnExpression columnExpression = x.First() as ColumnExpression;
 
            SqlConstantExpression timeZoneExpression = x.Skip(1).First() as SqlConstantExpression;
 
            string timeZoneLiteralValue = timeZoneExpression.TypeMapping.GenerateSqlLiteral(timeZoneExpression.Value);
 
            SqlFragmentExpression valueExpression =
                new($"{columnExpression.Table.Alias}.{columnExpression.Name} AT TIME ZONE {timeZoneLiteralValue} AS DATETIMEOFFSET");
 
            return new SqlFunctionExpression(
                "CAST",
                new List<SqlExpression>() { valueExpression },
                false,
                new List<bool>() { falsefalsefalse },
                typeof(DateTimeOffset),
                null
            );
    });
}

Here I am using Transact-SQL CAST and AT TIME ZONE to generate the proper SQL statements for TimeZone conversion.

And now I am changing my query as follows.

var timeZone = "Eastern Standard Time";
 
var orders = await context.Orders
    .Select(x => new
    {
        OrderDate = x.OrderDate,
        ConvertedOrderDate = EfFunctions.ConvertToTimeZone(x.OrderDate, timeZone)
    })
    .ToListAsync();
This time, for ConvertedOrderDate, I am using the EF Function we just wrote. 

And now when we execute the query, EF is creating the following SQL statement.
SELECT [o].[OrderDate], CAST(o.OrderDate AT TIME ZONE N'Eastern Standard Time' AS DATETIMEOFFSET) AS [ConvertedOrderDate]
FROM [Orders] AS [o]
With the following result as above.
OrderDate                     | ConvertedOrderDate
------------------------------| ------------------------------
20/02/2022 12:00:00 am +00:00 | 19/02/2022 7:00:00 pm -05:00
Hope this helps.


Happy Coding.

Regards,
Jaliya

No comments:

Post a Comment