Sunday, June 22, 2025

Super Simple .NET Run Program.cs

From .NET 10 Preview 4 onwards, we can run individual .NET Code files without any ceremony just like you would do in Python etc.

For example, I can just create a .cs file (HelloWorld.cs) with some C# code.
Console.WriteLine("Hello World!");
And then do the following.
dotnet run HelloWorld.cs
dotnet run HelloWorld.cs
If we need to use external packages within our code, we can do something like this. Here we don't have a .csproj file, so we can do the package reference inline. For demo purposes, I am using  Humanizer package.
// Reference the Humanizer package
#:package Humanizer@2.*

// Using directive
using Humanizer;

// Using the package
Console.WriteLine("Hello World!".Humanize(LetterCasing.AllCaps));
dotnet run HelloWorld.cs
That's neat!

Happy Coding.

Regards,
Jaliya

Thursday, June 19, 2025

Running Python Code within .NET Projects

Back home now after what was meant to be a relaxing holiday - unfortunately, it took quite a turn with some unexpected hospital stays both overseas and after coming home.

Started catching up and there has been some mind blowing announcements in the world of Software Development with AI.

For ML and AI, I think we all agree that Python is the go to programming language. Might change in the future, but at least currently that's the main one.

I recently had a requirement where we want to use a functionality that is written on Python to be consumed by a .NET application. The way I thought is to expose a Python API (using Flask, FastAPI etc), but then there is going to be a lot of data (floats) travelling through HTTP. Since I didn't have any other option, started on that.

And then on Microsoft Build 2025, there was this cool session on Python Meets .NET with Anthony Shaw and Scott Hanselman. It's a new project called CSnakes, where we could use Python using .NET and most importantly that's not by emulating or simulating Python. It's embedding Python into .NET process. .NET and Python code shares the same threads and same memory etc.
CSnakes
Let's have a look at a basic working example.

Here I have a basic Python function in a file called hello_world.py.
def say_hello(name: str) -> str:
    return f"Hello {name}!"
I have now created a Console Application that targets .NET 9 and installed CSnakes.Runtime package.
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="CSnakes.Runtime" Version="1.0.34" />
  </ItemGroup>

</Project>
I am putting the hello_world.py inside the project and updating .csproj file to copy the .py to Output directory. And this is a very important step, this enables CSnakes to run the source generator over Python files.
<ItemGroup>
  <AdditionalFiles Include="hello_world.py">
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </AdditionalFiles>
</ItemGroup>
And now updating the Program.cs as follows.
using CSnakes.Runtime;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

IHostBuilder builder = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        // Path to your Python modules
        var home = Path.Join(Environment.CurrentDirectory, ".")
        services
            .WithPython()
            .WithHome(home)
            .FromRedistributable()// Download Python 3.12 and store it locally
    });

IHost app = builder.Build();

IPythonEnvironment pythonEnvironment =  app.Services.GetRequiredService<IPythonEnvironment>();

// IMPORTANT: Source generated by CSnakes
IHelloWorld helloWorld = pythonEnvironment.HelloWorld();
string result = helloWorld.SayHello("John Doe");
Console.WriteLine(result)// Hello John Doe!
Output
If you are wondering how IHelloWorld comes into the picture, it was generated at compile time. 
Generated Source
While it's the recommended approach, you can still use CSnakes without source generator.

Now let's see how an example of how to use a Python file that uses packages. We can make use of Python pip install and  requirements.txt .

First I am adding a requirements.txt file and enable copying it to the output.
<ItemGroup>
<AdditionalFiles Include="requirements.txt">
   <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </AdditionalFiles>
</ItemGroup>
Here for the demo purposes I am adding a simple package stringcase.
stringcase
Now I am modifying the hello_world.py file as follows.
import stringcase

def say_hello(name: str) -> str:
    return f"Hello {stringcase.titlecase(name)}!"
Now I am updating Program.cs the as follows.
using CSnakes.Runtime;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

IHostBuilder builder = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        // Path to your Python modules
        var home = Path.Join(Environment.CurrentDirectory, ".");
        services
            .WithPython()
            .WithHome(home)
            .FromRedistributable() // Download Python 3.12 and store it locally
            .WithVirtualEnvironment(Path.Join(home".venv"))
            .WithPipInstaller()// Optional, Installs packages listed in requirements.txt on startup
    });

