cli  
 
Article Series

This article series discuss more than 30 different programming languages. Please read overview before you read any of the details.

Playing with Records Related Articles.

Where to Discuss?

Local Group

Preface

Goal: A practical case to collect unique record fields using C#.

With C# 9.0, we have OOP in high level, and FP everywhere else.

For linux user, I give additional setup procedures. But don’t worry, this should be quick. And this is not a cumbersome process. And after that you can have so much fun with the language itself.

Reference Reading

Source Examples

You can obtain source examples here:


Common Use Case

Task: Get the unique tag string

Please read overview for more detail.

Prepopulated Data

Songs and Poetry

using System;

#nullable enable

public record MySong {
  public string Title  { get; init; }  = null!;
  public string[]? Tags { get; init; } = null!;
}; 

class MySongs
{
  public MySong[] songs;

  public MySongs() {
    songs = new [] {
      new MySong { 
        Title = "Cantaloupe Island",
        Tags  = new string[] {"60s", "jazz"}
      },
      new MySong { 
        Title = "Let It Be",
        Tags  = new string[] {"60s", "rock"}
      },
      new MySong { 
        Title = "Knockin' on Heaven's Door",
        Tags  = new string[] {"70s", "rock"}
      },
      new MySong { 
        Title = "Emotion",
        Tags  = new string[] {"70s", "pop"}
      },
      new MySong { 
        Title = "The River",
        Tags  = null
      },
    };
  }
}

C# Solution

The Answer

There might be many ways to do things in C#. One of them is this oneliner as below:

using System;
using System.Linq;

class Step09Unique
{
  public void Show() {
    MySongs mySongs = new MySongs();
  
    var tags = (mySongs.songs)
      .Select(song => song.Tags)
      .Where(tags => tags != null)
      .SelectMany(tag => tag)
      .Distinct();

    Console.WriteLine("[{0}]",
      string.Join(", ", tags));
  }
}

Enough with introduction, at this point we should go straight to coding.


Environment

We are using C# 9.0 for this project.

Install

This case example require C# 9.0 that is shipped with .NET 5.0.

Just in case your package manager in linux is still using .NET 3.1, you can download the binary, extract the tar.gz, and make an alias to .bashrc/zshrc to dotnet exectuable.

I’m a linux user, and I can say .NET is doing it right. Better and get better.

Project Directory

One installed properly we can make a project straight away.

$ cd /home/epsi/Documents/songs/csharp
$ dotnet new console -o SongsApp
Getting ready...
The template "Console Application" was created successfully.

Processing post-creation actions...
Running 'dotnet restore' on SongsApp/SongsApp.csproj...
  Determining projects to restore...
  Restored /home/epsi/Documents/songs/csharp/SongsApp/SongsApp.csproj (in 186 ms).
Restore succeeded.

.NET C#: Making New Project

Project File

If a project was made from previous .NET version. The project file should be altered to provide C# 9.0.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp5.0</TargetFramework>
    <LangVersion>9.0</LangVersion>
  </PropertyGroup>

</Project>

Working with Multiple Script.

I need multiple script to explain this case example.

I love how mature the .NET has become. But it has tradeoff. We no longer can affor multiple entry point in one project.

This is my workaround to work with multiple script, in just one project folder. So I guess my problem is solved.

Main Method

.NET use csc to compile the code. Code compiled should have static main method as an entry point.

class Program
{
  static void Main(string[] args) {
    
  }
}

Multiple Entry Point

In the past you can use csc to compile one script, and you can still do it with mono. This means you can have many script with, each script has their own main method.

This no longer valid with newer roslyn csc. We can have as many script as we have, but there can only be one static main method. This is a good thing actually, and I would like to follow the .Net direction as possible.

Instead of doing different command line to run different script, I choose to alter the main script, to call different script. This way we can work with multiple class with each different script.

class Program
{
  static void Main(string[] args) {
    // Step01Tags step = new Step01Tags();
    
    Step09Unique step = new Step09Unique();
    step.Show();
  }
}

I can just switch from different script easily this way.

Using REPL

Alternatively, you can use REPL with csi.


1: Class and Record

We are going to use array throught out this article

Namespace

For simplicity, I strip out all namespaces. Namespace is good for a real project, we should do it a lot. But for this simple step by step case example, we need to simplified a bit.

So here we are just class and method(s).

class Program
{
  static void Main(string[] args) {
    Step01Tags step = new Step01Tags();
    step.Show();
  }
}

From the very start, we separate class in different file.

Simple Array

Before building a record, I begin with simple array with fix length string[5] as below code:

using System;

class Step01Tags
{
  private string[] tags;

  public Step01Tags() {
    tags = new string[5]
      {"rock", "jazz", "rock", "pop", "pop"};
  }

  public void Show() {
    Console.WriteLine("[{0}]",
      string.Join(", ", tags));
  }
}

It is easy to dump variable in C# using Console.WriteLine. This require us to declare using System; in the first place.

Now we can have the result similar as below array:

$ cd /home/epsi/Documents/songs/csharp
$ cd SongsApp
$ dotnet run
[rock, jazz, rock, pop, pop]

The Record Type

C# 9.0 New Feature.

We can continue our journey to use record structure type. Record is a new feature from C# 9.0. So this script won’t run with previous C#.

Our song record would be as simple as below code:

  public record MySong {
    public string Title  { get; init; }
    public string[] Tags { get; init; } 
  };

This record is very similar with class, but simplified for easy to use.

The Song Record

How do I populate a record?

The complete class is as below:

using System;

class Step02Type
{
  public record MySong {
    public string Title  { get; init; }
    public string[] Tags { get; init; } 
  }; 

  private MySong song;

