Tuesday, December 29, 2020

EF Core 5.0: SaveChanges Events and Interceptors

In this post let's have a look at one of the new features that got introduced with EF Core 5.0. That is SaveChanges Events and Interceptors.

Events

Following events has been introduced,

  • DbContext.SavingChanges
    • This event is raised at the start of SaveChanges or SaveChangesAsync
  • DbContext.SavedChanges
    • This event is raised at the end of a successful SaveChanges or SaveChangesAsync
  • DbContext.SaveChangesFailed
    • This event is raised at the end of a failed SaveChanges or SaveChangesAsync
static async Task Main(string[] args)
{
    using var context = new MyDbContext();
    await context.Database.EnsureDeletedAsync();
    await context.Database.EnsureCreatedAsync();

    context.SavingChanges += Context_SavingChanges;
    context.SavedChanges += Context_SavedChanges;
    context.SaveChangesFailed += Context_SaveChangesFailed;

    var john = new Student { Name = "John" };
    var jane = new Student { Name = "Jane" };

    context.AddRange(john, jane);
    await context.SaveChangesAsync();
}

private static void Context_SavingChanges(object sender, SavingChangesEventArgs args)
{
    Console.WriteLine($"Starting saving changes.");
}

private static void Context_SavedChanges(object sender, SavedChangesEventArgs args)
{
    Console.WriteLine($"Saved {args.EntitiesSavedCount} No of changes.");
}

private static void Context_SaveChangesFailed(object sender, SaveChangesFailedEventArgs args)
{
    Console.WriteLine($"Saving failed due to {args.Exception}.");
}

Here note that the event sender is always the DbContext instance.

Interceptors

Interceptors can be used for interception (as the name suggests of course), modification, and most importantly Interceptors allow SaveChanges to be suppressed unlike Events. It also provides async counterparts of methods so you can do async actions.

Interceptors needs to implement ISaveChangesInterceptor interface. It contains following method definitions.

  • SavingChanges
    • Called at the start of DbContext.SaveChanges
  • SavingChangesAsync
    • Called at the start of DbContext.SaveChangesAsync.
  • SavedChanges
    • Called at the end of DbContext.SaveChanges.
  • SavedChangesAsync
    • Called at the end of DbContext.SaveChangesAsync.
  • SaveChangesFailed
    • Called when there is an exception thrown from DbContext.SaveChanges.
  • SaveChangesFailedAsync
    • Called when there is an exception thrown from DbContext.SaveChangesAsync.

There is also a SaveChangesInterceptor abstract class included in the framework, so for your Interceptors you can derive from that and override only the required methods, rather than implementing all the methods in the ISaveChangesInterceptor.

So let's see some code. I can create a custom Interceptor like this.

public class MyCustomInterceptor : SaveChangesInterceptor
{

}

Then I need to tell EF to use this Interceptor.

public class MyDbContext : DbContext
{
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer($@"Data Source=RAVANA-TPP50\MSSQLSERVER2017; Initial Catalog=EfCore5;User ID=sa;Password=sa")
            .AddInterceptors(new MyCustomInterceptor());
    }

    // Some Code
}

Now I can start customizing my Interceptor. For example, I am going to override SavingChangesAsync and SavedChangesAsync.

public class MyCustomInterceptor : SaveChangesInterceptor
{
    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        Console.WriteLine($"Started saving changes.");

        return new ValueTask<InterceptionResult<int>>(result);
    }

    public override ValueTask<int> SavedChangesAsync(SaveChangesCompletedEventData eventData,
        int result,
        CancellationToken cancellationToken = default)
    {
        Console.WriteLine($"Saved {result} No of changes.");

        return new ValueTask<int>(result);
    }
}

Now if I add a record and do SaveChangesAsync(), I can see following output.

Started saving changes.
Saved 1 No of changes.

I have mentioned above, with Interceptors we can suppress the operations.

public class MyCustomInterceptor : SaveChangesInterceptor
{
    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        Console.WriteLine($"Started saving changes.");

