Wednesday, February 22, 2023

System.Text.Json: Configure Polymorphism using the Contract Model

In this post let's see how we can configure Polymorphism using the Contract Model in System.Text.Json. You can also configure Polymorphism using attributes (How to configure Polymorphism using attributes), but there can be scenarios it isn't possible to use attributes. 

In that case, you can achieve Polymorphism using the Contract Model and this is only available with .NET 7 onwards.

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 instead of using attributes, I can update the JsonSerializerOptions, by calling the DefaultJsonTypeInfoResolver() constructor to obtain the JsonSerializerOptions.TypeInfoResolver and adding custom actions to its Modifiers property to configure Polymorphism.
var jsonSerializerOptions = new JsonSerializerOptions
{
    WriteIndented = true,
    PropertyNameCaseInsensitive = true,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    TypeInfoResolver = new DefaultJsonTypeInfoResolver
    {
        Modifiers =
        {
            static jsonTypeInfo =>
            {
                if (jsonTypeInfo.Type != typeof(Person))
                {
                    return;
                }

                jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions
                {
                    TypeDiscriminatorPropertyName = "type",
                    IgnoreUnrecognizedTypeDiscriminators = true,
                    UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
                    DerivedTypes =
                    {
                        new JsonDerivedType(typeof(Student), "Student"),
                        new JsonDerivedType(typeof(Employee), "Employee"),
                    }
                };
            }
        }
    }
};
And now I can above JsonSerializerOptions to Serialize/Deserialize as follows.
Person student = new Student
{
    Name = "John Doe",
    StudentId = 1
};

// Serializing
string jsonString = JsonSerializer.Serialize(student, jsonSerializerOptions);
Console.WriteLine(jsonString);
//{
//  "type": "Student",
//  "studentId": 1,
//  "name": "John Doe"
//}

// Deserializing
Person parsedPerson = JsonSerializer.Deserialize<Person>(jsonString, jsonSerializerOptions);
Console.WriteLine(parsedPerson is Student);  //true
Hope this helps.

Happy Coding.

Regards,
Jaliya

Thursday, February 16, 2023

System.Text.Json: Enforce Required Properties for Deserialization

I had a requirement where I want to enforce some property values to be present before deserializing the JSON string. And with .NET 7, you can do that pretty easily (another reason to move to .NET 7).

In this post, let's see how we can enforce the required properties for deserialization. There are three ways,

Failing to provide a required property will throw a JsonException.

Let's have a look.

By adding the required modifier

Consider the following POCO class. Here I have applied required modifier to FirstName and LastName.

public record Person
{
    public required string FirstName { getset; }

    public string? MiddleName { getset; }

    public required string LastName { getset; }
}

And let's try to deserialize the following string.

string @string = """
    {
        "FirstName": "John",
        "LastName": "Doe"
    }
    """
;

try
{
    Person? person = JsonSerializer.Deserialize<Person>(@string);
}
catch (JsonException jsonException)
{
    Console.WriteLine(jsonException.Message);
}

The above code is successfully deserializing the string into a person. Because the string has required properties set.

Now let's try the below. I am removing the LastName from JSON string.

string @string = """
    {
        "FirstName": "John"
    }
    """
;

try
{
    Person? person = JsonSerializer.Deserialize<Person>(@string);
}
catch (JsonException jsonException)
{
    Console.WriteLine(jsonException.Message);
    // JSON deserialization for type 'Person' was missing required properties, including the following: LastName
}

This will fail because our string doesn't contain the LastName property.

By annotating it with JsonRequiredAttribute

We can achieve the same result as above by using JsonRequiredAttribute. So instead of adding required modifier, we can decorate the property with JsonRequiredAttribute.

public record Person
{
    [JsonRequired]
    public string FirstName { getset; }

    public string? MiddleName { getset; }

    [JsonRequired]
    public string LastName { getset; }
}

By modifying the JsonPropertyInfo.IsRequired property of the contract model

This is kind of a more advanced approach.

string @string = """
    {
        "FirstName": "John"
    }
    """
;

var jsonSerializerOptions = new JsonSerializerOptions
{
    TypeInfoResolver = new DefaultJsonTypeInfoResolver
    {
        Modifiers =
        {
            static typeInfo =>
            {
                if (typeInfo.Kind != JsonTypeInfoKind.Object)
                {
                    return;
                }

                foreach (JsonPropertyInfo propertyInfo in typeInfo.Properties)
                {
                    propertyInfo.IsRequired = propertyInfo.Name switch
                    {
                        nameof(Person.FirstName) or nameof(Person.LastName) => true,
                        _ => false
                    };
                }
            }
        }
    }
};