  public Step02Type() {
    song = new MySong {
      Title = "Cantaloupe Island",
      Tags  = new string[] {"60s", "jazz"}
    };
  }

  public void Show() {
    Console.WriteLine("[{0}, ({1})]",
      song.Title, string.Join(", ", song.Tags)
    );
  }
}

In order to run this class, do not forget to alter the entry point. It is the static main method.

class Program
{
  static void Main(string[] args) {
    // Step01Tags step = new Step01Tags();
    Step02Type step = new Step02Type();
    step.Show();
  }
}

With the result similar as below record:

$ dotnet run
[Cantaloupe Island, (60s, jazz)]

.NET C#: The Song Record Structure Type

We are going to use string.Join throughout this article.

Class Summary

Here we have four items in a class.

  1. Record Type: MySong,
  2. Property: song,
  3. Constructor: Step02Type(),
  4. Method: Show().
class Step02Type
{
  public record MySong {}; 

  private MySong song;

  public Step02Type() {}

  public void Show() {}
}

Take a deep breath. Take your caffeine. There are some interesting material ahead.


2: Data Structure

From just karaoke, we can go pro with recording studio. From simple data, we can build a structure to solve our task.

Array of Song Record

Meet The Songs Array

using System;

class Step03Songs
{
  public record MySong {
    public string Title  { get; init; }
    public string[] Tags { get; init; } 
  }; 

  private MySong[] songs;

  public Step03Songs() {
    songs = new [] {
      new MySong { 
        Title = "Cantaloupe Island",
        Tags  = new string[] {"60s", "jazz"}
      },
      
      new MySong { 
        Title = "The River",
        Tags  = new string[] {}
      },
    };
  }

  public void Show() {
    Array.ForEach(songs,
      song => Console.WriteLine("[{0}, ({1})]",
        song.Title, string.Join(", ", song.Tags))
    );
  }
}

with the result similar as below sequential lines of MySong record:

$ dotnet run
[Cantaloupe Island, (60s, jazz)]
[Let It Be, (60s, rock)]
[Knockin' on Heaven's Door, (70s, rock)]
[Emotion, (70s, pop)]
[The River, ()]

How does it Works?

It is more convenience to show the result using Array.ForEach, followed by lambda function.

  public void Show() {
    Array.ForEach(songs,
      song => Console.WriteLine("[{0}, ({1})]",
        song.Title, string.Join(", ", song.Tags))
    );
  }

The last record is empty string.

      new MySong { 
        Title = "The River",
        Tags  = new string[] {}

Maybe Null

I also add Nullable option so Tags can accept null value, instead of just empty list.

using System;

#nullable enable

class Step04Options
{
  public record MySong {
    public string Title  { get; init; }  = null!;
    public string[]? Tags { get; init; } = null!;
  }; 

  private MySong[] songs;

  public Step04Options() {
    songs = new [] {
      new MySong { 
        Title = "Cantaloupe Island",
        Tags  = new string[] {"60s", "jazz"}
      },
      
      new MySong { 
        Title = "The River",
        Tags  = null
      },
    };
  }

  public void Show() {
    Array.ForEach(songs,
      song => Console.WriteLine("[{0}, ({1})]",
        song.Title, string.Join(", ",
          (song.Tags?? new string[] {})))
    );
  }
}

with the result similar as below sequential lines of MySong record:

❯ dotnet run
[Cantaloupe Island, (60s, jazz)]
[Let It Be, (60s, rock)]
[Knockin' on Heaven's Door, (70s, rock)]
[Emotion, (70s, pop)]
[The River, ()]

Using Nullable

We need to be examine this nullable carefully, or we may end up with compilation errors and warnings.

The most important part is we have to add pragma.

#nullable enable

Then we can add the ? operator in record type.

  public record MySong {
    public string Title  { get; init; }  = null!;
    public string[]? Tags { get; init; } = null!;
  }; 

Now we can declare null data for the last record.

      new MySong { 
        Title = "The River",
        Tags  = null

And to to show, we are still using Array.ForEach, followed by lambda function.

    Array.ForEach(songs,
      song => Console.WriteLine("[{0}, ({1})]",
        song.Title, string.Join(", ",
          (song.Tags?? new string[] {})))
    );

But this time we add default value.

  (song.Tags?? new string[] {})

This would result empty array whenever the field is null.

Separating Class

Since we need to reuse the songs record multiple times, it is a good idea to separate the record from logic.

using System;

#nullable enable

public record MySong {
  public string Title  { get; init; }  = null!;
  public string[]? Tags { get; init; } = null!;
}; 

class MySongs
{
  public MySong[] songs;

  public MySongs() {
    songs = new [] {
      new MySong { 
        Title = "Cantaloupe Island",
        Tags  = new string[] {"60s", "jazz"}
      },
      new MySong { 
        Title = "Let It Be",
        Tags  = new string[] {"60s", "rock"}
      },
      new MySong { 
        Title = "Knockin' on Heaven's Door",
        Tags  = new string[] {"70s", "rock"}
      },
      new MySong { 
        Title = "Emotion",
        Tags  = new string[] {"70s", "pop"}
      },
      new MySong { 
        Title = "The River",
        Tags  = null
      },
    };
  }
}

.NET C#: The MySongs Class

Using MySongs Class

Now we can have a very short code.

using System;

class Step05Class
{
  public void Show() {
    MySongs mySongs = new MySongs();
    MySong[] songs = mySongs.songs;

    Array.ForEach(songs,
      song => Console.WriteLine("[{0}, ({1})]",
        song.Title, string.Join(", ",
          (song.Tags?? new string[] {})))
    );
  }
}

.NET C#: Using MySongs Class


What is Next 🤔?

We are still far from finished. We need to conclude.

Consider continue reading [ C# - Playing with Records - Part Two ].