C# User-defined explicit and implicit conversion operators
Tags: csharp, dotnetcore, devops, productivity
Published: 2024-01-01 21:59:14

Learn about explicit and implicit conversion operators with practical examples for C#12 and higher.

## Source code

Clone the following [GitHub repository](https://github.com/karenpayneoregon/conversion-operators-csharp).

## Definitions
A user-defined type can define a custom implicit or explicit conversion from or to another type. 

Implicit conversions don't require special syntax to be invoked and can occur in various situations, for example, in assignments and methods invocations. Predefined C# implicit conversions always succeed and never throw an exception. User-defined implicit conversions should behave in that way as well. If a custom conversion can throw an exception or lose information, define it as an explicit conversion.

(above is from [Microsoft](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/user-defined-conversion-operators))

## Important 

In several cases, classes are [aliased](https://dev.to/karenpayneoregon/working-with-the-using-directive-in-c-14lp) in the project file so that, in this case ProductItem class can have different definitions without the need for a long path to the specific model for a specific code sample.

```xml
<ItemGroup>
   <Using Include="OperatorSamples.Models1" Alias="M1" />
   <Using Include="OperatorSamples.Models2" Alias="M2" />
   <Using Include="OperatorSamples.Models3" Alias="M3" />
</ItemGroup>
```

[INotifyPropertyChanged](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.inotifypropertychanged?view=net-8.0) has nothing to do with the conversions, just good practice for change notification.

:bulb: Check out [System.Convert](https://learn.microsoft.com/en-us/dotnet/api/system.convert?view=net-8.0) for conversion methods in the Framework.

Console and Windows Forms projects are used to reach a wide audience rather than web or MAUI. 

## Example 1

We want to convert from Celsius to Fahrenheit using a conversion between the two types implicitly.

Base class which has a single property other classes inherit.

```csharp
public class Temperature
{
    public float Degrees { get; set; }
}
```

Below **Celsius** utilizes an _implicit operator_ to convert to **Fahrenheit** by passing in itself and a conversion is done. The opposite is done for the **Fahrenheit** class to **Celsius**.



```csharp
public class Celsius : Temperature
{
    public Celsius(float sender)
    {
        Degrees = sender;
    }

    public static implicit operator Fahrenheit(Celsius sender) 
        => new((9.0f / 5.0f) * sender.Degrees + 32);
}

public class Fahrenheit : Temperature
{
    public Fahrenheit(float sender)
    {
        Degrees = sender;
    }

    public static implicit operator Celsius(Fahrenheit sender) 
        => new((5.0f / 9.0f) * (sender.Degrees - 32));
}
```

**Example usage**

```csharp
private static void TemperatureExample()
{
    Celsius celsius1 = new(10);
    Fahrenheit fahrenheit = celsius1;
    Celsius celsius2 = fahrenheit;
}
```

![Shows results from Visual Studio local window](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wn7w56y0by36di0ute5w.png)

:bulb: A similar conversion might be currency conversion.

## Example 2

Working with paths for files, here we define to implicit operators which encapsulates commonly used methods, does the path exists and to list, in this case all .csproj files in the current solution.

Base class

```csharp
public class FilePath(string path)
{
    public static implicit operator string(FilePath self) => self?.ToString();
    public static implicit operator FilePath(string value) => new(value);
    public override string ToString() => path;
}
```

**Wrapper class**

```csharp
public class PathOperations
{
    public void PrintFolderContents(string path)
    {
        GlobbingOperations.TraverseFileMatch += GlobbingOperations_TraverseFileMatch;
        GlobbingOperations.GetProjectFilesAsync(path);
    }

    private void GlobbingOperations_TraverseFileMatch(FileMatchItem sender)
    {
        Console.WriteLine(Path.GetFileNameWithoutExtension(sender.FileName));
    }

    public void PathExists(FilePath path)
    {
        AnsiConsole.MarkupLine(Path.Exists(path)
            ? $"[cyan]{nameof(PathExists)}[/] [yellow]{path}[/] [cyan]exists[/]"
            : $"[cyan]{nameof(PathExists)}[/] [yellow]{path}[/] [cyan]does not exists[/]");
    }
}
```

**Usage**

There is a lot going on under the covers that is not important to the conversions, for example, using [globbing](https://learn.microsoft.com/en-us/dotnet/core/extensions/file-globbing).


```csharp
private static void FilePathExample()
{

    PathOperations pathOps = new();
    pathOps.PathExists(DirectoryOperations.GetSolutionInfo().FullName);
    pathOps.PathExists(new FilePath(DirectoryOperations.GetSolutionInfo().FullName));
    pathOps.PrintFolderContents(DirectoryOperations.GetSolutionInfo().FullName);
    pathOps.PrintFolderContents(new FilePath(DirectoryOperations.GetSolutionInfo().FullName));
    
}
```

![Result of the above operation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l2jpuo8595duqdyedpq0.png)

## Example 3

In this example, a DataGridView is populated with a list with property change notification as follows with a DataGridViewCheckBoxColumn which is not and should not be part of the model.


![Shows the DataGridView with a checkbox column](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8xd839tn6nbjuqzpiouz.png)

```csharp
public class Product : INotifyPropertyChanged
{
    private int _productId;
    private string _productName;
    private decimal _unitPrice;
    private short _unitsInStock;

    public int ProductId
    {
        get => _productId;
        set
        {
            if (value == _productId) return;
            _productId = value;
            OnPropertyChanged();
        }
    }

    public string ProductName
    {
        get => _productName;
        set
        {
            if (value == _productName) return;
            _productName = value;
            OnPropertyChanged();
        }
    }

    public decimal UnitPrice
    {
        get => _unitPrice;
        set
        {
            if (value == _unitPrice) return;
            _unitPrice = value;
            OnPropertyChanged();
        }
    }

    public short UnitsInStock
    {
        get => _unitsInStock;
        set
        {
            if (value == _unitsInStock) return;
            _unitsInStock = value;
            OnPropertyChanged();
        }
    }

    public Product(int id)
    {
        ProductId = id;
    }

    public Product()
    {
        
    }

    public override string ToString() => ProductName;

    public event PropertyChangedEventHandler? PropertyChanged;
    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null!)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

}
```

Since there is a need to remember checked products we use the following class so not to taint the base product class by having a Process property.

```csharp
public class ProductContainer : Product
{
    private bool _process;

    public ProductContainer(int id)
    {
        ProductId = id;
    }

    public ProductContainer()
    {
        
    }

    public bool Process
    {
        get => _process;
        set
        {
            if (value == _process) return;
            _process = value;
            OnPropertyChanged();
        }
    }
}
```

A list is populated with NuGet package [Bogus](https://www.nuget.org/packages/Bogus).

```csharp
public static List<ProductContainer> Products1(int productCount = 50)
{
    int identifier = 1;
    Faker<ProductContainer> fake = new Faker<ProductContainer>()
        .CustomInstantiator(f => new ProductContainer(identifier++))
        .RuleFor(p => p.ProductName, f => f.Commerce.ProductName())
        .RuleFor(p => p.UnitPrice, f => f.Random.Decimal(10, 2))
        .RuleFor(p => p.UnitsInStock, f => f.Random.Short(1, 5));

    return fake.Generate(productCount).OrderBy(x => x.ProductName).ToList();
}
```
### implicit operator

We do not want the Process property so a class shown below uses an implicit operator to only get the other properties, in short the implicit operator is allowing for a simple mapping.

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

    public static implicit operator ProductItem(Product product) =>
        new()
        {
            ProductId = product.ProductId,
            ProductName = product.ProductName,
            UnitPrice = product.UnitPrice,
            UnitsInStock = product.UnitsInStock,
        };
}
```

**Usage**

Since the DataGridView has a BindingList as the DataSource we can get checked items directly from the BindingList as follows.

```csharp
List<ProductContainer> products = 
    _bindingList.Where(pc => pc.Process).ToList();
```

Then using the following to get checked items without the Process property which without the **implicit operator** would not be possible.

```csharp
List<ProductItem> results = products
    .Select<Product, ProductItem>(container => container).ToList();
```


![For above code](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/21jdvhp5d1us7y2w0hu9.png)

Finally the resulting list is written to a json file and note the converter.

```csharp
            File.WriteAllText("Products.json", JsonSerializer.Serialize(results, new JsonSerializerOptions
            {
                WriteIndented = true,
                Converters = { new FixedDecimalJsonConverter() }
            }));
```

Which writes decimals as a string.

```json
  {
    "ProductId": 41,
    "ProductName": "Awesome Soft Shoes",
    "UnitPrice": "8.16",
    "UnitsInStock": 2
  }
```

To Deserialize, use the following generic method.

```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);

    }
}
```
Similarly for **explicit operator** the following would be used.

```csharp
List<ProductItem> results = products.Select(pc => (ProductItem)pc).ToList();
```