        result = InterceptionResult<int>.SuppressWithResult(0);

        return new ValueTask<InterceptionResult<int>>(result);
    }

    public override ValueTask<int> SavedChangesAsync(SaveChangesCompletedEventData eventData,
        int result,
        CancellationToken cancellationToken = default)
    {
        Console.WriteLine($"Saved {result} No of changes.");

        return new ValueTask<int>(result);
    }
}

Here on SavingChangesAsync, I have Suppressed the operation. And now If I try SaveChangesAsync, I can see nowthing is getting Saved.

Started saving changes.
Saved 0 No of changes.

There is more to Interceptors like you can Intercept different low-level database operations such as Db Connection Interception, Db Command Interception, Transaction Interception etc. I suggest you read Interceptors for more information.

Hope this helps.

Happy Coding.

Regards,
Jaliya

Tuesday, December 15, 2020

Azure Functions: Change PlanType from Consumption (Serverless) to Premium

This is a quick post on how we can change the Plan Type of an Azure Function App from Consumption (Serverless) to a Premium Plan. If you have created an Azure Function App initially selecting Consumption (Serverless) as the plan type and then later if you want to change it to a Premium Plan, Azure Portal has no direct way to do it.

Here you have 2 options,

  1. Create a new Azure Function App with a Premium plan and redeploy code
  2. Create a new Azure Function Premium Plan and change the existing Function App to use the new plan through Azure CLI
The first option can be a pain, you basically need to setup everything and deploy the code. The second option is pretty easy.

First, we need to create a new Azure Function Premium Plan.
az functionapp plan create `
--resource-group <resource group> `
--name <new plan name> `
--location <location> `
--sku <EP1 or EP2 or EP3> `
--is-linux true
--is-linux is only if you want it to be Linux, well it's self-explanatory.

Next, we need to associate the existing Function App with the new plan.
az functionapp update `
--resource-group <resource group of the existing function app> `
--name <existing function app name> `
--plan <new plan name>
That's easy. Hope this helps.

Happy Coding.

Regards,
Jaliya

Thursday, December 3, 2020

EF Core 5.0: Many to Many Relationships

One of the nicest features available in EF Core 5.0 is now you can define a many-to-many relationship without explicitly mapping the join table. 

With earlier EF Core versions, you didn't have the luxury of doing something like this,

Example 1:

public class Student
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<Course> Courses { getset; }
}

public class Course
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<Student> Students { getset; }
}

You needed to explicitly create the join table, something like below.

Example 2:

public class Student
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<CourseStudent> CourseStudents { getset; }
}

public class Course
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<CourseStudent> CourseStudents { getset; }
}

public class CourseStudent
{
    public int Id { getset; }

    public Course Course { getset; }     public Student Student { getset; }
}

But with EF Core 5.0, you don't have to do that anymore, the code in Example 1 would work just fine. EF will understand we need a join table and it will create one automatically.

Database
You can use it like below.
using var context = new MyDbContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

var john = new Student { Name = "John" };
var jane = new Student { Name = "Jane" };
var tim = new Student { Name = "Tim" };

var csharp = new Course { Name = "C#", Students = new List<Student> { john, jane } };
var sql = new Course { Name = "SQL", Students = new List<Student> { tim } };

context.AddRange(john, jane, tim, csharp, sql);
await context.SaveChangesAsync();

List<Student> csharpStudents = await context.Students .Where(x => x.Courses.Any(sc => sc.Name == csharp.Name)) .ToListAsync();

foreach (Student student in csharpStudents)
{
    Console.WriteLine(student.Name);
}

Output,

John
Jane

Often times, you need additional data (known as payload properties) in the join table. In our case here, maybe the date that the student got enrolled with the Course. So I can design the data model like below. 

public class Student
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<Course> Courses { getset; }

    public ICollection<CourseStudent> CourseStudents { getset; }
}

public class Course
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<Student> Students { getset; }

    public ICollection<CourseStudent> CourseStudents { getset; }
}

