Gentle introduction for generics (C#)
Tags: csharp, dotnetcore, tutorial, devops
Published: 2024-01-02 22:38:45

## Introduction

Generics are the most powerful feature of C#. Generics allow you to define type-safe data structures, without committing to actual data types. This results in a significant performance boost and higher quality code because you get to reuse data processing algorithms without duplicating type-specific code.

Generics allow you to define type-safe classes without compromising type safety, performance, or productivity. You implement the server only once as a generic server, while at the same time you can declare and use it with any type.

Above from [Microsoft](https://learn.microsoft.com/en-us/previous-versions/ms379564(v=vs.80)?redirectedfrom=MSDN)

To do that, use the **<** and **>** brackets, enclosing a generic type parameter.

Example, to deserialize a json string the following can be used for any model.

```csharp
public class JsonHelpers
{
    /// <summary>
    /// Read json from string with converter for reading decimal from string
    /// </summary>
    /// <typeparam name="T">Type to convert</typeparam>
    /// <param name="json">valid json string for <see cref="T"/></param>
    public static List<T>? Deserialize<T>(string json)
    {

        JsonSerializerOptions options = new(JsonSerializerDefaults.Web)
        {
            WriteIndented = true
        };

        return JsonSerializer.Deserialize<List<T>>(json, options);

    }
}
```

To deserialize a list of Product.

```csharp
public class Product
{
    public int ProductId { get; set; }
    public string ProductName { get; set; }
    public decimal? UnitPrice { get; set; }
    public short? UnitsInStock { get; set; }
}
```

**Usage**

```csharp
var json = File.ReadAllText(fileName);
var products = Deserialize<Product>(json);
```

If instead there were a Customer class

**Usage**

```csharp
var json = File.ReadAllText(fileName);
var products = Deserialize<Customer>(json);
```

To many developers, learning how to best work with generics can be difficult without solid real life code samples. In this article see several code samples that go from non-generic to generic.

Most of the code samples are in Windows Forms project as using Windows Forms tends to be easier to learn from even if Windows Forms are not used. There is also one Razor Pages project which demonstrates using generics and one console project for non-generics verses generic counterparts.

## In a hurry

Here is the [code](https://github.com/karenpayneoregon/generic-basics-csharp).

## Example  1

The following code sample (a console project) uses [Spectre.Console](https://www.nuget.org/packages/Spectre.Console/0.48.1-preview.0.7) NuGet package to provide easy methods for gathering user input like first and last name of type string or perhaps birth date for a DateOnly property.

Non-generic method using [Text Prompt](https://spectreconsole.net/prompts/text) which expects the type to obtain information to get first and last name along with birth date we need three methods.

```csharp
public static string GetFirstName() =>
    AnsiConsole.Prompt(
        new TextPrompt<string>("[white]First name[/]?")
            .PromptStyle("yellow")
            .AllowEmpty());

public static string GetLastName() =>
    AnsiConsole.Prompt(
        new TextPrompt<string>("[white]Last name[/]?")
            .PromptStyle("yellow")
            .AllowEmpty());

public static DateOnly? GetBirthDate() =>
    AnsiConsole.Prompt(
        new TextPrompt<DateOnly>("What is your [white]birth date[/]?")
            .PromptStyle("yellow")
            .AllowEmpty());
```

Instead let's look at a generic way, one method instead of three.

```csharp
public static T GetInput<T>(string text) => 
    AnsiConsole.Prompt(new TextPrompt<T>($"[white]{text}[/]?")
        .AllowEmpty()
        .PromptStyle("yellow"));
```


![Layout of generic](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qzpszvg33hr13mznihue.png)

**Usage**

Here we pass the type to GetInput and pass in the prompt.

```csharp
var firstName = Prompts.GetInput<string>("First name");
var lastName = Prompts.GetInput<string>("Last name");
var birthDate = Prompts.GetInput<DateOnly>("Birth date");
```
## Example 2

In a Windows Forms project there is a CheckedListBox populated via the DataSource property with a list of Company.

```csharp
public class Company
{
    public int Id { get; set; }
    public string Name { get; set; }
}
```
A novice will first type the following to get checked companies but will not return the id property.

```csharp
List<Company> list = [];
for (int index = 0; index < CompaniesCheckedListBox.Items.Count; index++)
{
    if (CompaniesCheckedListBox.GetItemChecked(index))
    {
        list.Add(new Company() { Name = CompaniesCheckedListBox.Items[index].ToString()! });
    }
}
```

A intermediate developer will use the following but is still tied to a type of Company.

```csharp
public static class CheckedListBoxExtensions
{
    public static List<Company> CheckedCompanies(this CheckedListBox sender)
        => sender.Items.Cast<Company>()
            .Where(( _ , index) => sender.GetItemChecked(index))
            .Select(item => item)
            .ToList();
}
```

If the CheckedListBox were populated with a Product class we need the following.

```csharp
    public static List<Product> CheckedProducts(this CheckedListBox sender)
    => sender.Items.Cast<Product>()
        .Where((item, index) => sender.GetItemChecked(index))
        .Select(item => item)
        .ToList();
}
```

In both cases, generics to the rescue!!!

```csharp
public static class CheckedListBoxExtensions
{
    public static List<T> CheckedList<T>(this CheckedListBox sender)
        => sender.Items.Cast<T>()
            .Where((item, index) => sender.GetItemChecked(index))
            .Select(item => item)
            .ToList();
}
```

**Usage**

```csharp
List<Company> result = CompaniesCheckedListBox.CheckedList<Company>();

List<Product> result = ProductCheckedListBox.CheckedList<Product>();
```
## Abstraction from controls

In the last set of examples language extension methods were used on a specific control, a CheckedListBox. Let's change to thinking ListBox or DataGridView for a change.

The task is to save the contents to a json file. If either control DataSource is bound to a BindingList**<T>** which keeping with the above could be a list of Company or Product, instead of creating a language extension for a control we create one for a BindingList**<T>**.

```csharp
public static class BindingListExtensions
{
    public static void SaveToFile<T>(this BindingList<T> sender, string FileName)
    {
        File.WriteAllText(FileName, JsonSerializer.Serialize(sender, new JsonSerializerOptions
        {
            WriteIndented = true
        }));
    }

    public static void SaveToFile1<T>(this BindingList<T> sender, string FileName)
    {

        JsonSerializerOptions options = new()
        {
            WriteIndented = true
        };

        File.WriteAllText(FileName, JsonSerializer.Serialize(sender, options));
    }
}
```

**Usage**

Here is does not matter what control uses the BindingList

```csharp
protected BindingList<Product> _bindingListRight = new();
.
.
.
var fileName = "ProductsList.json";
_bindingListRight.SaveToFile<Product>(fileName);
```

## Example 3

The following class provides generic extension methods for writing and reading temp data in ASP.NET Core/Razor Pages.

```csharp
public static class TempDataHelper
{
    public static void Put<T>(this ITempDataDictionary sender, string key, T value) where T : class
    {
        sender[key] = JsonSerializer.Serialize(value);
    }

    public static T Get<T>(this ITempDataDictionary sender, string key) where T : class
    {
        sender.TryGetValue(key, out var unknown);
        return unknown == null ? null : JsonSerializer.Deserialize<T>((string)unknown);
    }
}
```
Rather than stepping through usage, run the project WorkingWithDateTime ASP.NET Core project which on submit on the index page post sets temp data and opens another page which reads the data back.

## Consistency 

Another great use for generics is consistency in naming methods in classes that perform various operations.

Example, for adding a new record, one developer writes a method AddNewRecord while another developer names the method Add. This makes it difficult to easily work with the code base. Instead use an interface to enforce, in this case method names.

Sample generic interface

```csharp
public interface IOperations<T> where T : class
{
    IEnumerable<T> GetAll();
    Task<List<T>> GetAllAsync();
    T GetById(int id);
    T GetByIdWithIncludes(int id);
    Task<T> GetByIdAsync(int id);
    Task<T> GetByIdWithIncludesAsync(int id);
    bool Remove(int id);
    void Add(in T sender);
    void Update(in T sender);
    int Save();
    Task<int> SaveAsync();
}
```

Going with a Company model

```csharp
public class Company
{
    public int Id { get; set; }
    public string Name { get; set; }
    public override string ToString() => Name;

    // for Bogus
    public Company(int id)
    {
        Id = id;
    }

}
```

Create a class for working with companies and implement IOperations.

```csharp
public class CompanyOperations : IOperations<Company>
{
}
```

Visual Studio will prompt to implement missing members and in return we get the following read to write code.

```csharp
public class CompanyOperations : IOperations<Company>
{
    public IEnumerable<Company> GetAll()
    {
        throw new NotImplementedException();
    }

    public Task<List<Company>> GetAllAsync()
    {
        throw new NotImplementedException();
    }

    public Company GetById(int id)
    {
        throw new NotImplementedException();
    }

    public Company GetByIdWithIncludes(int id)
    {
        throw new NotImplementedException();
    }

    public Task<Company> GetByIdAsync(int id)
    {
        throw new NotImplementedException();
    }

    public Task<Company> GetByIdWithIncludesAsync(int id)
    {
        throw new NotImplementedException();
    }

    public bool Remove(int id)
    {
        throw new NotImplementedException();
    }

    public void Add(in Company sender)
    {
        throw new NotImplementedException();
    }

    public void Update(in Company sender)
    {
        throw new NotImplementedException();
    }

    public int Save()
    {
        throw new NotImplementedException();
    }

    public Task<int> SaveAsync()
    {
        throw new NotImplementedException();
    }
}
```

Same goes for other models.

## Using constraints

Constraints inform the compiler about the capabilities a type argument must have. Without any constraints, the type argument could be any type.

Examples taken from provided source code. All extension methods must implement [INumber](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.inumber-1?view=net-8.0)&lt;T&gt;.

So we can only use the following extension methods on numerics. And if this is not understood than a developer might use GetFractionalPart3 which uses int.Parse which not necessary, instead simply do a conversion using Convert.ToInt32 as done in GetFractionalPart2. The point here is to understand that if there is a possibility of null we use Convert.

```csharp
public static class Extensions
{
    // Get fractional part of a number as an integer
    public static int GetFractionalPart1<T>(this T sender) where T : INumber<T>
    {
        int value = (int)(decimal.CreateChecked(sender) % 1 * 100);
        return int.IsNegative(value) ? value.Invert() : value;
    }

    // Get fractional part of a number as an integer
    public static int GetFractionalPart2<T>(this T sender) where T : INumber<T>
    {
        var value = Convert.ToInt32((decimal.CreateChecked(sender) % 1)
            .ToString(CultureInfo.InvariantCulture).Replace("0.", ""));
        return int.IsNegative(value) ? value.Invert() : value;
    }

    // Get fractional part of a number as an integer
    public static int GetFractionalPart3<T>(this T sender) where T : INumber<T>
    {
        var value = int.Parse((decimal.CreateChecked(sender) % 1)
            .ToString(CultureInfo.InvariantCulture).Replace("0.", ""));
        return int.IsNegative(value) ? value.Invert() : value;
    }


    // Get fractional part of a number as a decimal
    public static decimal GetFractionalPart<T>(this T sender, int places) where T : INumber<T>
        => Math.Round(decimal.CreateChecked(sender) - 
                      Math.Truncate(decimal.CreateChecked(sender)), places);

    public static T Invert<T>(this T source) where T : INumber<T>
        => -source;
}
```

## Summary

Generics should be used when it makes sense, not just to use generics. Take time to study the code provided in the GitHub repository, step through the code to get a better understanding of what has been presented. Once comfortable consider places that can benefit from using generics.

Code provided with this article goes beyond what has been presented so take time to study all of the code.

## Source code

Clone the following [GitHub repository](https://github.com/karenpayneoregon/generic-basics-csharp) which has code written using Microsoft Visual Studio 2022 NET8 Framework.

## See also

[Gentle introduction to Generic Repository Pattern with C#](https://dev.to/karenpayneoregon/gentle-introduction-to-generic-repository-pattern-with-c-1jn0)