Monday, April 5, 2010

RIA and DTO (Part 2)

Ok, let’s make our DTO a bit more complex (or more real life like I would say)

Code Snippet
public class MyCustomDTO
{
    [Key]
    public int ID { get; set; }

    public int MyPayload { get; set; }
    public string MyOtherPayload { get; set; }
    
    [Include]
    [Association("MyCustomDTO_MyItem", "ID", "ParentID")]
    public IEnumerable<MyItem> MyItems { get; set; }
}

public class MyItem
{
    [Key]
    public int ID { get; set; }
    public int ParentID { get; set; }

    public string Name { get; set; }
    public int Value { get; set; }
}

so we added list of MyItems to our DTO. We also marked this list with [Include] and [Association]. It’s very important part and soon you’ll see why. Let’s test our DTO, is it really able to transfer data to/from client? Server-side is

Code Snippet
[Invoke]
public MyCustomDTO MyOtherOperation(MyCustomDTO data)
{
    data.MyItems = new List<MyItem> { new MyItem { Name = "new", Value = 72 } };
    return data;
}

Note: for some strange reason RIA won’t allow you to use sets/enumerations of DTO as parameter, so the following will not compile. Be aware!

Code Snippet
[Invoke]
public void MyOtherOperation(IEnumerable<MyCustomDTO> data)

the workaround is simple – just encapsulate your list inside another DTO class ;)

ok, now client side

Code Snippet
var data = new MyCustomDTO
{
    MyPayload = 13,
    MyOtherPayload = "Test"
};
ctx.MyOtherOperation(data,
    op =>
    {
        var result = op.Value;
        Debug.Assert(result.MyItems.Count == 1);
    },
    null);

Let’s run it… boom! Assertion failed! We expected our list be non empty but it is ;( You might ask – why? We did all good server side. Well.. not really ;) Here comes very important part – how client client side code generated by RIA handle references. It does handle it via associations. The same way as relational databases handle foreign key associations. So to make it work you have to provide valid values of properties that make association. Here is the code it uses internally (generated client side)

Code Snippet
[Association("MyCustomDTO_MyItem", "ID", "ParentID")]
public EntityCollection<MyItem> MyItems
{
    get
    {
        if ((this._myItems == null))
        {
            this._myItems = new EntityCollection<MyItem>(this, "MyItems", this.FilterMyItems);
        }
        return this._myItems;
    }
}

and the magic happens here

Code Snippet
private bool FilterMyItems(MyItem entity)
{
    return (entity.ParentID == this.ID);
}

so, as you now see, to fix our problem we have to do this

Code Snippet
var data = new MyCustomDTO
{
    ID  = 1,
    MyPayload = 13,
    MyOtherPayload = "Test",
};
data.MyItems.Add(new MyItem { ID = 1, ParentID = 1, Name = "client", Value = 5 });

Well, let’s give it another go. F5… same result! The sad truth is that unfortunately at the moment [Invoke] operations don't support complex composite DTOs. The only place you can use it is either [Query] methods (as a result) or in Submit based operations. Which means that we can either wait till RIA team fixes this awkward functional gap or we could use normal WCF service or this simple but ugly workaround

Server side

Code Snippet
[Insert]
public void MyItemDummyInsert(MyItem data) { }

[Insert]
public void MyOperation(MyCustomDTO data)

Client side

Code Snippet
var ctx = (OrderContext)orderDomainDataSource.DomainContext;
var data = new MyCustomDTO
{
    ID  = 1,
    MyPayload = 13,
    MyOtherPayload = "Test",
};
data.MyItems.Add(new MyItem { ID = 1, ParentID = 1, Name = "client", Value = 5 });
ctx.MyCustomDTOs.Add(data);
ctx.SubmitChanges();

the idea here is pretty banal – use part of Submit processing as our custom operation.

The most painful limitation of using DTOs in WCF RIA is that you can’t use DTOs as [Query] methods parameters

Code Snippet
public IQueryable<TestData> MyQuery(MyCustomDTO data)

will produce following compilation error

Parameter 'data' of domain operation entry 'MyQuery' must be one of the predefined serializable types.

Why??? Why RIA team has decided that providing only basic types as parameters would be enough? Take a look outside your Ivory Tower guys – there is real world outside and developers do need complex query parameters (you just can’t express everything with IQueryable<T> and simple typed parameters)

The workaround is as usual simple and ugly – you can expose your parameter as string, serialize your DTO on client and de-serialize on server

Code Snippet
public IQueryable<TestData> TestDataSearch(string searchCriteria)

Ok, to summarize – by now RIA has very limited support for passing DTOs back and forth. We can pray and wait for this to be fixed or use dirty workarounds.

Happy RIA coding, to be continued…

4 comments:

  1. Hi Sergey,

    I am using DTO and following your post as an example and am encountering an issue. I posted my question here: http://forums.silverlight.net/forums/p/183679/418001.aspx#418001

    If you have any thoughts that will be great.
    Thanks.

    ReplyDelete
  2. Thanks. The [invoke] and [association] attributes were exactly what I was needing to resolve my issue.

    ReplyDelete
  3. Those were some helpful points there about RIA and DTO.

    ReplyDelete
  4. Dear sir,
    I am using an empty domain service and return a class using domain service.
    But i am facing following exception
    Load operation failed for query 'GetAnnuncements'. To create an EntityKey, you must specify at least one key value.Parameter name: keyValues

    Kindly help me on this.
    Thank you

    ReplyDelete