Thursday, May 12, 2022

.NET 7 Preview 4: Introducing Self-describing Support for Minimal APIs in ASP.NET Core

.NET 7 Preview 4 is released and it includes some nice features related to ASP.NET Core Minimal APIs. One of them is the support for self-describing API endpoints. 

In this post, let's have a look at how it works.

Consider the below Minimal API endpoints prior to .NET 7 Preview 4.

app.MapGet("/employees"async (EmployeeContext dbContext) =>
{
    return Results.Ok(await dbContext.Employees.ToListAsync());
});

Now if we have a look at the Swagger document, I can see something like this.

GET: /employees
It only says the endpoint returns 200, but nothing about the response type.

Let's have a look at another example. Consider the below endpoint.

app.MapGet("/employees/{id}"async (int id, EmployeeContext dbContext) =>
{
    Employee employee = await dbContext.Employees.FindAsync(id);
    if (employee is null)
    {
        return Results.NotFound();
    }
 
    return Results.Ok(employee);
});

And this would appear in the Swagger document as follows.

GET: /employees/{id}
Again nothing about the Response Type, and obviously no sign about the endpoint returning 404.
 
If we are to enrich these missing details, we will have to update the code as follows.

app
    .MapGet("/employees"async (EmployeeContext dbContext) =>
    {
        return Results.Ok(await dbContext.Employees.ToListAsync());
    })
    .Produces<List<Employee>>();
 
app
    .MapGet("/employees/{id}"async (int id, EmployeeContext dbContext) =>
    {
        Employee employee = await dbContext.Employees.FindAsync(id);
        if (employee is null)
        {
            return Results.NotFound();
        }
 
        return Results.Ok(employee);
    })
    .Produces<Employee>()
    .Produces(StatusCodes.Status404NotFound);

And now we can see the Swagger document is updated.

GET: /employees
GET: /employees/{id}
But what if we can let the APIs describe themselves without adding additional annotations.

With .NET 7 Preview 4, I can change the above endpoints as follows.

app.MapGet("/employees"async (EmployeeContext dbContext) =>
{
    return TypedResults.Ok(await dbContext.Employees.ToListAsync());
});

This will describe the endpoint the same way it did with annotations. 

The only change I did here is use the new TypedResults factory class instead of Results factory class when generating the result. The new TypedResults factory class will create Typed results (as the name suggests of course) instead of IResult as it did with Results factory class. And all these Typed results implement a new interface IEndpointMetadataProvider.

public interface IEndpointMetadataProvider
{
    static abstract void PopulateMetadata(EndpointMetadataContext context);
}

The framework will call PopulateMetadata() when the endpoint is built and that adds the necessary endpoint metadata to describe the HTTP response type.

Now when we have multiple return types, we need to explicitly specify the return types as follows.

app.MapGet("/employees/{id}"async Task<Results<Ok<Employee>, NotFound>> (int id, EmployeeContext dbContext) =>
{
    Employee employee = await dbContext.Employees.FindAsync(id);
    if (employee is null)
    {
        return TypedResults.NotFound();
    }
 
    return TypedResults.Ok(employee);
});

And this also will describe the endpoint the same way it did with annotations. 

You can find the complete sample code here.
   https://github.com/jaliyaudagedara/minimal-api

More Read
   ASP.NET Core updates in .NET 7 Preview 4

Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, May 11, 2022

CoreWCF Is Released

CoreWCF, the .NET Core version of Windows Communication Foundation is finally released. The 1.0 release of CoreWCF is compatible with .NET Standard 2.0 so that it will work with,
  • .NET Framework 4.6.2 (and above)
  • .NET Core 3.1
  • .NET 5+
In this post, let's have a look at a sample implementation of WCF on top of .NET 6.

I have an ASP.NET Core Web API (with Minimal API support) created and installed the following packages.
<ItemGroup>
  <PackageReference Include="CoreWCF.Http" Version="1.0.1" />
  <PackageReference Include="CoreWCF.Primitives" Version="1.0.1" />
</ItemGroup>
Then I created the following services. I am exposing two services as I want to show the support for different Bindings.

GreetService.cs
[ServiceContract]
public interface IGreetService
{
    [OperationContract]
    string Greet(string message);
}
 
public class GreetService : IGreetService
{
    public string Greet(string message)
    {
        return $"You said: {message}";
    }
}
AnotherGreetService.cs
[ServiceContract]
public interface IAnotherGreetService
{
    [OperationContract]
    string AnotherGreet(string message);
}
 
