Archive for June 19th, 2009

19
Jun

Gaining some context into ASP.NET AJAX 4’s DataContext…

The ASP.NET AJAX 4 release has some really cool features in it that can help lower the barrier of entry into developing client-side web applications (jQuery doesn’t hurt either). One of the more compelling new classes is the DataContext. Basically, the DataContext is an object that is capable of consuming a server-side resource that serves JSON data. In its most basic form, you simply give it the URI of a service and the operation name to execute and it handles making the underlying request. If you had an AJAX-enabled ASMX service like so (note: I’m using the Entity Framework)…

[ScriptService] 
public class CustomersAsmx : WebService { 
    private CustomersContext context = new CustomersContext(); 
 
    [WebMethod] 
    public Customer[] GetCustomers(int count) { 
        var customers = (from cus in context.Customers 
                         orderby cus.Id 
                         select cus) 
                         .Take(count) 
                         .ToArray(); 
 
        return customers; 
    } 
}

Calling that service using a DataContext might looking something like this…

var context = $create(Sys.Data.DataContext, { serviceUri: "Customers.svc" }); 
context.fetchData("GetCustomers", // Operation name 
                  { count: 10 }, // Operation parameters 
                  Sys.Data.MergeOption.overwriteChanges, // Merge-option (optional) 
                  "POST", // HTTP verb (optional) 
                  function customerFetchSuccess(customers) { 
                      // Do something with customers... 
                  }, 
                  function customerFetchFailure(error) { 
                      // Handle the error somehow... 
                  });

As you can see, there are all kinds of options that can be passed to the fetchData method, many of which are optional. The above scenario makes use of an ASMX service, but you could just as easily be targeting an AJAX-enabled WCF service, which both have pretty similar semantics in this scenario.

What if you’re using ADO.NET Data Services though? In theory, since the DataContext class can make requests against any service that responds with JSON data (which Data Services can do), it should be able to consume a Data Service with a few modifications to the above code sample…

var context = $create(Sys.Data.DataContext, { serviceUri: "Customers.svc" }); 
context.fetchData("Customers", 
                 { $top: 10 }, 
                  null, // Defaults to overwrite changes 
                  "GET", 
                  function customerFetchSuccess(customers) { 
                      // Do something with customers... 
                  }, 
                  function customerFetchFailure(error) { 
                      // Handle the error somehow... 
                  });

Obviously the serviceUri property now points at a Data Service instead of an ASMX/WCF service, but the real points of interest lie in the operation/parameters. While ASMX and WCF are traditionally operation-centric services (see note below), ADO.NET Data Services is resource-centric, and doesn’t have methods that we can target (unless of course you’re using service operations). Hence, to consume the Data Service from our DataContext, we set the operation method to the name of the resource set (i.e. Customers) that we want to retrieve. Operation parameters work the same as with ASMX/WCF, but since Data Services has its own predefined set of query options, we simply need to comply with them (i.e. using $top). Finally, Data Services uses the GET HTTP verb for all data read requests (as opposed to traditional RPC style services that use POST for everything), so we need to set that.

By default, WCF leans towards the edge of RPC-style/SOAP services, but is capable of providing REST-style services that are resource-oriented. An ADO.NET Data Service is itself a WCF service that exposes a RESTful interface and makes data-centric services really easy to implement and consume.

If you ran the above sample, it would work, but not exactly as you might like. The customers data that is passed into the success callback would be a string of XML, representing the AtomPub feed sent back from the Data Service. The reason for this is because ADO.NET Data Services will by default serve its data as AtomPub. If a request comes in that includes the Accept HTTP header set to “application/json” then the Data Service will return JSON data. Unfortunately the DataContext class doesn’t set the Accept header when making service calls, which makes consuming a Data Services from it pretty sub-optimal.

You might be thinking to yourself: “Hey that’s cool, I’ll just grab the request that is associated with the DataContext and add the Accept header myself!”. Unfortunately that isn’t possible since the DataContext doesn’t expose its underlying request. If you’re a seriously hardcore ASP.NET AJAX dev, you might be thinking: “Alright fine, I’ll just use the WebRequestManager to tap into the request before it’s sent”. That is totally possible, and would look something like this…

Sys.Net.WebRequestManager.add_invokingRequest(function(sender, args) { 
    var request = args.get_webRequest(); 
    request.get_headers()["Accept"] = "application/json"; 
});

Unfortunately that solution is ridiculously unintuitive and is now intercepting all requests just to accommodate a single case. Unless you have a really good reason you should almost never do this. How then can we use a DataContext to easily consume a Data Service? Well, you can’t use the DataContext per se, but you can use a subclass of it, like so…