public class CourseStudent
{
    public Course Course { getset; }

    public Student Student { getset; }

    public DateTime EnrolledDate { getset; }
}

If you have noticed in the above code, you can even specify a navigation property to the Join table. Let's see how it can make our life easy in certain cases.

And EF Core allows full customization of the join table, something like below.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Student>()
        .HasMany(x => x.Courses)
        .WithMany(x => x.Students)
        .UsingEntity<CourseStudent>(
            x => x
                .HasOne(x => x.Course)
                .WithMany(x => x.CourseStudents),
            x => x
                .HasOne(x => x.Student)
                .WithMany(x => x.CourseStudents),
            x =>
            {
                x.Property(x => x.EnrolledDate).HasDefaultValueSql("CURRENT_TIMESTAMP");
            });
}

Then I can do things like this.

using var context = new MyDbContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

var john = new Student { Name = "John" };
var jane = new Student { Name = "Jane" };
var tim = new Student { Name = "Tim" };

var csharp = new Course
{
Name = "C#",
    CourseStudents = new List<CourseStudent>
    {
        new CourseStudent
        {
            Student = john,
            EnrolledDate = new DateTime(2020, 06, 01)
        },
        new CourseStudent
        {
            Student = jane,
            EnrolledDate = new DateTime(2020, 10, 01)
        }
    }
};
var sql = new Course { Name = "SQL", Students = new List<Student> { tim } };

context.AddRange(john, jane, tim, csharp, sql);
await context.SaveChangesAsync();

List<Course> courses = await context.Courses
    .ToListAsync();

foreach (Course course in courses)
{
    Console.WriteLine($"{course.Name}");

    // If you don't want payload property information, you can just use Students 
    foreach (Student student in course.Students)
    {
     Console.WriteLine($"-- {student.Name}");
    }

    // If you want payload property information, you can use CourseStudents, the navigation to join table  
    foreach (CourseStudent courseStudent in course.CourseStudents)
    {
        Console.WriteLine($"-- {courseStudent.Student.Name} : {courseStudent.EnrolledDate}");
    }
}

Output,

C#
// Without using Join Table
-- John
-- Jane

// With using Join Table
-- John : 6/1/2020 12:00:00 AM
-- Jane : 10/1/2020 12:00:00 AM

SQL
// Without using Join Table
-- Tim

// With using Join Table
-- Tim : 12/3/2020 9:17:00 PM

Hope this helps.

To know more on EF Core 5, you can read: What's New in EF Core 5.0

Happy Coding.

Regards,
Jaliya

Thursday, November 26, 2020

Setting up SSH on a Custom Linux Docker Container

I had some Azure App Services for Containers and when I wanted to SSH, it was giving this annoying error: SSH CONNECTION CLOSE - Error: connect ECONNREFUSED <ipaddress>:2222.

SSH CONNECTION CLOSE - Error: connect ECONNREFUSED

In this post let's see how we can set up SSH on a Custom Linux Docker Container and get past this error. It's actually pretty easy, there is official documentation: Configure SSH in a custom container. I feel it's not that intuitive, hence this post.

First, you need to install openssh-server inside your docker image, I happen to be using aspnet:3.1-buster-slim as the base in my case. It shouldn't matter.
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base

RUN apt-get update -y \
    && apt-get install -y --no-install-recommends openssh-server 
Now you need to set a password for the root account.
RUN mkdir -p /run/sshd && echo "root:Docker!" | chpasswd
Then you need to add this sshd_config file inside /etc/ssh in the image.
# This is ssh server systemwide configuration file.
#
# /etc/sshd_config

Port                 2222
ListenAddress  0.0.0.0
LoginGraceTime  180
X11Forwarding  yes
Ciphers aes128-cbc,3des-cbc,aes256-cbc,aes128-ctr,aes192-ctr,aes256-ctr
MACs hmac-sha1,hmac-sha1-96
StrictModes  yes
SyslogFacility  DAEMON
PasswordAuthentication  yes
PermitEmptyPasswords  no
PermitRootLogin  yes
Subsystem sftp internal-sftp

