Code Housekeeping refers to general rules of thumb that make code easier to read, digest, and modify for other developers, yourself included.

A very common task that you will be required to do is to hydrate a type from the database and then use that downstream, either in an API or for binding to a user interface.

Take for example this Car type, that is modeled for the database.

public sealed class Car
{
    public required int Id { get; set; }
    public required string Make { get; set; }
    public required string Model { get; set; }
    public required int Capacity { get; set; }
    public required int YearOfManufacture { get; set; }
}

We then have some sort of data transfer object, DTO, that we use for UI binding.

public sealed class CarDTO
{
    public required int Id { get; set; }
    public required string Make { get; set; }
    public required string Model { get; set; }
    public required int Capacity { get; set; }
    public required int YearOfManufacture { get; set; }
}

Our code will then look like this:

var subaru = new Car
{
    Id = 1,
    Capacity = 2000,
    Make = "Subaru",
    Model = "Outback",
    YearOfManufacture = 2026
};

// Loaded initially
var firstDTO = new CarDTO
{
    Id = subaru.Id,
    Capacity = subaru.Capacity,
    Make = subaru.Make,
    Model = subaru.Model,
    YearOfManufacture = subaru.YearOfManufacture
};

// Loaded sepearately
var secondDTO = new CarDTO
{
    Id = subaru.Id,
    Capacity = subaru.Capacity,
    Make = subaru.Make,
    Model = subaru.Model,
    YearOfManufacture = subaru.YearOfManufacture
};

Here, for whatever reason, we are loading the Car in two different contexts.

A problem arises when we do this:

// Check if the cars are the same

if (firstDTO == secondDTO)
{
    Console.WriteLine("This is the same vehicle");
}
else
{
    Console.WriteLine("These are different vehicles");
}

This will print the following:

These are different vehicles

recordvsClass

This is because as far as the runtime is concerned, these are different things despite having identical properties.

As discussed in this post, “Customizing Object Equality In C# & .NET”, you have to do extra work to get the runtime to understand what you mean by equality.

public sealed class CarDTO : IEquatable<CarDTO>
{
    public required int Id { get; set; }
    public required string Make { get; set; }
    public required string Model { get; set; }
    public required int Capacity { get; set; }
    public required int YearOfManufacture { get; set; }

    public bool Equals(CarDTO? other)
    {
        if (other is null)
            return false;

        if (ReferenceEquals(this, other))
            return true;

        return Id == other.Id &&
               Make == other.Make &&
               Model == other.Model &&
               Capacity == other.Capacity &&
               YearOfManufacture == other.YearOfManufacture;
    }

    public override bool Equals(object? obj)
        => Equals(obj as CarDTO);

    public override int GetHashCode()
        => HashCode.Combine(Id, Make, Model, Capacity, YearOfManufacture);

    public static bool operator ==(CarDTO? left, CarDTO? right)
    {
        if (left is null)
            return right is null;

        return left.Equals(right);
    }

    public static bool operator !=(CarDTO? left, CarDTO? right)
        => !(left == right);
}

In this case, we have implemented the IEquatable interface and provided implementations for:

  1. Equals
  2. GetHashCode
  3. ==
  4. !==

The code now runs as expected.

recordVsClass2

This is a lot of unnecessary work.

You can enjoy the same benefits by simply making the DTO a record, rather than a class.

public sealed record CarDTO
{
    public required int Id { get; set; }
    public required string Make { get; set; }
    public required string Model { get; set; }
    public required int Capacity { get; set; }
    public required int YearOfManufacture { get; set; }
}

It is important to note that a record is still a class, just with some handy enhancements injected by the compiler.

TLDR

For data transfer scenarios, use record rather than class.

The code is in my GitHub.

Happy hacking!