public class AnotherGreetService : IAnotherGreetService
{
    public string AnotherGreet(string message)
    {
        return $"You said another: {message}";
    }
}
Now I am modifying the Startup.cs as follows.

Startup.cs
using CoreWCF;
using CoreWCF.Configuration;
using CoreWCF.Description;
using CoreWcfDemo.Server.Services;
 
var builder = WebApplication.CreateBuilder(args);
 
// Add WSDL support
builder.Services.AddServiceModelServices().AddServiceModelMetadata();
builder.Services.AddSingleton<IServiceBehavior, UseRequestHeadersForMetadataAddressBehavior>();
 
WebApplication? app = builder.Build();
app.UseServiceModel(builder =>
{
    // This service only supports BasicHttpBinding
    builder
        .AddService<GreetService>()
        .AddServiceEndpoint<GreetService, IGreetService>(new BasicHttpBinding(), 
            "/GreetService/BasicHttp");
 
    // This service supports BasicHttpBinding and WSHttpBinding
    builder
        .AddService<AnotherGreetService>()
        .AddServiceEndpoint<AnotherGreetService, IAnotherGreetService>(new BasicHttpBinding(), 
            "/AnotherGreetService/BasicHttp")
        .AddServiceEndpoint<AnotherGreetService, IAnotherGreetService>(new WSHttpBinding(SecurityMode.Transport), 
            "/AnotherGreetService/WSHttps");
});
 
var serviceMetadataBehavior = app.Services.GetRequiredService<ServiceMetadataBehavior>();
 
serviceMetadataBehavior.HttpGetEnabled = true;
serviceMetadataBehavior.HttpsGetEnabled = true;
 
serviceMetadataBehavior.HttpGetUrl = new Uri("http://localhost:5051/metadata");
serviceMetadataBehavior.HttpsGetUrl = new Uri("https://localhost:7051/metadata");
 
app.Run();
Now, I am adding a Console Application and adding Service References to the project.
Add Service Reference
Add Service Reference: WCF Web Service
Discover Services
Once the Service is discovered, I have selected Next and opted to use the defaults. And once the Service Reference is created, I have the following code to call the different WCF Service methods using different bindings.
using ServiceReference1;
 
// BasicHttpsBinding
var greetServiceClient = new GreetServiceClient(
    GreetServiceClient.EndpointConfiguration.BasicHttpBinding_IGreetService,
    "http://localhost:5051/GreetService/BasicHttp"
);
var result = await greetServiceClient.GreetAsync("Hello");
Console.WriteLine(result);
 
// WSHttpBinding
var anotherGreetServiceClient = new AnotherGreetServiceClient(
    AnotherGreetServiceClient.EndpointConfiguration.WSHttpBinding_IAnotherGreetService,
    "https://localhost:7051/AnotherGreetService/WSHttps"
);
result = await anotherGreetServiceClient.AnotherGreetAsync("Hello");
Console.WriteLine(result);
 
Console.ReadLine();
And now when I run the Console App while the Server App is running, I can see everything is working as expected.

You can find the complete code sample here:
   https://github.com/jaliyaudagedara/corewcf-demo

Hope this helps.

Happy Coding.

Regards,
Jaliya

Sunday, May 1, 2022

Visual Studio 2022: Temporary Breakpoints

This is a quick post on a nice feature that got introduced in Visual Studio 2022.

Have you faced this scenario where you have set multiple breakpoints in your code while debugging, and the next time you are running the application locally (maybe after fixing the issues or you are done with the debugging), you keep getting hit on all those breakpoints back to back?

Most of the time, the breakpoints we are adding are temporary and only needed for that particular session. Visual Studio now lets you add Temporary Breakpoints and once it's hit, it's gone.

Insert Temporary Breakpoint
You can also simply use the shortcut F9 + Shift + Alt, T and set the temporary breakpoint on the line desired.

Hope this helps!

Happy Coding.

Regards,
Jaliya

Tuesday, April 26, 2022

C# 11.0: Raw String Literals

In this post, let's have a look at one of the nicest features coming in C# 11.0. And this is one of my favorites.