You can copy the file as below,
COPY sshd_config /etc/ssh/
In sshd_config, SSH port is 2222, so we need to expose it.
EXPOSE 2222
We are almost there now. The final step is to start the SSH server (here, I am just running 2 commands, in my case I needed to run a .NET application as well).
ENTRYPOINT ["/bin/bash""-c""/usr/sbin/sshd && dotnet SomeApplication.dll"]
And now when this container is running, you should be able to SSH into it.
SSH

SSH Connection Established
Hope this helps.

Happy Coding.

Regards,
Jaliya

Tuesday, November 24, 2020

Azure Static Web Apps with Blazor

In this post let's see what Azure Static Web Apps are and how we can easily set up an Azure Static Web App using Blazor. 

Azure Static Web Apps is a service that automatically builds and deploys full-stack web apps to Azure from a GitHub repository. As of today I am writing this post, it's still in its Preview stage and is completely free to try out. 

One of the nicest things about Static Web Apps is, static assets are separated from a traditional web server and are instead served from points geographically distributed around the world. And this makes serving files much faster as files are physically closer to end-users. In addition, API endpoints are hosted using a serverless architecture, which avoids the need for a full back-end server altogether. 

And Blazor WebAssembly Apps makes a nice fit for Azure Static Web Apps.

Now let's see how we can easily create an Azure Static Web App with Blazor powered with Azure Functions Backend API.

First I am creating a Blazor Web Assembly app.
Blazor WebAssembly App
Once the project is created, I am adding an Azure Functions App to the the project with a simple HTTP Trigger function (nothing fancy here)
public static class WeatherForecastFunction
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing""Bracing""Chilly""Cool""Mild""Warm""Balmy""Hot""Sweltering""Scorching"
    };
    [FunctionName("WeatherForecast")]
    public static async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequest request)
    {
        var rng = new Random();
        return await Task.FromResult(new OkObjectResult(Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray()));
    }
}
Next, in the Blazor Project, I am changing the default the code in FetchData page to call this function instead of loading from a sample JSON file.

Then I am chaning the Program.cs to setup the HttpClient to pick its' BaseAddress from app settings for local development. So I can run the app locally and make sure all works.
public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        builder.RootComponents.Add<App>("#app");
        var baseAddress = builder.Configuration["ApiUrl"] ?? builder.HostEnvironment.BaseAddress;
        builder.Services.AddScoped(sp => new HttpClient
        {
            BaseAddress = new Uri(baseAddress)
        });
        await builder.Build().RunAsync();
    }
}
And for local app settings, I have set the local ApiUrl.
{
  "ApiUrl""http://localhost:7071"
}
Now I am all set. This is how my solution looks like.
Solution
I am running both the projects locally to make sure all is good.
Running Locally
That's promising. Now I am pushing the source into a GitHub repo (you can actually avoid all the above and fork this repo: https://github.com/jaliyaudagedara/hello-serverless-blazor)

Now the next step is to create an Azure Static Web App. 

Create a Static Web App
Search for Static Web App and click on Create.
Sign in with GitHub
       Here I have given some name. Then I need to Sign in with GitHub to chose the Repo.
Static Web App Setup
Here I have selected my repo and then under Build Presets, I have selected Blazor. There you can see is a nice list of supported presets, some I actually haven't even heard before.

 Build Presets
