DEV Community

mohamed Tayel
mohamed Tayel

Posted on

Customizing Compiler-Generated Methods in C# Records with Full Code Examples

"Learn how to customize compiler-generated methods in C# records, including ToString, PrintMembers, Equals, and GetHashCode. This detailed guide features full code examples to help you enhance string representations, implement advanced equality logic, and tailor records to your specific needs."

C# records are a powerful feature for creating immutable data types with value-based equality. By default, the compiler generates several methods for records, including ToString, Equals, GetHashCode, and PrintMembers. While the default implementations are usually sufficient, certain scenarios may require customization to meet specific needs.

This article explores how to customize these compiler-generated methods with clear explanations and full code examples.


Overview of Compiler-Generated Methods in Records

When you define a record in C#, the compiler automatically provides:

  • ToString: Creates a string representation of the record, including its type name and properties.
  • Equals: Compares records based on their property values.
  • GetHashCode: Generates a hash code derived from the record's property values.
  • PrintMembers: Used internally by ToString to format the string representation of the record's properties.

These methods enable records to behave predictably and efficiently. However, there are times when the default behavior needs to be adjusted.


Customizing ToString

The ToString method provides a string representation of a record, including the type name and property values. You might want to customize this behavior for more readable or domain-specific output.

Example: Customizing ToString

Here’s how you can override ToString for a Product record and its derived record DiscountedProduct:

using System;

public record Product(int Id, string Name, decimal Price)
{
    public override string ToString() => $"Product: {Name} (ID: {Id}), Price: ${Price:F2}";
}

public record DiscountedProduct(int Id, string Name, decimal Price, decimal Discount) : Product(Id, Name, Price)
{
    public override string ToString() =>
        $"{base.ToString()}, Discount: ${Discount:F2}, Final Price: ${(Price - Discount):F2}";
}

class Program
{
    static void Main()
    {
        var product = new Product(1, "Book", 25.99m);
        var discountedProduct = new DiscountedProduct(2, "Laptop", 999.99m, 100m);

        Console.WriteLine(product);
        Console.WriteLine(discountedProduct);
    }
}
Enter fullscreen mode Exit fullscreen mode

Output:

Product: Book (ID: 1), Price: $25.99
Product: Laptop (ID: 2), Price: $999.99, Discount: $100.00, Final Price: $899.99
Enter fullscreen mode Exit fullscreen mode

By overriding ToString, you can tailor the string output to your specific requirements.


Customizing PrintMembers

The ToString method relies on the PrintMembers method to format the properties of a record. Customizing PrintMembers provides more control over how properties are represented in the output.

Example: Customizing PrintMembers

Here’s how you can override PrintMembers to control the output:

using System;
using System.Text;

public record Product(int Id, string Name, decimal Price)
{
    protected virtual bool PrintMembers(StringBuilder builder)
    {
        builder.Append($"ID = {Id}, Name = {Name}, Price = ${Price:F2}");
        return true;
    }

    public override string ToString()
    {
        var builder = new StringBuilder();
        builder.Append(GetType().Name).Append(" { ");
        if (PrintMembers(builder)) builder.Append(" }");
        return builder.ToString();
    }
}

public record DiscountedProduct(int Id, string Name, decimal Price, decimal Discount) : Product(Id, Name, Price)
{
    protected override bool PrintMembers(StringBuilder builder)
    {
        base.PrintMembers(builder);
        builder.Append($", Discount = ${Discount:F2}, Final Price = ${(Price - Discount):F2}");
        return true;
    }
}

class Program
{
    static void Main()
    {
        var product = new Product(1, "Book", 25.99m);
        var discountedProduct = new DiscountedProduct(2, "Laptop", 999.99m, 100m);

        Console.WriteLine(product);
        Console.WriteLine(discountedProduct);
    }
}
Enter fullscreen mode Exit fullscreen mode

Output:

Product { ID = 1, Name = Book, Price = $25.99 }
DiscountedProduct { ID = 2, Name = Laptop, Price = $999.99, Discount = $100.00, Final Price = $899.99 }
Enter fullscreen mode Exit fullscreen mode

This approach provides granular control over how properties are formatted when ToString is called.


Customizing Equals and GetHashCode

Customizing Equals and GetHashCode can be useful for advanced scenarios where the default value-based equality isn’t sufficient.

Example: Customizing Equals and GetHashCode

Here’s how you can customize these methods for the Product record:

using System;

public record Product(int Id, string Name, decimal Price)
{
    public override bool Equals(object? obj) =>
        obj is Product product &&
        Id == product.Id &&
        Name == product.Name &&
        Price == product.Price;

    public override int GetHashCode() => HashCode.Combine(Id, Name, Price);
}

class Program
{
    static void Main()
    {
        var product1 = new Product(1, "Book", 25.99m);
        var product2 = new Product(1, "Book", 25.99m);
        var product3 = new Product(2, "Laptop", 999.99m);

        Console.WriteLine(product1.Equals(product2)); // Output: True
        Console.WriteLine(product1.Equals(product3)); // Output: False
    }
}
Enter fullscreen mode Exit fullscreen mode

Output:

True
False
Enter fullscreen mode Exit fullscreen mode

This example ensures that equality is based only on the Id, Name, and Price properties.


When to Customize Methods in Records

Scenarios for Customization

  1. Logging: Provide detailed and clear information for debugging or monitoring.
  2. Improved Readability: Tailor the string representation to be user-friendly or domain-specific.
  3. Advanced Formatting: Include derived or computed values in the output.

Best Practices

  • Use ToString for general-purpose string representation.
  • Leverage PrintMembers for fine-grained control over string formatting.
  • Override Equals and GetHashCode only when necessary.

Conclusion

C# records simplify immutable data types by generating methods like ToString, Equals, GetHashCode, and PrintMembers. While the default implementations work for most use cases, customizing these methods can provide better control, especially for logging, debugging, and specific domain requirements.

The examples provided in this article demonstrate how to override and customize these methods effectively. Experiment with the code to explore further possibilities and tailor records to your needs.

Top comments (0)