You can also try out this feature now by setting the LangVersion to preview in your .csproj file.
<LangVersion>preview</LangVersion>
Say you need to declare a variable with a JSON string, something like this.
{
  "name""John Doe",
  "address": {
    "addressLine1""Address Line 1",
    "addressLine2""Address Line 2",
    "city""City",
    "state""State",
    "postalCode""12345-6789",
    "country""Country"
  }
}
And prior to C# 11.0, in order to get this into a variable, we need to modify the JSON string to escape the double-quotes.
string jsonString =
    @"{
    ""name"": ""John Doe"",
    ""address"": {
        ""addressLine1"": ""Address Line 1"",
        ""addressLine2"": ""Address Line 2"",
        ""city"": ""City"",
        ""state"": ""State"",
        ""postalCode"": ""12345-6789"",
        ""country"": ""Country""
    }
}";
And now say you want to use string interpolation for some of the values. And for that you need to escape the curly braces, something like this.
string name = "John Doe";
string jsonString =
    @$"{{
    ""name"": ""{name}"",
    ""address"": {{
        ""addressLine1"": ""Address Line 1"",
        ""addressLine2"": ""Address Line 2"",
        ""city"": ""City"",
        ""state"": ""State"",
        ""postalCode"": ""12345-6789"",
        ""country"": ""Country""
    }}
}}";
And that's a lot of work.

With Raw String Literals in C# 11.0, you can do something like below.
string name = "John Doe";
string jsonString =
    $$"""
    {
        "name": "{{name}}",
        "address": {
            "addressLine1": "Address Line 1",
            "addressLine2": "Address Line 2",
            "city": "City",
            "state": "State",
            "postalCode": "12345-6789",
            "country": "Country"
        }
    }
    """;
And note, here I didn't escape double quotes nor the curly braces. I only had to change the value of the name property to use the string interpolation. So basically it's just copying and pasting the JSON as it is and doing a minor change if we are using string interpolation which we will have to do anyway.

A couple of important notes here:
  • Raw string literals start and end with at least three double-quotes.
string jsonString =
    """
    {
        "name": "John Doe",
    }
    """;
  • Within these double quotes, single " are considered content and included in the string
  • Any number of double quotes less than the number that opened the raw string literal are treated as content. So, in the common case of three double quotes opening the raw string literals, two double quotes appearing together would just be content.
  • If you need to output a sequence of three or more double-quotes, then open and close the raw string literal with at least one more quote than that sequence, something like below.
