10

I want immutable anonymous types with named members that can be passed around, compared and recognized -- a merging of Tuples and anonymous types. This doesn't exist, and I realize that.

So the question is: What is a good idiomatic substitute for this using C#4 or 5?

The use case is fluid LINQ data processing from heterogeneous data sources. In a word, ETL in C#. I do some pretty complex analysis and data comes from multiple platforms and sources. It's not an option to "just put it all on the same platform and use Entity Framework". I want to able to fluidly pass around what are essentially arbitrary records -- immutable named sets of read only properties.

The only thing I can come up with short of creating a custom immutable POCO for every single otherwise-anonymous type is to use attributes to add compiled annotations to methods which return Tuples. Certainly it wouldn't be hard to write a code-generator for spitting out the immutable POCOs, but I hate how that clutters up the object model of a project. Using dynamic completely erases all the performance and design-time usefulness of static typing, especially if composing further queries from the results of other methods, so I don't consider it a viable solution.

// My idea: Adding a attribute to methods to at least record the names
// of the "columns" of a Tuple at a method level
public class NamedTupleAttribute : Attribute {
    public string[] Names { get; private set; }
    public NamedTupleAttribute(string[] Names) { this.Names = Names; }
}

// Using NamedTuple attribute to note meaning of members of Tuple
[NamedTuple(new[] { "StoreNumber", "Gross", "Cost", "Tax" })]
public IEnumerable<Tuple<int, decimal, decimal, decimal>> GetSales { ... }