try
{
   // This time we are passing the jsonSerializerOptions
    Person? person = JsonSerializer.Deserialize<Person>(@string, jsonSerializerOptions);
}
catch (JsonException jsonException)
{
    Console.WriteLine(jsonException.Message);
    // JSON deserialization for type 'Person' was missing required properties, including the following: LastName
}

The above will also throw a JsonException with a message saying Required properties aren't set.

Hope this helps. 

Happy Coding.

Regards,
Jaliya

Monday, February 6, 2023

Azure Cosmos DB for MongoDB: Backup and Restore Databases using MongoDB Command Line Database Tools

In this post let's see how we can backup and restore databases in an Azure Cosmos DB for MongoDB account using MongoDB Command Line Database Tools.

I am using Windows and the first step is to download MongoDB Command Line Database Tools. As of today, the version is 100.6.1 and I have downloaded the zip version. You can even download the msi and install the tools.

Backup

You can run the mongodump.exe from the command prompt providing the connection string of your Azure Cosmos DB for MongoDB account, the database you want to backup and an output path to write the backup files to.

mongodump.exe --uri "<connectionString>" --db <databaseName> --out <backupDirectory> 

mongodump.exe

Restore

You can run the mongorestore.exe from the command prompt providing the connection string of your Azure Cosmos DB for MongoDB account, a target database name, and the directory containing the backup files of the database.

mongorestore.exe --uri "<connectionString>" --db <databaseName> --dir <backupDirectory\databaseName>

mongorestore.exe
I am yet to confirm whether this approach is recommended, but so far I haven't faced any issues with the restored database. Collections and their documents count look great.

If you want to copy a database from one Azure Cosmos DB for MongoDB account to other, you can try this out. I have tried, Copy data to or from Azure Cosmos DB for MongoDB using Azure Data Factory or Synapse Analytics, but unfortunately, it didn't work out well due to some issues, hence tried MongoDB Command Line Database Tools.
   Issue: Copy Activity: Copying data from and to Azure Cosmos DB for MongoDB: Failing due to incorrect Data Type 
   Issue: Copy Activity: Copying data from and to Azure Cosmos DB for MongoDB: "The retrieved type of data JObject is not supported yet"

Hope this helps. 

Please don't forget to leave a comment on whether this approach worked for you or not.

Happy Coding.

Regards,
Jaliya

____________________________________________________________________________________________________________________________

Update on 2023/02/08:

I have received some recommendations from Product Group: Azure Cosmos DB.
  • Native MongoDB tools would be recommended for smaller workloads
  • Azure Data Factory (ADF) for medium (<1TB)
  • Spark if it’s a large dataset (>1 TB)

Thursday, February 2, 2023

Azure DevOps Pipelines: Install npm Packages from an External Private Package Registry

In this post let's see how we can Install npm Packages from an external private package registry in an Azure DevOps Pipeline.

The first step is adding a Service Connection for Connection Type: npm. You can do it by going into your Project Settings -> Service Connections and then New service connection and choosing npm.

New npm service connection
Click on Next.
New npm service connection
Here you can put the details, give the Service Connection a name and save it (note down the name as we are going to need it in a future step). In my case, I want to consume npm packages from Form.io private package repository and I have only the Username and the Password. So I have filled those up.

Next, we need to add/update the .npmrc file and specify the private package registry URL.
# Some other Azure Artifact Packages
registry=https://pkgs.dev.azure.com/{some-organization}/{some-project}/_packaging/{some-feed}/npm/registry/

# Form.io Premium Packages
@formio:registry=https://pkg.form.io/


always-auth=true
And now we can modify the DevOps pipeline to install the private packages.
trigger:
  branches:
    include:
      - main
      - feature/*
      - version/*

pool:
  vmImage: 'ubuntu-latest'

name: $(Build.SourceBranchName).$(Build.BuildId)

steps:
  - task: npmAuthenticate@0
    displayName: "Authenticate for FormIo Premium Packages"
    inputs:
      workingFile: .npmrc
      customEndpoint: FormIo-Packages # Important: Name of the npm Service Connection created

  - task: NodeTool@0
    displayName: 'Install Node.js'
    inputs:
      versionSpec: '16.x'
      checkLatest: true

  - task: Npm@1
    displayName: 'Install Formio Premium Packages'
    inputs:
      command: custom
      workingDir: $(RootPath)
      verbose: false
      customCommand: 'install @formio/premium --registry https://pkg.form.io --force'
Here the important step is using npmAuthenticate@0 task to provide npm credentials to the .npmrc file for the scope of the build. So it will get used when we are doing the npm install.

And that should do.

Hope this helps.

Happy Coding.

Regards,
Jaliya