string jsonString =
    """"
    {
        "name": "John Doe",
        "description": "Some Description with """ quotes "
    }
    """";
  • Raw string literals can be interpolated by preceding them with a $. The number of $ that prefixes the string is the number of curly brackets that are required to indicate a nested code expression.
string name = "John Doe";
string someString = $""" His name is "{name}".""";
  • If a raw string literal is prefixed with $$, a single curly bracket is treated as content and it takes two curly brackets to indicate nested code (as shown in the jsonString with string interpolation code snippet above). Just like with quotes, you can add more $ to allow more curly brackets to be treated as content.
Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, April 20, 2022

C# 11.0: Parameter Null Checking (Revisited)

C# 11.0 was initially planned to have the bang-bang operator for improved Parameter Null Checking. If you don't know what that is, I have written this post a while back.
   C# 11.0 Preview: Parameter Null Checking

But unfortunately, the C# Lang team has deferred this feature and we won't be having this feature in C# 11.0. 

With C# 10.0 and .NET 6, we still have the ArgumentNullException.ThrowIfNull method which is the recommended/preferred approach for going forward. So while we are here, maybe it's worth having a closer look at ArgumentNullException.ThrowIfNull.

Basically, with this feature syntax is something like below.
static void PrintFullName(Person person)
{
    ArgumentNullException.ThrowIfNull(person);
 
    Console.WriteLine($"FullName: {person.FirstName} {person.LastName}");
}
Now If I call this method supplying a null value, I am going to get an exception something like below.
Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'person')
   at System.ArgumentNullException.Throw(String paramName)
   at System.ArgumentNullException.ThrowIfNull(Object argument, String paramName)
   at Program.<<Main>$>g__PrintFullName|0_0(Person person) in C:\Users\Jaliya\Desktop\ConsoleApp1\ConsoleApp1\Program.cs:line 3
   at Program.<Main>$(String[] args) in C:\Users\Jaliya\Desktop\ConsoleApp1\ConsoleApp1\Program.cs:line 8

So here in the exception, we have the following details.

And these Caller Info attributes were introduced in C# 5.0 back in 2012.

If you notice the exception, you should see we also have the name of the parameter in the exception. But when doing ArgumentNullException.ThrowIfNull, we haven't included the parameter name. So what happened here?

Enter CallerArgumentExpressionAttribute.

This new attribute was introduced with C# 10.0, and this allows us to capture the expressions passed to a method. If you examine the ArgumentNullException.ThrowIfNull method, you will see it's using this new attribute as below and that's how we are getting parameter name in the exception.

public static void ThrowIfNull([NotNull] objectargument, [CallerArgumentExpression("argument")] stringparamName = null);

Reference: ArgumentNullException.cs

Let's consider the below code.

static void PrintFullName(Person person)
{
    ThrowIfNull(person);
}
 
static void ThrowIfNull(object argument, [CallerArgumentExpression("argument")] stringexpression = default)
{
    if (argument is null)
    {
        throw new ArgumentNullException(expression);
    }
 
    Console.WriteLine($"Expression: {expression}");
}

Now if I call these methods as follows, I am getting these outputs.

PrintFullName(new Person("John""Doe"));
// Expression: person
ThrowIfNull(new Person("John""Doe")); // Expression: new Person("John", "Doe")
PrintFullName(null);
// System.ArgumentNullException: Value cannot be null. (Parameter 'person')
ThrowIfNull(null);
// System.ArgumentNullException: Value cannot be null. (Parameter 'null')

Hope this helps.

Happy Coding.

Regards,
Jaliya

Friday, April 15, 2022

ASP.NET Core: Custom Controller Action Parameter Binding using TryParse in Minimal APIs

.NET 7 Preview 3 is out and in this post let's see how we can customize Controller Action parameter binding using TryParse in Minimal APIs.

Let's consider the following code.

// /employees/search?searchCriteria={"name":"John"}
app.MapGet("/employees/search",
    async (EmployeeContext dbContext,
        EmployeeSearchCriteria searchCriteria,
        CancellationToken cancellationToken) =>
{
    return await dbContext.Employees
        .Where(x => x.Name == searchCriteria.Name)
        .ToListAsync(cancellationToken);
})
.Produces<List<Employee>>(StatusCodes.Status200OK);
 
public class EmployeeSearchCriteria
{
    public string Name getset; }
}

Here let's say, I want to bind the query parameter searchCriteria that I am sending to the searchCriteria object in the action. Here above code won't work, because the runtime has no knowledge of translating the query parameter to the searchCriteria objectWe can instruct the runtime on how it should get translated by using TryParse.

I am updating the EmployeeSearchCriteria class by introducing the following TryParse method.

public class EmployeeSearchCriteria
{
    public string Name { getset; }
 
    public static bool TryParse(string valueout EmployeeSearchCriteria result)
    {
        if (value is null)
        {
            result = default;
            return false;
        }
 
        JsonSerializerOptions options = new()
        {
            PropertyNameCaseInsensitive = true
        };
        result = JsonSerializer.Deserialize<EmployeeSearchCriteria>(value, options);
 
        return true;
    }
}

ASP.NET Core will look for a TryParse method in a complex object parameter when trying to do the parameter binding. The TryPrase method signature should be one of the following.

public static bool TryParse(string value, T out result);
public static bool TryParse(string value, IFormatProvider provider, T out result);

Now moving back to the code sample, if I run the updated code, I can see the query parameter searchCriteria is correctly bound to the object.

Customize Action Parameter Binding using TryParse
Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, April 6, 2022

In-Process Azure Function, EF Core Logging using ILogger

In this post, let's see how we can configure EF Core logging using ILogger in an In-Process Azure Function.

First, I am registering my DbContext in the Startup.cs as follows.

Startup.cs

[assembly: FunctionsStartup(typeof(FunctionApp1.Startup))]
namespace FunctionApp1;
 
public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        IConfiguration configuration = builder.Services.BuildServiceProvider().GetService<IConfiguration>();
 
        builder.Services.AddDbContext<MyDbContext>(options =>
            options
                .UseSqlServer(configuration.GetValue<string>("MyDbContext_ConnectionString")));
    }
}

Then in my DbContext, I am overriding the OnConfiguring method as follows.

MyDbContext.cs

public class MyDbContext : DbContext
{
    private readonly IHostingEnvironment _hostingEnvironment;
    private readonly ILogger<MyDbContext> _logger;
 
    public MyDbContext(DbContextOptions options, 
        IHostingEnvironment hostingEnvironment, 
        ILogger<MyDbContext> logger) : base(options)
    {
        _hostingEnvironment = hostingEnvironment;
        _logger = logger;
    }
 
    public DbSet<Employee> Employees { getset; }
 
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        // Only logging in Development environment to avoid unnecessary noice in Production Environment
        if (_hostingEnvironment.IsDevelopment())
        {
            optionsBuilder
                .LogTo(action =>
                {
                    _logger.LogInformation(action);
                    // TODO: Customize logging, use any LogTo Overload
                });
        }
    }
}

Here I am using DbContextOptionsBuilder.LogTo Method which was introduced in EF Core 5.0.

And now I am invoking the following function to generate some logs.

public class Function1
{
    private readonly MyDbContext _myDbContext;
 
    public Function1(MyDbContext myDbContext)
    {
        _myDbContext = myDbContext;
    }
 
    [FunctionName("Employees")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest request)
    {
        string requestBody = await new StreamReader(request.Body).ReadToEndAsync();
        dynamic data = JsonConvert.DeserializeObject(requestBody);
        string name = data?.name;
 
        Employee employee = new()
        {
            Name = name
        };
        await _myDbContext.Employees.AddAsync(employee);
        await _myDbContext.SaveChangesAsync();
 
        return new OkObjectResult(employee);
    }
}

And now I can see EF Core Logging is getting triggered.

I am using App Insights, and I can see the logs there.

Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, March 30, 2022

Enabling Tab Completion for .NET CLI in PowerShell

In this post, let's see how we can enable Tab Completion for .NET CLI commands in PowerShell. By default, Tab completion doesn't work for .NET CLI commands.

As you can see in the below image, I am trying tab-completion after typing some letters and it doesn't resolve me the available commands in .NET CLI. Not so much of a friendly experience.
PowerShell: .NET CLI Tab Completion Does Not Work
But we can get this feature enabled in like 2 steps.

First, run the following command.
# Assuming you have VS Code
code $PROFILE
 
# If you don't have VS Code
notepad $PROFILE

Now update your PowerShell profile by adding the following code snippet.

Register-ArgumentCompleter -Native -CommandName dotnet -ScriptBlock {
    param($commandName, $wordToComplete, $cursorPosition)
    dotnet complete --position $cursorPosition "$wordToComplete" | ForEach-Object {
        [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue'$_)
    }
}
Save the file, close PowerShell, and open it back up. Now as you can see here, tab completion is working nicely.
PowerShell: .NET CLI Tab Completion in Action
If this doesn't work, try running the following command and ensure it works.
dotnet complete "dotnet *"

dotnet complete
If this doesn't work, make sure that .NET Core 2.0 SDK or above is installed and dotnet --version command resolves to a version of .NET Core 2.0 SDK and above.

If you want to enable .NET CLI tab completion for other Shells like bash, read more here.

Hope this helps.

Happy Coding.

Regards,
Jaliya

Saturday, March 12, 2022

Azure App Service Supports Windows Containers up to Windows Server Core 2019 and Nano Server 1809

Did you know that as of today, Windows Containers on Azure App Service only supports up to Windows Server Core 2019 and Nano Server 1809?

We were having a Windows Container App running on Azure App Service and recently our deployments started failing with the error" UnsupportedMediaType - The parameter WindowsFxVersion has an invalid value. Cannot run the specified image as a Windows Containers Web App. App Service supports Windows Containers up to Windows Server Core 2019 and Nanoserver 1809. Platform of the specified image: windows, Version: 10.0.20348.587; (CODE: 415)".

Our docker image was based on mcr.microsoft.com/dotnet/aspnet:6.0. Upon inspecting mcr.microsoft.com/dotnet/aspnet:6.0, it's now using Windows OS: 10.0.20348.587 (when it's built on Windows). The change was made on 2022-03-08, likely with the announcement of .NET 6.0.3.

{
    "RepoTags": [
        "mcr.microsoft.com/dotnet/aspnet:6.0"
    ],
    "Created""2022-03-08T18:56:16.0190214Z",
    "Os""windows",
    "OsVersion""10.0.20348.587",
    "..."
}

So to get the things back to work, I had to change the base image to mcr.microsoft.com/dotnet/aspnet:6.0.3-nanoserver-1809 which will enforce underline OS to be Nano Server 1809

While we are here, it's worth specifying the difference between Nano Server and Server Core, so you can choose an appropriate base image that works for you.

  • Nano Server is an ultralight Windows offering for new application development.
  • Server Core is medium in size and a good option for "lifting and shifting" Windows Server apps.

It's always a good practice to be explicit and knowingly update as we go rather than using generic mcr.microsoft.com/dotnet/aspnet:6.0 as Microsoft can update the underline OS anytime.

Hope this helps.

Happy Coding.

Regards,
Jaliya

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