Skip to Content
Course content

212: Anonymous Types Revisited

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've likely already used anonymous types in LINQ queries to grab just a few columns from a database. They're great for that. But as you move from simple queries into building actual business logic, there's a temptation to use them everywhere because they save you from writing a class definition. I've seen this a lot in pull requests: developers trying to "shortcut" their way through a project by passing anonymous types between methods. It feels efficient until it doesn't.

The "Object" Trap

Let's say you're building a reporting tool for an e-commerce site. You want a method that pulls a quick snapshot of a customer's order total and their current loyalty tier. The naive approach looks like this:

public object GetCustomerSnapshot(int customerId)
{
    // Imagine this is a database call
    return new { CustomerName = "Jane Doe", TotalSpent = 1250.50m, Tier = "Gold" };
}

// Usage:
var snapshot = GetCustomerSnapshot(123);
// Console.WriteLine(snapshot.CustomerName); // This won't compile!

Here is the problem: anonymous types are internal to the method where they are created. The moment you try to return one, the compiler has no choice but to treat it as a generic object. You've just erased all your type safety. To actually get the CustomerName back, you'd have to use reflection or the dynamic keyword, both of which are an invitation for runtime crashes and a complete loss of IntelliSense. You've traded a few lines of boilerplate for a maintenance nightmare.

The Tuple Compromise

If you're just passing a few values between two private methods and you really don't want to pollute your namespace with a one-off class, don't reach for anonymous types. Reach for ValueTuples. It's the "middle ground" that gives you the brevity of an anonymous type but keeps the type system intact.

public (string Name, decimal Total, string Tier) GetCustomerSnapshot(int customerId)
{
    return ("Jane Doe", 1250.50m, "Gold");
}

// Usage:
var snapshot = GetCustomerSnapshot(123);
Console.WriteLine(snapshot.Name); // This works perfectly.

I generally suggest this for internal helper methods. It's clean, it's fast, and the compiler knows exactly what's inside the tuple. However, tuples start to smell once you have more than three or four properties, or if that data needs to leave your current service and head toward a UI layer or an external API.

When to Commit to a Record

Once the data represents a "thing" in your business domain—like a CustomerSnapshot—you need to stop hacking and start modeling. This is where C# records come in. They provide the same conciseness as anonymous types (single-line declaration) but give you a first-class type that can be passed anywhere in your application.

public record CustomerSnapshot(string Name, decimal TotalSpent, string Tier);

public CustomerSnapshot GetCustomerSnapshot(int customerId)
{
    return new CustomerSnapshot("Jane Doe", 1250.50m, "Gold");
}

The trade-off here is a tiny bit more typing upfront. But in exchange, you get a named type that appears in your API documentation, is easily unit-testable, and won't break the moment you decide to move the logic into a different file. Use anonymous types for the .Select() in your LINQ query, use tuples for quick internal hand-offs, and use records for everything else.




📋 Practical Task

Refactoring the Order Summary Service

You have inherited a piece of code where a developer tried to be "clever" by returning anonymous types from a service method, resulting in the use of dynamic to access the data. This is causing intermittent runtime errors.

Your Task: Refactor the following code. Create a record named OrderSummary to replace the anonymous type. Update the GetOrderSummary method to return this record instead of object, and remove the dynamic keyword from the calling code to restore compile-time type safety.

public class OrderService 
{
    public object GetOrderSummary(int orderId) 
    {
        // Simulated data retrieval
        return new { OrderId = orderId, Total = 99.99m, Status = "Shipped" };
    }
}

public class Program 
{
    public static void Main() 
    {
        var service = new OrderService();
        dynamic summary = service.GetOrderSummary(101); 
        System.Console.WriteLine($"Order {summary.OrderId} is {summary.Status}");
    }
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.