IHost app = builder.Build();

IPythonEnvironment pythonEnvironment =
    app.Services.GetRequiredService<IPythonEnvironment>();

// Source generated by CSnakes
IHelloWorld helloWorld = pythonEnvironment.HelloWorld();
string result = helloWorld.SayHello("JohnDoe");
Console.WriteLine(result)// Hello John Doe!
Output
This is pretty cool!

Watch the video:

Happy Coding.

Regards,
Jaliya

Tuesday, May 6, 2025

ASP.NET Core 9.x: Support for Polymorphic Types with [FromForm]

I recently wanted to have an ASP.NET Core Web API endpoint, that accepts data via [FromForm] and the payload contains a Polymorphic Type.

For an example consider the following POCO types that the endpoint expects via [FromForm].

[JsonDerivedType(typeof(ClassificationOptions)nameof(ClassificationOptions))]
[JsonDerivedType(typeof(ExtractionOptions)nameof(ExtractionOptions))]
public class AnalyzeOptions
{
    public string? CommonOption1 { getset} = null;
}

public class ClassificationOptions : AnalyzeOptions
{
    public stringClassificationOption1 { getset} = null;
}

public class ExtractionOptions : AnalyzeOptions
{
    public string? ExtractionOption1 { getset} = null;
}

public class AnalyzeRequestModel
{
    public IFormFileCollection Files { getset}

    public AnalyzeOptions Options { getset}
}

And the endpoint looks like below:

[ApiController]
[Route("[controller]")]
public class ValuesController : ControllerBase
{
    [HttpPost("FromForm", Name = "PostFromForm")]
    public IActionResult PostFromForm([FromForm] AnalyzeRequestModel analyzeRequestModel)
    {
        // TODO: Process the files and options

        return Ok(analyzeRequestModel);
    }
}

And I was sending data as below, hoping System.Text.Json will kick in and do the deserialization as expected.

Postman
Not deserialized

But it seems [FromForm] model binding isn't honoring System.Text.Json support for Polymorphic Types which is working perfectly fine with [FromBody].

So in order to get this working, we can write a custom model binder for AnalyzeOptions.

public class AnalyzeOptionsModelBinder : IModelBinder
{
    private Dictionary<Type(ModelMetadataIModelBinder)> binders;

    public AnalyzeOptionsModelBinder(Dictionary<Type(ModelMetadataIModelBinder)> binders)
    {
        this.binders = binders;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        string modelKindName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, "$type");
        stringmodelTypeValue = bindingContext.ValueProvider.GetValue(modelKindName).FirstValue;

        IModelBinder modelBinder;
        ModelMetadata modelMetadata;
        if (modelTypeValue == nameof(ClassificationOptions))
        {
            (modelMetadatamodelBinder) = binders[typeof(ClassificationOptions)];
        }
        else if (modelTypeValue == nameof(ExtractionOptions))
        {
            (modelMetadatamodelBinder) = binders[typeof(ExtractionOptions)];
        }
        else
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return;
        }

        ModelBindingContext newBindingContext = DefaultModelBindingContext.CreateBindingContext(
            bindingContext.ActionContext,
            bindingContext.ValueProvider,
            modelMetadata,
            bindingInfonull,
            bindingContext.ModelName);

        await modelBinder.BindModelAsync(newBindingContext);
        bindingContext.Result = newBindingContext.Result;

        if (newBindingContext.Result.IsModelSet)
        {
            // Setting the ValidationState ensures properties on derived types are correctly 
            bindingContext.ValidationState[newBindingContext.Result.Model] = new ValidationStateEntry
            {
                Metadata = modelMetadata,
            };
        }
    }
}
public class AnalyzeOptionsModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType != typeof(AnalyzeOptions))
        {
            return null;
        }

        Type[] subclasses = [typeof(ClassificationOptions)typeof(ExtractionOptions),];

        var binders = new Dictionary<Type(ModelMetadataIModelBinder)>();
        foreach (Type type in subclasses)
        {
            ModelMetadata modelMetadata = context.MetadataProvider.GetMetadataForType(type);
            binders[type] = (modelMetadatacontext.CreateBinder(modelMetadata));
        }

        return new AnalyzeOptionsModelBinder(binders);
    }
}

