Thursday, June 16, 2022

.NET 7 Preview 5: Improved Polymorphic Serialization/Deserialization Support in System.Text.Json

.NET 7 Preview 5 is out and we now have omproved Polymorphic Serialization/Deserialization Support in System.Text.Json.

Consider the following classes.
public class Person
{
    public string Name { getset; }
}
 
public class Student : Person
{
    public int StudentId { getset; }
}
 
public class Employee : Person
{
    public int EmployeeId { getset; }
}
Now if I do something like the below, you can see it only serialized the properties that are in the Parent class. (wrote a post about this a couple of months back: System.Text.Json.JsonSerializer: Serialize Properties of Derived Classes).
JsonSerializerOptions options = new() { WriteIndented = true };
 
Person person = new Student
{
    Name = "John Doe",
    StudentId = 1
};

string jsonString = JsonSerializer.Serialize<Person>(person, options);
Console.WriteLine(jsonString);
 
//{
//  "Name": "John Doe"
//}
We can change this behavior now using JsonDerivedType attribute annotations.
[JsonDerivedType(typeof(Student))]
[JsonDerivedType(typeof(Employee))]
public class Person
{
    public string Name { getset; }
}
This configuration enables polymorphic serialization for Person, specifically when the runtime type is one of the derived classes.

Now if we do something like the below, we can see correct runtime types are getting serialized.
JsonSerializerOptions options = new() { WriteIndented = true };
 
Person person = new Student
{
    Name = "John Doe",
    StudentId = 1
};
string jsonString = JsonSerializer.Serialize<Person>(person, options);
Console.WriteLine(jsonString);
 
//{
//  "StudentId": 1,
//  "Name": "John Doe"
//}
 
person = new Employee
{
    Name = "Jane Doe",
    EmployeeId = 1
};
 
jsonString = JsonSerializer.Serialize<Person>(person, options);
Console.WriteLine(jsonString);
 
//{
//  "EmployeeId": 1,
//  "Name": "Jane Doe"
//}
If I try to serialize a derived type that's not annotated in the base class, I am going to see an exception like the one below.
System.NotSupportedException: Runtime type 'Teacher' is not supported by polymorphic type 'Person'
Now when deserializing, it would be as follows.
string jsonString = """
{
  "StudentId": 1,
  "Name": "John Doe"
}
""";
 
Person person = JsonSerializer.Deserialize<Person>(jsonString);
Console.WriteLine(person is Student); // false, polymorphic deserialization doesn't work
Here note that this does not enable polymorphic deserialization. 

In order to enable polymorphic deserialization, we need to specify a type discriminator.
[JsonDerivedType(typeof(Student), typeDiscriminator: "student")]
public class Person
{
    public string Name { getset; }
}
Now when we serialize, the JSON will include the type discriminator.
Person person = new Student
{
    Name = "John Doe",
    StudentId = 1
};
string jsonString = JsonSerializer.Serialize<Person>(person, options);
Console.WriteLine(jsonString);
 
//{
//  "$type": "student",
//  "StudentId": 1,
//  "Name": "John Doe"
//}
Now when deserializing, we can see polymorphic deserialization works.
string jsonString = """
{
  "$type": "student",
  "StudentId": 1,
  "Name": "John Doe"
}
""";
 
Person person = JsonSerializer.Deserialize<Person>(jsonString);
Console.WriteLine(person is Student); // true, polymorphic deserialization works
Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, June 15, 2022

Azure Functions (In Process): Output Binding to a Dynamic Queue/Topic

In this post let's see how we can set up an Output Binding in an In Process Azure Function to send messages to a dynamic target Azure Service Bus Queue or Topic.

We usually use ICollector or IAsyncCollector to write values to an output binding. And often times, the target queue/topic is set up through attributes. Something like below,

[FunctionName(FunctionNames.SomeFunction)]
public async Task Run(
    [ServiceBusTrigger("%ServiceBus_ListeningQueue%", Connection = "ServiceBus_ConnectionString")] 
        ServiceBusReceivedMessage serviceBusReceivedMessage,
    [ServiceBus("%ServiceBus_TargetQueue%", Connection = "ServiceBus_ConnectionString")] 
        IAsyncCollector<MyOutputMessage> asyncCollector)
{
    // TODO: Some code

    await asyncCollector.AddAsync(new MyOutputMessage { });
}