What I want (Imaginary MSDN documentaion for C# 6):

duck (C# reference)

The duck keyword allows anonymous types to be used in all the statically-typed features of C#. Like normal anonymous types, the compiler will treat anonymous types with the same number, names, and types of properties as having the same type. However, the duck keyword also allows these types to be used in member declarations and as type parameters for generic types.

1. duck type instances

Like anonymous types, instances of duck type objects can only be created using an object initializer without a type name. The syntax is the same as for a normal anonymous type except that the keyword duck is added after the new operator:

var record = new duck { StoreNumber=1204,
                        Transaction=410, 
                        Date=new DateTime(2012, 12, 13), 
                        Gross=135.12m, 
                        Cost=97.80m,
                        Tax=12.11m };

2. duck type references

Duck types can be referenced using a duck type literal, a duck type alias, or implicitly when the return type of a property or method can be inferred.

2.1 duck type literals

A duck type can be expressed with a type literal, which can be used in the place of any type reference. Duck type literals consist of the keyword duck followed by a list of name - type identifier pairs just as in a parameter list of a method, except enclosed in curly braces:

// A duck type literal:
duck { int StoreNumber, int Transaction, DateTime Date, decimal Gross, decimal Cost, decimal Tax }

// In a member declaration:
public duck { int StoreNumber, int Transaction, DateTime Date, 
              decimal Gross, decimal Cost, decimal Tax } GetTransaction(...) { ... }

// As a type parameter:
var transactions = new List<duck { int StoreNumber, int Transaction, DateTime Date, 
                                   decimal Gross, decimal Cost, decimal Tax }>();
2.2 duck type aliases

You can assign an alias to a duck type with a using directive immediately after the namespace using directives in a C# code file or namespace. The alias may then be used in the place of any type reference.

// Namespace directives:
using System;
using Odbc = System.Data.Odbc;

// duck type aliases:
using TransTotal = duck { int StoreNumber, int Transaction, DateTime Date, 
                           decimal Gross, decimal Cost, decimal Tax };

// Member declaration:
public TransTotal GetTransaction(...) { ... }

// As a type parameter:
var transactions = new List<TransTotal>();
2.3 Inferred duck types

If the return type of a property or method can be inferred, the body of a duck type literal may be omitted in a member declaration:

// Long form:
public duck { int StoreNumber, int Transaction, DateTime Date, 
              decimal Gross, decimal Cost, decimal Tax } GetDummyTransaction() {
    return new duck { ... };
}

// Short form:
public duck GetDummyTransaction() {
    return new duck { ... };
}

// Short form as a type parameter:
public IEnumerabe<duck> GetTransactions(...) {
    return 
        from record in someProvider.GetDetails(...)
        where ((DateTime)record["Date"]).Date == someDate
        group record by new {
            StoreNumber = (int)record["Number"],
            Transaction = (int)record["TransNum"],
            Date = (DateTime)record["Date"]
        } into transTotal
        select new duck {
            transTotal.Key.StoreNumber,
            transTotal.Key.Transaction,
            transTotal.Key.Date,
            Gross = transTotal.Sum(x => (decimal)x["Gross"]),
            Cost = transTotal.Sum(x => (decimal)x["Cost"]),
            Tax = transTotal.Sum(x => (decimal)x["Tax"]),
        };
}
Community
  • 1
  • 1
Joshua Honig
  • 12,925
  • 8
  • 53
  • 75
  • 4
    Sad to see there is no dynamic duck there. – Oded Oct 22 '12 at 16:20
  • 3
    Personally I would be happy with writing a custom immutable POCO for every single otherwise-anonymous type. I have written lots of complex ETL code myself, and trust me, it's safer and easier on the brain to write everything out explicitly, with full XML comments. – Christian Hayter Oct 22 '12 at 16:24
  • 2
    Is there a question here? This seems like it should be on the visual studio user voice site, not here: http://visualstudio.uservoice.com – Reed Copsey Oct 22 '12 at 16:25
  • @ReedCopsey Yeah, the question is in the second line: "What's an idiomatic workaround?" But I can definitely move the proposed `duck` keyword to uservoice. I wasn't familiar with it, so thanks for the link. – Joshua Honig Oct 22 '12 at 16:30
  • Might be silly, but I suppose it's plausible to **contort** some mocking framework (like Moq) to create some immutable object (via methods or property getters). This _might_ work within a local method scope (as anonymous types are still "real" types), but to pass it around, you'd probably still have to _declare_ immutable interfaces _somewhere_. At that point though you may as well declare the immutable POCOs yourself. – Chris Sinclair Oct 22 '12 at 16:50

4 Answers4

6

ExpandoObject may be of interest to you.

Paul Ruane
  • 37,459
  • 12
  • 63
  • 82
1

Seems like you'd want to implement your own IDynamicObjectProvider: http://msdn.microsoft.com/en-us/library/system.dynamic.idynamicmetaobjectprovider.aspx

Example implementation: http://msdn.microsoft.com/en-us/vstudio/ff800651.aspx

You seem to want to access a structure like List> where String is the name, Type is the value type and Object is the value.

BUT that seems like a big hassle and probably wouldn't offer very good performance. You should probably just implement all the proper classes you need. For the sanity of whoever has to maintain the code after you, defining interfaces for each input just seems rationale.

Louis Ricci
  • 20,804
  • 5
  • 48
  • 62
  • Good point about other people maintaining, as well. Most of the time I work alone but _eventually_ someone else will have to pick it apart. – Joshua Honig Oct 22 '12 at 18:42
1

You may want to have a look at this approach:

public IEnumerable<T> GetTransactions<T>(..., 
    Func<int, int, DateTime, decimal, decimal, decimal, T> resultor) {
    return 
        from record in someProvider.GetDetails(...)
        where ((DateTime)record["Date"]).Date == someDate
        group record by new {
            StoreNumber = (int)record["Number"],
            Transaction = (int)record["TransNum"],
            Date = (DateTime)record["Date"]
        } into transTotal
        select resultor(
            transTotal.Key.StoreNumber,
            transTotal.Key.Transaction,
            transTotal.Key.Date,
            transTotal.Sum(x => (decimal)x["Gross"]),
            transTotal.Sum(x => (decimal)x["Cost"]),
            transTotal.Sum(x => (decimal)x["Tax"])
        );
}

the resultor Func is mapped to the layout of your concrete duck, taking as many parameters as it does, with matching types, and it returns a T. This T gets inferred when you call you method ad supply your concrete Func, for example like this:

GetTransactions(..., (sn, t, d, g, c, tx) => return new {
    StoreNumber = sn,
    Transaction = t,
    Date        = d,
    Gross       = g,
    Cost        = c,
    Tax         = tx
});

This way the generated types are available outside of the called method, because you give the responsibility of defining it to the caller. No POCO DTOs, no dynamic, immutability, equality out of the box. Check it out.

Wasp
  • 3,395
  • 19
  • 37
0

Are you SURE Dynamic is too slow? I've done performance tests on dynamic operations versus direct invocations and was actually surprised by how little impact dynamic invocation adds. I don't have my numbers with me, but found this little blog post that shows only about a ten-fold increase in operation length. His example shows 2 million invocations taking 85 milliseconds versus the 7 milliseconds for a static invoke.

It does add up. A six second operation would take a minute but it's FAR less than dealing with pure reflection.

Michael Brown
  • 9,041
  • 1
  • 28
  • 37
  • The type validation and intellisense support of NOT using `dynamic` is even more important to me. I think I'll be heeding the collective advice of just generating all the necessary immutable POCOs. – Joshua Honig Oct 23 '12 at 12:26