Then you need to select the App location. This is the location of our Blazor Application. For the Api location,  we have a function app, so I have specified the Azure Function location. Once you specify all, it will create a GitHub actions workflow file that you can preview(it's read-only). And then we can click on Create.

Once we clicked on Create, Azure will commit the workflow file to our repo and the workflow will get triggered. It will basically build and deploy the code to the Static Web App we just created.
GitHub Workflow
Once the workflow is completed, I can try navigating to the Static Web App. It's running and just had a look at Fetch data page, and yes, it's calling our API and displaying data.

Hope this helps.

Happy Coding.

Regards,
Jaliya

Thursday, November 12, 2020

C# 9.0: Target-typed new expressions

C# 9.0 is finally here along with .NET 5. Happy times.

In this post, let's have a look at a small but nice feature which can make some part of our code looks much nicer. 

In C#, all this time when you are using new expression, you needed to specify the type (which type you are going to be newing on) except when newing an array.  

// Type is required when newing
var person = new Person("John""Doe");
var fruits = new List<string>() { "Apple""Orange" };

// Except this one
int[] arrray = new[] { 1, 2, 3 };

With C# 9.0, you can do something like this.

Person person = new ("John""Doe");

List<string> fruits = new() { "Apple""Orange" };

Now instead of using implicit type on the left-hand side, we can be specific by putting the explicit type and then on the right-hand side when we are newing, we can omit the type. Because the compiler knows what type of object we are going to create based on the left-hand side.

It's really becoming handy when it comes to a more complex collection initializer. Consider the below Dictionary initializer.

var keyValuePairs = new Dictionary<intPerson>()
{     { 1, new Person("John""Doe") },
    { 2, new Person("Jane""Doe") }
};

With Target-typed new expressions,

Dictionary<intPerson> keyValuePairs = new ()
{
    { 1, new ("John""Doe") },
    { 2, new ("Jane""Doe") }
};

Isn't it pretty neat?

Happy Coding.

Regards,
Jaliya

Saturday, October 31, 2020

Session: Consuming Configuration Data from Azure App Configuration

Delivered an online session for Sri Lanka DevOps Communities October 2020 monthly meetup.

The session was titled Session: Consuming Configuration Data from Azure App Configuration and there I discussed what is Azure App Configuration (AAC) and what was Microsofts' main motivation to develop AAC.

Then demoed how we can consume AAC configuration data from Azure Functions App which references Azure Key Vault (AKV) values, how to setup the local development environment to read values from AAC and AKV, how to setup AAC, AKV using User-Assigned Managed Identity. And then demoed refreshing of configurations using ASP.NET Core Web API.

Session: Consuming Configuration Data from Azure App Configuration
Meetup: SL DevOps Community - October 2020 - Online Meetup
Video: YouTube
Demo Source Code: https://github.com/jaliyaudagedara/AzureAppConfigurationDemo

Happy Coding.

Regards,
Jaliya

Wednesday, October 14, 2020

C# 9.0: Records

C# 9.0 is almost around the corner, we are just less than a month away from .NET Conf 2020, where C# 9.0 along with .NET 5.0 and Visual Studio 16.8.0 will be officially released. 

One of the biggest features coming with C# 9.0 is Records. I think it's high time we have a look at what Records are.

You can try out this feature even now with the latest Preview of Visual Studio 2019 (Version 16.8.0 Preview 4 as of today) and .NET 5.0 RC (SDK 5.0.100-rc.2 as of today).

In this post, let's have an overview of what Records are. But before diving in, let's consider the following example using C# 8.

Consider, you have a Person class with just 2 properties, FirstName and LastName.

var person = new Person("John""Doe");

So to be able to do this, I need to create a Person class with 2 properties and then set values to them through the constructor, something like below.

public class Person
{
    public string FirstName { getset; }
    public string LastName { getset; }

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }
}

Next, say, I am creating another Person with the same FirstName, LastName.

var otherPerson = new Person("John""Joe");

There are often times, I need to compare whether it's the same person. I don't care about the references, I am only concerned about its values. Basically something like this,

bool isTheSamePerson = person == otherPerson;

And for us to be able to something like this, we need to modify Person class to implement IEquatable<T> and override operators.

public class Person : IEquatable<Person>
{
    public string FirstName { get; }
    public string LastName { get; }

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public bool Equals(Person other) => other is object && FirstName == other.FirstName && LastName == other.LastName;

    public static bool operator ==(Person left, Person right) => left is object ? left.Equals(right) : left is null;

    public static bool operator !=(Person left, Person right) => !(left == right);