The above function is listening to an Azure Service Bus Queue (ServiceBus_ListeningQueue) and outputting a message to another Queue (ServiceBus_TargetQueue). Here the target Queue is static and cannot be changed. 

What if we want to route the message to a target Queue or Topic based on a property of the incoming message (most of the time based on ServiceBusReceivedMessage.ReplyTo Property).

In these cases, we can use Azure Functions imperative binding instead of declarative binding (which is done above).

[FunctionName(FunctionNames.SomeFunction)]
public async Task Run(
    [ServiceBusTrigger("%ServiceBus_ListeningQueue%", Connection = "ServiceBus_ConnectionString")]
        ServiceBusReceivedMessage serviceBusReceivedMessage,
    IBinder binder)
{
    // TODO: Some code
 
    IAsyncCollector<MyOutputMessage> asyncCollector =
        await binder.BindAsync<IAsyncCollector<MyOutputMessage>>(new ServiceBusAttribute(serviceBusReceivedMessage.ReplyTo)
        {
            // Note: This should be the App Setting Name that contains the Service Bus connection string
            Connection = "ServiceBus_ConnectionString",
        });
 
    await asyncCollector.AddAsync(new MyOutputMessage { });
}

And that's it. You can change the EntityType property of ServiceBusAttribute to ServiceBusEntityType.Topic if you want to output into a Topic (default is Queue).

Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, June 8, 2022

Update Azure APIM API from an OpenAPI Definition using az apim Command Group

This is a quick post on how you can update Azure APIM API from an OpenAPI definition using az apim command group. 

Note: As of the day I am writing this post, apim Command group is experimental and under development.

The command we need to use is az apim api import.

$resourceGroup = "<Resource Group>"
$serviceName = "<APIM Name>"
$apiId = "<API Name>"
$path = "/<API URL suffix>"
$specificationUrl = "https://.../swagger.json"
 
az apim api import `
    --resource-group $resourceGroup `
    --service-name $serviceName `
    --api-id $apiId `
    --path $path `
    --specification-url $specificationUrl `
    --specification-format OpenApi

This will update the existing API while preserving existing policies that you may have on your API.

There is also az apim api update command and that is to be used if you want to update the properties of the API.

This approach can be used if you are facing 504 Gateway Timeout errors when updating APIs that contain a large schema using Azure Resource Manager (ARM) APIs and you are on a Developer Pricing Tier APIM: Azure API Management with Developer Pricing Tier: 504 Gateway Timeout.

Hope this helps.

Happy Coding.

Regards.
Jaliya

Monday, June 6, 2022

Playing Around with GitHub Copilot

I have been spending some quality time with GitHub Copilot and so far it has made me fall out of my chair a couple of times. It truly is amazing. Thought of writing this post to share some experience, so whoever hasn't tried it so far, can get amazed like me.

GitHub Copilot is a Pair Programmer, which does way more than auto-completion and it's completely powered with AI (Artificial Intelligence). I have been using Visual Studio for a long time, and probably since the day I started, we had IntelliSense which does the basic auto-completion. Then a couple of years back, Microsoft announced IntelliCode which is an AI-Assisted tool to suggest the next piece of code based on where we are, and it was great.

Now GitHub Copilot places things on a whole different level. It basically can write code based on the comments in Natural Language, can you believe that? So whether it's a comment, docstring, function name, or the code itself, GitHub Copilot uses those to synthesize code. Currently, it's still in its Technical Preview stage, and has first-class support for the following programming languages.

  • Python
  • JavaScript
  • TypeScript
  • Ruby
  • Java
  • Go

Same time it understands dozens of other languages and can help you find your way around almost anything. I have tried with C#, T-SQL and it's working super nicely.

As of today, GitHub Copilot  is available as an extension for 

This is a simple express app, I have created using Visual Studio Code, I only wrote the comments, and the code was completely written by GitHub Copilot.
// Create an Express application
var express = require('express');
 
// Expose an endpoint
var app = express();
app.get('/'function (reqres) {
    res.send('Hello World!');
});
 
