DomainDataSource Server Control: LINQ + Code Generation

A prototype data source server control that provides the combined goodness of ObjectDataSource and LINQ... source code included. What do you think of this model? And for server-control developers, a behind-the-scenes look at dynamic code-generation.

We have LinqDataSource in ASP.NET. However, the fact that I have to break up my LINQ statement into individual string properties (as shown below) has bugged me ever since the feature existed.

<asp:LinqDataSource id="ds1" runat="server" ContextType="PubsDBDataContext"
  TableName="Title"
  Where="Price >= @FilterPrice">
  <WhereParameters>
    <asp:ControlParameter Name="FilterPrice" ControlID="priceTextBox" Type="Decimal" />
  </WhereParameters>
</asp:LinqDataSource>

I'd like to write an honest-to-goodness complete LINQ expression without learning a DataSource control-specific syntax. Furthermore, I'd like to get the goodness of ObjectDataSource and have the data source work against some business logic rather than directly against the DataContext or the DAL. So I set about to write a new data source control...

DomainDataSource Control
I prototyped a DomainDataSource control that tries to fulfill both these requirements. It is built on some interesting code-generation capabilities of server controls that I will discuss later in the post. But first, here is an example of the resulting syntax available to page developers via this control:

<sample:DomainDataSource ID="titlesDataSource" runat="server"
   DomainObjectTypeName="Bookstore"
   DataItemTypeName="Title" DataMethodName="GetTitles">
  <QueryParameters>
    <asp:ControlParameter Name="price" Type="Decimal" ControlID="priceTextBox" />
    <asp:ControlParameter Name="type" Type="String" ControlID="typeDropDown" />
  </QueryParameters>
  <Query>
    from t in @data
    where t.price > @price && ((@type == "(all)") || (t.type == @type))
    select t
  </Query>
</sample:DomainDataSource>

Of course if my page language is set to VB, I would now write the LINQ expression in VB as well:

<sample:DomainDataSource ID="titlesDataSource" runat="server"
  DomainObjectTypeName="Bookstore"
  DataItemTypeName="Title" DataMethodName="GetTitles">
  <QueryParameters>
    <asp:ControlParameter Name="price" Type="Decimal" ControlID="priceTextBox" />
    <asp:ControlParameter Name="type" Type="String" ControlID="typeDropDown" />
  </QueryParameters>
  <Query>
    From t In data _
    Select t _
    Where t.price > price AndAlso ((type = "(all)") Or (t.type = type))
  </Query>
</sample:DomainDataSource>

Here's the code in my Bookstore class (for purposes of illustration the business logic within GetTitles only surfaces title's whose authors have a contract):

public class Bookstore {
    public IQueryable<Title> GetTitles() {
        DataLoadOptions loadOptions = new DataLoadOptions();
        loadOptions.LoadWith<Title>(t => t.TitleAuthors);
        loadOptions.LoadWith<TitleAuthor>(ta => ta.Author);

        PubsDataContext dc = new PubsDataContext();
        dc.LoadOptions = loadOptions;

        return from t in dc.Titles
               where t.TitleAuthors.Any(ta => ta.Author.contract)
               select t;
    }
}

Before we get to what is happening behind the scenes to make the LINQ expressions work, I'd love to hear your thoughts. Is this appealing in terms of functionality and as a model for bringing data into the presentation tier?

I know some will argue that having LINQ code in the presentation is bad. I agree and at the same time disagree. The power of LINQ is being able to defer the execution of a query and to be able to fully compose it before it finally executes. Unlike LinqDataSource which works directly against a DataContext, in my example above, the method I am referencing from DomainDataSource is on some business logic class encapsulating any domain logic that is independent of the UI and presentation. The presentation layer is only further filtering or re-shaping the collection based on parameters associated with visual elements on the page for use in rendering. Yet, when the query does result in a database access, just the right bit of data is fetched, thanks to the composability of LINQ.

