Implicit and explicited operators are provided as a means of converting one datatype to another.

// this is an implicit conversion from an int to a double
int i = 8;
double d = i;

// this is an explicit conversion from a double to an int
double d = 8.8;
int i = (int) d;

I recently learned that you can implement your own implicit/explicit operators to convert custom classes, which is useful when needing to convert between API, domain and storage objects in an application.

Take the followng Person class as an example.

public sealed class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public string FullName
        => $"{FirstName} {LastName}";
    
    public DateOnly DateOfBirth { get; set; }
    
    // obviously not accurate, just go with it
    public int Age
        => DateOnly.FromDateTime(DateTime.Today).Year - DateOfBirth.Year;
}

If we create a new instance of a Person and use sqlite-net-pcl to write it into a SQLite database, we’ll see that it fails because SQLite does not understand the DateOnly type. There are also plenty of other real-life examples as to why we want to use different classes in each part of our application.

So instead we’d want a PersonDto class, which could look something like this:

[Table("people")]
public sealed class PersonDto
{
    [Column("first_name")]
    public string FirstName { get; set; }
    
    [Column("last_name")]
    public string LastName { get; set; }
    
    [Column("dob")]
    public int DateOfBirth { get; set; }
}

So first we create a Person, convert it to a PersonDto, and then write it to the database. The long way could be:

var path = Path.Combine(Directory.GetCurrentDirectory(), "people.db");

var conn = new SQLiteAsyncConnection(path);
await conn.CreateTableAsync<PersonDto>();

var person = new Person
{
    FirstName = "Charles",
    LastName = "Dickens",
    DateOfBirth = new DateOnly(1812, 2, 7)
};

var dto = new PersonDto
{
    FirstName = person.FirstName,
    LastName = person.LastName,
    DateOfBirth = person.DateOfBirth.DayNumber
};

await conn.InsertAsync(person);

We could then read the PersonDto back from the database, convert it back to a Person, and carry out any business operations on it.

dto = await conn.Table<PersonDto>().FirstAsync();

person = new Person
{
    FirstName = dto.FirstName,
    LastName = dto.LastName,
    DateOfBirth = DateOnly.FromDayNumber(dto.DateOfBirth)
};

Console.WriteLine($"{person.FullName} is {person.Age} years old.");
Charles Dickens is 210 years old.

Obviously, manually mapping between Person and PersonDto every time is a tiresome process. Some projects, such as AutoMapper try to solve this for you, but sometimes you don’t need or want more external packages. This is where the implicit/explicit operators can help you out.

Here’s an example of using an implicit operator to convert a Person to a PersonDto.

[Table("people")]
public sealed class PersonDto
{
    ...

    public static implicit operator PersonDto(Person person)
    {
        return new PersonDto
        {
            FirstName = person.FirstName,
            LastName = person.LastName,
            DateOfBirth = person.DateOfBirth.DayNumber
        };
    }
}

This would allow us to convert a Person to a PersonDto by simply changing the variable type, like so:

var person = new Person
{
    FirstName = "Charles",
    LastName = "Dickens",
    DateOfBirth = new DateOnly(1812, 2, 7)
};

PersonDto dto = person;

Here’s an example of using an implicit operator to convert a PersonDto back to a Person.

[Table("people")]
public sealed record PersonDto
{
    ...

    public static explicit operator Person(PersonDto dto)
    {
        return new Person
        {
            FirstName = dto.FirstName,
            LastName = dto.LastName,
            DateOfBirth = DateOnly.FromDayNumber(dto.DateOfBirth)
        };
    }
}

This would allow us to convert a PersonDto to a Person using a cast.

dto = await conn.Table<PersonDto>().FirstAsync();
person = (Person) dto;

This makes your domain-level code much more readable and easier to work with. You can use multiple converstions in your class as long as they have different method signatures. For instance, you wouldn’t be able to declare both an implicit and explicit operator like this:

public static implicit operator PersonDto(Person person)
{
    ...
}

public static explicit operator PersonDto(Person person)
{
    ...
}

What’s the difference between implicit and explicit, and where should you use each one? At the beginning of this blog, we had the following two examples:

// implicit
int i = 8;
double d = i;

// explicit
double d = 8.8;
int i = (int) d;

The only real difference has to do with whether or not there’s a loss of data in the conversion.

Since an int is less accurate than an a double, there is no problem with converting it. 8 as an int is also just 8 when converted to a double. However, the reverse is not true. 8.8 as a double becomes only 8 when converted to an int, so we lose the .8 of precision. The compiler requires us to do the cast to make us damn aware that this is happening.

You should always stick to this rule when implementing these conversions. In my scenario above, it does not make sense to use explicit converstions since no data loss is occuring. In which case, the following would work just fine:

[Table("people")]
public sealed class PersonDto
{
    [Column("first_name")]
    public string FirstName { get; set; }
    
    [Column("last_name")]
    public string LastName { get; set; }
    
    [Column("dob")]
    public int DateOfBirth { get; set; }

    public static implicit operator PersonDto(Person person)
    {
        return new PersonDto
        {
            FirstName = person.FirstName,
            LastName = person.LastName,
            DateOfBirth = person.DateOfBirth.DayNumber
        };
    }

    public static implicit operator Person(PersonDto dto)
    {
        return new Person
        {
            FirstName = dto.FirstName,
            LastName = dto.LastName,
            DateOfBirth = DateOnly.FromDayNumber(dto.DateOfBirth)
        };
    }
}