And then register this custom ModelBinderProvider.

builder.Services.AddControllers(options =>
{
    // Add the custom model binder to support [FromForm] with Polymorphic types
    options.ModelBinderProviders.Insert(0, new AnalyzeOptionsModelBinderProvider());
});

And now if I execute the request again,

Custom Model Binder
And that works.

Raised an issue in ASP.NET Core repo,
   [FromForm]: Support for Polymorphic Types #61378

Hope this helps.

Happy Coding.

Regards,
Jaliya

Sunday, May 4, 2025

EF Core 10.0: Simplified LeftJoin and RightJoin

In this post, let's see a nice feature that is already available with EF Core 10.0 Preview.

For an example, consider the following DbContext Entities.

public record Customer
{
    public int Id { getset}

    public string Name { getset}
}

public record Order
{
    public int Id { getset}

    public string OrderNumber { getset}

    public Customer Customer { getset}

    public int CustomerId { getset}
}

Here, a Customer can have one or more Orders, or they even not have any Orders. Say we want to get all the Customers  and their Orders (if any).

So I want to write a LEFT JOIN query. Prior to EF Core 10.0, to write a LEFT JOIN, we needed to write a complex LINQ query, something like the following.

var query = context.Customers
    .GroupJoin(
        context.Orders,
        customer => customer.Id,
        order => order.CustomerId,
        (customerorders) => new
        {
            Customer = customer,
            Orders = orders
        })
    .SelectMany(
        x => x.Orders.DefaultIfEmpty(),
        (customerorder) => new
        {
            CustomerName = customer.Customer.Name,
            OrderNumber = order.OrderNumber ?? "N/A"
        });

foreach (var item in query)
{
    Console.WriteLine($"{item.CustomerName,-20} {item.OrderNumber}");
}

This would generate a SQL Query as follows:

SELECT [c].[Name] AS [CustomerName], COALESCE([o].[OrderNumber], N'N/A') AS [OrderNumber]
FROM [Customers] AS [c]
LEFT JOIN [Orders] AS [o] ON [c].[Id] = [o].[CustomerId]
The output would be:
Output
And with EF Core 10.0, we can rewrite the same query as follows:
var query = context.Customers
    .LeftJoin(
        context.Orders,
        customer => customer.Id,
        order => order.CustomerId,
        (customerorder) => new
        {
            CustomerName = customer.Name,
            OrderNumber = order.OrderNumber ?? "N/A"
        });

If you run this with an EF Core version prior to 10.0, you will be getting a could not be translated error). But with EF Core 10.0, this would generate the same SQL query as before and will give the same output. The same would be applicable for RIGHT JOIN.

That's neat.

Happy Coding,

Regards,
Jaliya

Saturday, May 3, 2025

ASP.NET Core 10.0: OpenAPI Document in YAML Format

ASP.NET Core 10.0, now supports OpenAPI specification in YAML format. YAML also supports multi-line strings, which can be useful for long descriptions.

To configure an app to serve the generated OpenAPI document in YAML format, specify the endpoint in the MapOpenApi call with a .yaml or .yml suffix, as shown in the following example:

app.MapOpenApi();
app.MapOpenApi("/openapi/v1.yaml");

Now we are exposing OpenAPI specification in both JSON (default) and YAML format.

OpenAPI Document in YAML Format

Hope this helps.

Happy Coding.

Regards,
Jaliya

Friday, May 2, 2025

ASP.NET Core 10.0: OpenAPI 3.1 Support

From ASP.NET Core 10.0, the default OpenAPI version would be 3.1.x. Prior to ASP.NET Core 10.0, the default OpenAPI version was 3.0.x for as long as I could remember.
OpenAPI: 3.1.x
While this looks like a minor version bump, this is a significant update with breaking changes. If you want to keep using OpenAPI version 3.0.x, you can do so as follows:
builder.Services.AddOpenApi(x =>
{
    x.OpenApiVersion = Microsoft.OpenApi.OpenApiSpecVersion.OpenApi3_0;
});
If you are generating the OpenAPI document at build time, you can add the following MSBiild item.
<OpenApiGenerateDocumentsOptions>--openapi-version OpenApi3_0</OpenApiGenerateDocumentsOptions>
Hope this helps.