The other thing a number of you will likely point out is the missing intellisense for the LINQ expression authoring. I like to think of this prototype as just a start. It is possible to have a LINQ expression builder in much the same way that a SQL query builder exists (there is an example of this in the VLINQ project). It is also possible to use the code editor in VS along with all the functionality of language services for colorization and intellisense within a query builder dialog… so its just a matter of additional control designer smarts and design-time work, rather than a fundamental architectural hiccup with this approach.

A behind the scenes look at DomainDataSource
What's this about code-generation capabilities of server controls? And how does DomainDataSource make use of it?

The first thing to keep in mind is that ASP.NET parses your server control markup once, and generates code on the fly to transform the parse tree into code. This code is executed each time a page is created to initialize the page and the controls defined in the page.

One of the lesser known extensibility mechanisms in the server control space is the ControlBuilder infrastructure that allows your code to participate in the markup parsing process. This functionality grew a bit organically starting with the very early ASP.NET 1.0 days, and has continued to do so. In fact, one of the most recent additions (in .NET 3.5) has been to allow a server control's builder to participate in code generation as well when the server control markup is parsed and converted into code.

I had been wanting this functionality since the early days of .NET 2.0, and now it is finally there in the form of the ProcessGeneratedCode virtual method on ControlBuilder.

My DomainDataSource control has an associated ControlBuilder that overrides ProcessGeneratedCode to do some magic at code generation time. When overriding this method, the control builder gets the entire CodeDOM tree, and a reference to the method responsible for creating and initializing the control instance. The override does is a couple of things. First the control builder creates a new method that takes in an IQueryable, and the parameter values, and returns a new IQueryable by applying the user's Query as specified in the markup (the query is copied verbatim into the generated code). The lines in bold represent what the control builder generates:

// Create the method that applies the user's query by composing it on top of the existing
// query represented by "data" and using the specified parameter values.
private IEnumerable __ComposeQuery_1(DomainDataSource dataSource,
                                    IQueryable<Title> data, Decimal price, String type) {
    return from t in @data
           where t.price > @price && ((@type == "(all)") || (t.type == @type))
           select t;
}

// The delegate type matching the signature of the above generated method
public delegate IEnumerable __D__ComposeQuery_1(DomainDataSource dataSource,
                                    IQueryable<Title> data, Decimal price, String type);
 
private DomainDataSource __BuildControltitlesDataSource() {
    DomainDataSource __ctrl;

    __ctrl = new DomainDataSource();

    ...

    // Create a delegate to the method and pass it in into the control for use
    // at runtime
    __D__ComposeQuery_1 queryComposer = new __D__ComposeQuery_1(this.__ComposeQuery_1);
    __ctrl.SetQueryComposer(queryComposer);

    return __ctrl;
}

The above code represents a snippet of the generated code for creating and initializing an instance of the DomainDataSource. The lines in bold are the ones added by the control builder itself by overriding ProcessGeneratedCode. As you can see it contains the developer-specified LINQ expression as-is in the generated code.

At runtime, the DomainDataSource invokes the specified data method, and then invokes the delegate passing in the result of the data method along with all the evaluated parameter values. The final resulting IQueryable is what is enumerated to generate the set of items that will be handed to the data-bound control such as a GridView or ListView to render into the page.

This approach of using the code-generation extensibility point allows me to use the full set of LINQ capabilities without requiring me to write a custom parser or invent an alternative XML markup syntax to define a LINQ query.

The documentation doesn't do this powerful functionality any justice. Hopefully the sample above will show a little more of what is in fact possible. You'll of course need to deal with and stand CodeDOM. If you've developed server controls, where you wished you could convert some late-bound code into compiled code, there might be a way after all. What might you use it for?

Where is the code?
I hope this was an interesting look at some fairly advanced functionality deep inside the Web Forms framework! Feel free to post questions if this piques your interest. You can download the complete project to either play with DomainDataSource for yourself, or poke around in the control builder to see in more detail what its ProcessGeneratedCode actually looks like. Currently the code only implements the Select portion of the data source control. After MIX, in a few months, I plan to revisit this to take advantage of some new framework pieces coming out then, to implement the remainder of the Update, Insert and Delete portions of the data source control. Stay tuned on that end for more.