// Expose another endpoint
app.get('/api'function (reqres) {
    // Send a response of current time
    res.send(new Date());
});
 
// Run express application
app.listen(3000, function () {
    console.log('Listening on port ' + 3000);
});
I have also created a simple ASP.NET Core Minimal API with CRUD operations, again I only wrote the comments on natural language, and GitHub Copilot wrote all the new code for creating a class, the DbContext, and exposing CRUD endpoints and that's with the functionality.

So how does this work?

First, let's start with a bit of history. It all started with an AI Research Startup called OpenAI founded by Elon Musk and Sam Altman. In July 2019, Microsoft partnered up with OpenAI. In June 2020, OpenAI announced GPT-3, a language model trained on trillions of words from the Internet. In June 2021, OpenAI Codex was introduced, a model based on GPT-3 to translate natural language to code (and vice-versa).

GitHub Copilot is powered with OpenAI Codex models. 
How GitHub Copilot Works
OpenAI Codex was trained on publicly available source code (including code in public repositories on GitHub) and natural language, so it understands both programming and human languages. The GitHub Copilot editor extension sends our comments and code to the GitHub Copilot service, which then uses OpenAI Codex to synthesize and suggest individual lines and/or whole functions.

The whole GitHub Copilot experience is beyond words, you have to try it for yourself. Sign up with the Technical Program now, you will have to be on the waitlist for some time, but I would definitely say it's worth the wait, because once you have access, it's going to blow your minds off.

Hope this helps.

Happy Coding.

Regards,
Jaliya

Tuesday, May 31, 2022

Visual Studio 2022 17.3.0 Preview 1.1: Introducing Port Tunneling for ASP.NET Core Projects

Last week, it's Microsoft Build week, and hope you have enjoyed it.  

There were some nice announcements and one of my favorites out of them all is this nice feature that got released with Visual Studio 2022 17.3.0 Preview 1.1. That's introducing the private preview of port tunneling in Visual Studio for ASP.NET Core projects. 

With this, I can run my Web Application locally and the URL it's running is public and can be accessed from outside of my local environment. With most of us working from home, I am finding this very helpful. I can do things like, 

  • Share the public URL with a colleague to test out the application. 
  • If it's a Frontend Web Application,  access the URL from my mobile, and see how it's behaving.
  • I don't have to deploy the application to test a Webhook with a third party etc
In order to use this feature, there are a couple of things you need to do first.

The first thing is obviously you need to download and install the latest preview of Visual Studio 2022. And that's Visual Studio 2022 17.3.0 Preview 1.1.

Next, you need to sign up for the private preview program of port tunneling in Visual Studio. Otherwise, you are going to get an error like below when you are going to try it.
Missing sign-up for Port Tunneling program
You can do it by filling out the form here: https://aka.ms/tunnels-signup. Something to note here is, it's going to take some time for access to be granted into the private program and at this time individual users will not be considered, only organizations with tenant IDs.

After signing up with the preview program, log in to Visual Studio with the email address you have used. Then under Tools -> Options -> Environment -> Preview Features, check Enable port tunneling for Web Applications.
Enable port tunneling for Web Applications
Now you are almost set.

Finally, create a new ASP.NET Core Web Application, and once the project is created, update the launchSettings.json as below.

launchSettings.json
{
  "$schema""https://json.schemastore.org/launchsettings.json",
  "iisSettings": {
    "windowsAuthentication"false,
    "anonymousAuthentication"true,
    "iisExpress": {
      "applicationUrl""http://localhost:4367",
      "sslPort": 44305
    }
  },
  "profiles": {
    "WebApplication1": {
      "commandName""Project",
      "dotnetRunMessages"true,
      "launchBrowser"true,
      "launchUrl""swagger",
      "applicationUrl""https://localhost:7015;http://localhost:5015",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT""Development"
      },
      "createTunnel"true
    },
    "IIS Express": {
      "commandName""IISExpress",
      "launchBrowser"true,
      "launchUrl""swagger",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT""Development"
      }
    }
  }
}
The only change here I have done here is I have set a new property "createTunnel"true under the profile I am using to run the Application.

And that's it. Now you can launch the application.

Happy Coding.

Regards,
Jaliya

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 Shift + Alt + F9, 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