Happy Coding.

Regards,
Jaliya

Thursday, April 17, 2025

C# 14.0: Introducing Extension Members and Null-Conditional Assignment

Visual Studio 17.14.0 Preview 3.0 got released earlier today and now you can try out two nice C# 14.0 features that got released with .NET 10 Preview 3.0.

Extension Members


We all know about Extension Methods which was there since C# 3.0.
public static class INumberExtensions
{
    public static IEnumerable<int> WhereGreaterThan(this IEnumerable<int> sourceint threshold)
    {
        return source.Where(x => x > threshold);
    }

    public static bool AnyGreaterThan(this IEnumerable<int> sourceint threshold)
    {
        return source.WhereGreaterThan(threshold).Any();
    }
}
So here I have some extension methods for IEnumerable<int> which I can call like below:
IEnumerable<int> numbers = [1, 2, 3, 4, 5];

IEnumerable<int> largeNumbers = numbers.WhereGreaterThan(3);
bool hasLargeNumbers = numbers.AnyGreaterThan(3);
With C# 14.0, I can write the same with following syntax. Note: there is no use of this.
public static class INumberExtensions
{
    extension(IEnumerable<int> source)
    {
        public IEnumerable<int> WhereGreaterThan(int threshold)
            => source.Where(x => x > threshold);

        public bool AnyGreaterThan(int threshold)
            => source.WhereGreaterThan(threshold).Any();
    }
}
It also supports generic types, something like following:
using System.Numerics;

public static class INumberExtensions
{
    extension<T>(IEnumerable<T> source)
        where T : INumber<T>
    {
        public IEnumerable<T> WhereGreaterThan(T threshold)
            => source.Where(x => x > threshold);

        public bool AnyGreaterThan(T threshold)
            => source.WhereGreaterThan(threshold).Any();
    }
}

Null-Conditional Assignment


Consider the below class.
public class Person
{
    public required string Name { getinit}

    public DateOnly? Birthdate { getset}

    public string? Address { getset}
}
And say we need to have a method to update person details.
static void UpdatePerson(Person? personDateOnly birthdatestring address)
{
    if (person is null)
    {
        return;
    }

    person.Birthdate = birthdate;
    person.Address = address;
}
Here we need to explicitly check whether the person is null before assignment. Now with C# 14.0, we can do this:
static void UpdatePerson(Person personDateOnly birthdatestring address)
{
    person?.Birthdate = birthdate;
    person?.Address = address;
}
And this is exactly same as previous code, values will only get set if the person is not null.

Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, April 9, 2025

Azure DevOps: NuGet: Command Not Found with Ubuntu-Latest (24.04)

Our builds started failing today and it's Microsoft Servicing Tuesday even though it's Wednesday over here.

We are using ubuntu-latest in almost all our builds and started seeing the following error:

nuget: command not found

And our build pipelines was using NuGet.

task: NuGetAuthenticate@1
  displayName: NuGet Authenticate

script: nuget restore
  displayName: NuGet Restore

And then saw the following warning:

##[warning] See https://aka.ms/azdo-ubuntu-24.04 for changes to the ubuntu-24.04 image. 
Some tools (e.g. Mono, NuGet, Terraform) are not available on the image. 
Therefore some tasks do not run or have reduced functionality.

Have been seen that warning, but didn't put much attention. The link explains the failure.

So it's time to use dotnet restore (too bad we are late to the party)

task: NuGetAuthenticate@1
  displayName: NuGet Authenticate

task: DotNetCoreCLI@2
  displayName: .NET Restore
  inputs:
    command: 'custom'
    custom: 'restore'
    projects: '**/*.csproj'

And that's it, we are back to successful builds.

Look out for this one in your builds as well!

Hope this helps.

Happy Coding.

Regards,
Jaliya