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.