Note: if you're looking for more general information about writing a data source control itself, check out my 5 part series on implementing a data source - it is from a while back, but still pretty much applicable. The code generation capability is available to all server controls, and not just data sources.


[ Tags: | | ]
Posted on Thursday, 1/15/2009 @ 11:09 PM | #ASP.NET


Comments

16 comments have been posted.

Bert

Posted on 1/16/2009 @ 3:29 AM
This looks awesome; but perhaps the functionality of putting the LINQ query in the datasource control should be added to the upcoming BusinessObjectDataSource instead of its own datasource control? I think that would be better; build on top of BODS but layer the <Query /> tag on top of it.

Jeremy Sullivan

Posted on 1/16/2009 @ 5:18 AM
I've fought with the LinqDataSource myself, and the DomainDataSource would make my life a ton easier. It would be nice to have it built on top of the BODS (as Bert suggested), but I'll take what I can get... especially if it means I can use it now. ;)

Mohammad Mahdi Ramezanpour

Posted on 1/16/2009 @ 6:37 AM
I can be a great control.
I think ASP.NET needs a control like this.

Joe Mayo

Posted on 1/16/2009 @ 7:34 AM
Hi Nikhil,

I can see the benefit of being able to re-shape results at the UI level, which is even a dilemma with the ObjectDataSource control. However, I'm one of those guys who don't like to mix UI with business logic. From my perspective, there are going to be legions of developers who will have horrendous HTML pages with embedded LINQ expressions. I tend to dislike any of the controls like this (LinqDataSource, SqlDataSource, etc.) that make it too easy for developers to write bad code (aka Ball of Mud architecture). The newsgroups and forums today are riddled with developers who try to use these controls and then hit a brick wall when they try to move beyond what drag-and-drop and wizards will give them. Perhaps from a RAD or beginner perspective this might look like a good way to accomplish something quickly, but over time it costs companies a lot of money to maintain or re-write. While I'm a fan of the ObjectDataSource (and will look at the BusinessObjectDataSource), I don't recommend this style of programming - sorry.

Joe

Perry Bynum

Posted on 1/16/2009 @ 8:18 AM
Great concept. I am doing a lot of work around extending/customizing the code produced by various VS 'Generators' (LINQ to SQL, EF, etc.). Please encourage the ADO Team to adopt the ProcessGeneratedCode concept for the model generators.

Thanks for all the great work...

Nikhil Kothari

Posted on 1/16/2009 @ 9:07 AM
@Bert - yep, know about the BusinessObjectDataSource - I am in fact working on some very related areas (the stuff shown at PDC for building line of business applications using Silverlight). My hope is to see the feedback from this, and try incorporate it into what we do at the data source layer when it comes to asp.net.

@Joe Mayo - my hope is DomainDataSource does go a long way in keeping business logic out of the UI - it stays in the business logic class or domain object that this data source refers to. However, that separation shouldn't require the business logic to take every permutation of sorting, filtering, paging parameters in every method. You want to allow the UI to reshape the data based on UI concerns separate from the business logic, but do so while building on it. In other words, the UI can do no more than the business logic permits, but using LINQ it can do the reshaping more optimally, by composing its query before the real database access happens.

Sean

Posted on 1/16/2009 @ 1:37 PM
Nikhil,

This would be a great feature.

Since you're asking - it has always been somewhat frustrating not to be able to take advantage of the pre-defined associations between LINQ objects when binding to fields and using standard LINQ syntax in the HTML markup to reference these associated table and their fields - without resorting to a template field and EVAL or BIND statements.

I'd love to be able to use standard LINQ syntax in the HTML markup and put something like SalesOrder.OrderDate to reference data in the associated object - and to have the Gridview or FormView UI wizards be smart enough to see that relationship and allow me to select those fields.

Minor annoyance - but I think it would save people a lot of time and beginners a lot of headaches researching how to do this simple task.

Andrew Robinson

Posted on 1/16/2009 @ 5:45 PM
Nikhil,

I agree with your reason for building such a control but I routinely get most of the way there by coding my linq code in the selecting event of the linq data source control. I leave the control specific text / sql out of the markup.

Nikhil Kothari