var context = $create(Sys.Data.AdoNetDataContext, { serviceUri: "Customers.svc" }); 
context.fetchData("Customers", 
                 { $top: 10 }, 
                  null, // Defaults to overwrite changes 
                  null, // Defaults to GET 
                  function customerFetchSuccess(customers) { 
                      // Do something with customers... 
                  }, 
                  function customerFetchFailure(error) { 
                      // Handle the error somehow... 
                  });

Notice that we’re now using an AdoNetDataContext instead of the vanilla DataContext. This immediately does two interesting things for us: defaults the HTTP verb to GET (as opposed to POST), and sets the Accept header to “application/json” for us (so the Data Service will return JSON data instead of XML). Now if we ran the above code, we would successfully get back the top ten customers as JSON objects.

The DataContext class should be viewed as two things:

  1. An easy solution for consuming RPC-style services (i.e. ASMX or WCF)
  2. A great foundation for other higher-level service clients

If your server-side resource is an ASMX or WCF service then the basic DataContext is exactly what you want and will work seamlessly. If however you’re using an ADO.NET Data Service, then the DataContext lacks the semantics you need to work successfully, and you should use the AdoNetDataContext instead. If you want to consume some other service type that requires some special attention, you could also choose to subclass DataContext and add on the extra behavior you need (it honestly is pretty simple to do).

Hopefully some of you are reading this and thinking that I’m absolutely crazy to think you would use a DataContext just to retrieve data. The above code sample for consuming an ASMX service could be achieved using a class that has been in ASP.NET AJAX since its conception (WebServiceProxy), and doesn’t require us to create an instance (um, because it’s “static”)…

Sys.Net.WebServiceProxy.invoke("Customers.asmx", // Service URI 
                               "GetCustomers", // Operation name 
                               false, // Use GET 
                               { count: 10 }, // Parameters 
                               function customerFetchSuccess(customers) { 
                                  // Do something with customers... 
                               }, 
                               function customerFetchFailure(error) { 
                                  // Handle the error somehow... 
                               });

Notice that this code is nearly identical to what we wrote when using the DataContext, but we’ve removed the need to $create anything. There also happens to be an AdoNetServiceProxy class as well that enables you to make service calls to a Data Service, similarly to how we achieved it above. So the question arises: why on earth would you ever use a DataContext vs. just using its respective service proxy? There are two reasons:

  1. You need an abstraction over your underlying request/proxy type
  2. You need unit of work behavior, complete with automatic change tracking

The first part is important because the WebServiceProxy and AdoNetServiceProxy are drastically different and don’t share a common base class (er prototype). This becomes problematic when you have other classes that need to consume data, but don’t want to be tied to a specific service type. A great example of this is the new DataView class, which allows you to create dynamically-templated UI. The DataView has a property called “dataProvider” that accepts a DataContext, and uses that context to handle populating its templates with the data it needs. We can now hand the DataView any DataContext subtype we want and it will be able to consume the data without a care in the world…

var dataContext = $create(Sys.Data.AdoNetDataContext, { serviceUri: "Customers.svc" }); 
var customersTemplate = $create(Sys.UI.DataView, { 
                            autoFetch: true, 
                            dataProvider: dataContext, 
                            fetchOperation: "Customers", 
                            fetchParameters: { $top: 20 } 
                        }, 
                        null, 
                        null, 
                        $get("customers-template"));

I’ll save the gritty details of the DataView for another post. One point of interest though is that because we’re no longer manually calling DataContext.fetchData (because the DataView is doing that for us), the operation and parameters properties are now set on the DataView instead of the DataContext. This scenario is probably the most common usage of the DataContext, and illustrates how simple using a DataContext can be.

The second point made above was slipped covertly under the radar, but really should have been highlighted as being the major reason of why you would ever use a DataContext in the first place. The abstraction is nice but isn’t nearly compelling enough to warrant introducing an entirely new concept into your life. The DataContext is valuable because it provides you a unit of work experience. Up to this point I’ve purposely shown it being used for read-only scenarios, but it is so much more.

If you are looking to simply retrieve data from a service, you should use one of the proxy types (i.e. WebServiceProxy or AdoNetServiceProxy). In fact I would go so far as to say that if you find yourself manually calling the DataContext’s fetchData method, you are probably doing something wrong. If however you need to change the retrieved data, and then persist those changes back to the server, a DataContext is your best friend.

What if I told you that the code samples above that used a DataContext with a DataView will automatically provide change tracking and can save any changes back to the server as easy as a single line of code…

dataContext.saveChanges();

What subtleties exist with change tracking/persistence when working with DataContexts of different types? How exactly could such rich functionality be achieved with a single line of code? I’ll cover that aspect of the DataContext in another post…




June 2009
S M T W T F S
« Apr   Nov »
 123456
78910111213
14151617181920
21222324252627
282930  

Categories