    public override bool Equals(object obj) => Equals(obj as Person);

    public override int GetHashCode() => HashCode.Combine(FirstName, LastName);
}

Now say, you want to deconstruct the Person (part of C# 7.0), that's something like this,

(string firstName, string lastName) = person;

To be able to do this, you need to add a Deconstruct method to Person class.

public void Deconstruct(out string firstName, out string lastName)
{
    firstName = FirstName;
    lastName = LastName;
}

And finally, say we want to override Person.ToString() to return $"{FirstName} {LastName}". Yes, that means another method.

public override string ToString() => $"{FirstName} {LastName}";

Suddenly you can see our small class has grown and these functionalities are pretty much we are going need in most of the cases. So our C# 8.0 class is going to look like below with all the above functionality.

public class Person : IEquatable<Person>
{
    public string FirstName { get; }
    public string LastName { get; }

    public Person(string firstName, string lastName)
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public void Deconstruct(out string firstName, out string lastName)
    {
        firstName = FirstName;
        lastName = LastName;
    }

    public bool Equals(Person other) => other is object && FirstName == other.FirstName && LastName == other.LastName;

    public static bool operator ==(Person left, Person right) => left is object ? left.Equals(right) : left is null;

    public static bool operator !=(Person left, Person right) => !(left == right);

    public override bool Equals(object obj) => Equals(obj as Person);

    public override int GetHashCode() => HashCode.Combine(FirstName, LastName);

    public override string ToString() => $"{FirstName} {LastName}";
}

What if we can simplify all this code. Enter Records.

Records

With C# 9.0, you can have all the above functionality available by default by creating a Record of Person instead of a Class of Person.

public record Person(string FirstName, string LastName);

You should see something strange here, we can specify parameters at the record definition level. This form is called positional records. You can still use the traditional syntax, which is defining separate properties and a constructor, etc.

So basically, I can do something like this and I will get a below output.

using System;

var person = new Person("John""Doe");
var otherPerson = new Person("John""Doe");

// Value Comparision
Console.WriteLine($"Value Comparison: {person == otherPerson}");

// Reference Comparision
Console.WriteLine($"Reference Comparison: {ReferenceEquals(personotherPerson)}");

// Deconstruction
(string firstNamestring lastName) = person;
Console.WriteLine($"Deconstuct: {firstName} {lastName}");

// ToString()
Console.WriteLine($"ToString(): {person}");

public record Person(string FirstNamestring LastName);

Output:

Value Comparison: True
Reference Comparison: False
Deconstuct: John Doe
ToString(): Person { FirstName = John, LastName = Doe }

So what is this new type, Record? 

Records are reference types and are immutable by default. There is a nice read here What's new in C# 9.0: Record types, I highly recommend you to read that for a more detailed explanation of records and its internal implementation.

Another nice thing with Records is, records support with-expressions. It's something like this,

var someOtherPerson = person with { FirstName = "Jane" };

So this will create a new object someOtherPerson, and all the properties of person will be shallow copied, but the specified properties will be changed.  I can verify that by printing someOtherPerson,

Console.WriteLine(someOtherPerson);
// Person { FirstName = Jane, LastName = Doe }

If you try to set values to FirstName/LastName properties without using the object initializer, we can see something interesting here.

init accessor
Here we can see a new accessor called init. It's another feature coming with C# 9.0. Properties that have init accessor can be set only in the object initializer or in the instance constructor. So when a record is defined using positional record syntax, all its arguments will have init accessor.

Let's modify the person record a bit by adding another property.

public record Person(string FirstNamestring LastName)
{
    public int Age { getset; }
}

So if we try to set property values,

someOtherPerson.LastName = "Smith"// this is NOT ALLOWED, because it has init accessor
someOtherPerson.Age = 20; // this is allowed

Hope that's enough to get you started on exploring more on records.

If you haven't saved the date of .NET Conf 2020, just spend like a minute and do save the date right now.

Happy Coding.

Regards,
Jaliya