Posted on 1/16/2009 @ 6:43 PM
@Andrew - yes, the selecting event works for some scenarios.

If you've got parts of your query dependent on control values, then you typically have corresponding parameters on the data source. At that point it would be nice to have the values of those parameters in the Selecting event, but since you don't, you have to fetch the values of the parameters again... and if the data source happens to be inside the template of another control, then you have to resort to FindControl and other ugly stuff to get at the right controls.

You really want a model where what is declarative stays in the markup, so the data source is responsible for reacting to changes in the values, cause re-binding to happen, and be in charge of finding the controls, extracting values etc. The Selecting event is nice for scenarios where you want to compose a query that doesn't depend on the control tree itself.

By the way DomainDataSource does have a Selecting (and Selected) event.

Andrew

Posted on 1/18/2009 @ 8:53 AM
Nikhil,

I see your point. To me, it is an issue of syntax checking of linq code as part of the selecting event vesus markup. You are going after pure markup with linq syntax versus the sql ish code that that the LinqDataSource control uses. Interesting trade-offs.

One question that might not be directly related to this that I struggle with is the requirement to use ESQL with the selecting event on the Entity Data Source. I don't want to deal with any type of weakly typed text in my markup for data access (thus my interest in using the selecting event off of the LDS), but the EDS doesn't all you to return an IQueryable object. This is the single biggest reason that I am avoiding the EF for now. Will your control work with the EF? What has to be done to make this work of could I extend the EDS and add the functionality myself? Sorry, but I haven't played with your control yet but plan to.

Love your book! thanks!

Nikhil Kothari

Posted on 1/18/2009 @ 9:03 AM
@Andrew -
If EDS doesn't allow using LINQ in its Selected event then you won't be able to extend it to get similar functionality. I haven't used EDS, but the key thing it needs to do is offer a IQueryable implementation for you to compose your query in. From the sounds of your description it doesn't and that seems wrong - seems like inconsistent design with the overall support for LINQ - might be good to report this to the EF team.

Theoretically, this data source should work over EF, but I haven't tried it myself, since I personally like and am totally fine with LTS. EF would certainly be interesting though, so if you try and run into issues, let me know. There will be EF-specific things that you'll need to account for in the query, such as using First(), and not Single() for example.

One related note - the Query in DomainDataSource is strongly typed and not weakly typed. You'll get a compile error when the page is parsed and compiled, as opposed to runtime errors (like you would in LinqDataSource).

Andrew

Posted on 1/18/2009 @ 9:33 AM
Good stuff. Will explore a bit but likely not right away. Thanks much!

Do you ever speak to User Groups? I run the Bellingham .NET Group which is about 80 miles from Redmond.

-Andy

Howard

Posted on 1/20/2009 @ 12:51 AM
I think this would a great control - would it still be able to support inserts/updates/deletes?

I have an issue with the LinqDataSource because it only supports modification if the "tables" in the data context are Table<T> and I have a modified TableView<T> class (which adds security) that implements ITable<T>

Nikhil Kothari

Posted on 1/20/2009 @ 7:23 AM
@Howard:
The data source can support updates, but thats not done in its current sample form. I will be doing that post-MIX, so stay tuned. Since it calls into your domain logic methods, it should work fine with view types as opposed to DAL types.

Gil

Posted on 1/25/2009 @ 12:13 AM
Interesting approach, yet with the effort to decouple and add indirection levels in order to get flexibility my take is that linq query should be against a Data Transfer Object and not against the Domain Object. The Domain Object to my perspective should not be accessed at this level. In another context I see some different explanations in MVC as for what is the Domain. Some say it is the business object and others say it is the DTO. In a recent implementation I used the Object Data Source which was actually a DTO that was received to that layer from a wcf call. How the DTO was first created in the service is hidden from the UI level all together. just my 2 cents..

Sam

Posted on 6/24/2009 @ 11:07 AM
I would like to be able to crate a base page class and define/configure a DomainDataSource there. I would like to bind controls in my pages that inherit from the base page class to the DomainDataSource. Is that possible? Could you give me a brief example?
Thanks,

Sam
Post your comment and continue the discussion.