Friday, 19 December 2008

Astoria and LINQ-to-SQL; batching and replacing

So far, we have been doing individual updates - but if we perform multiple updates, each is currently sent as an individual update. We can do better; if the server supports it (and ADO.NET Data Services does), we can use batch-mode to send multiple requests at once, and to commit all the changes in a single transaction at the server. Note that this is far preferable to the option of using a distributed transaction to span multiple requests. Fortunately this latter option is not supported under ADO.NET Data Services: it simply doesn't scale, and besides - there is no way to convey this intent over a simple REST request which follow the YAGNI approach of keeping things simple.

Using Batches

The decision to use batches is taken at the client - for example, a non-batched update might be written:

var emp = ctx.Employees.Where(x => x.EmployeeID == empId).Single();
var boss = ctx.Employees.Where(x => x.EmployeeID == bossId).Single();

emp.BirthDate = boss.BirthDate = DateTime.Now;
ctx.UpdateObject(boss);
ctx.UpdateObject(emp);

ctx.SaveChanges();

This performs 2 http requests, and involves 2 data-contexts / SubmitChanges - i.e. adding some logging shows:

GET: http://localhost:28601/Restful.svc/Employees(1)
GET: http://localhost:28601/Restful.svc/Employees(2)
MERGE: http://localhost:28601/Restful.svc/Employees(2)
3: D 0, I 0, U 1 ## the 3rd data-context usage, doing a single update
MERGE: http://localhost:28601/Restful.svc/Employees(1)
4: D 0, I 0, U 1 ## the 4th data-context usage, doing a single update

Changing the last line at the client makes a big difference:

ctx.SaveChanges(SaveChangesOptions.Batch);

with the trace output:

GET: http://localhost:28601/Restful.svc/Employees(1)
GET: http://localhost:28601/Restful.svc/Employees(2)
POST: http://localhost:28601/Restful.svc/$batch ## note different endpoint and POST not MERGE
3: D 0, I 0, U 2 ## 3rd data-context usage, doing both updates

This usage can make the API far less chatty. However, it raises the issue of what to do if the second update fails (concurrency, blocking, etc). The answer lies in one of our few remaining IUpdatable methods. In fact, in batch mode (but not in single-update mode), erros in SaveChanges normally cause the ClearChanges method to be invoked, allowing us chance to cancel our changes. As it happens, LINQ-to-SQL uses transactions by default anyway, so our database should still be in a good state. Entity Framework, at this point, detaches all the objecs in the change-set, but LINQ-to-SQL doesn't offer this option; as a substitute, we can take this opportunity to prevent any further usage of the data-context in an unexpected state:

public static void ClearChanges(DataContext context)
{ // prevent any further usage
context.Dispose();
}

In other scenarios, this would be an opportunity to undo any changes.

Replacing Records

ADO.NET Data Services also provides another option for submits - replace-on-update. The difference here is that normally the data uploaded from the client is merged with the existing row, where-as with replace-on-update, the uplodaded row effectively *replaces* the existing. Importantly, any values not sent in the upload are reset to their default values (which might make a difference if the server knows about more columns than the client). At the REST level, the http verb is "PUT" instead of the "MERGE" or "POST" we saw earlier.

At the client, the only change here is our argument to SaveChanges:

ctx.SaveChanges(SaveChangesOptions.ReplaceOnUpdate);

Note that this mode is not compatible with batching; a request is made per-record. At the server, this maps to the final IUpdatable method - ResetResource. This method is responsible for clearing all data fields except the identity / primary key /etc (since we still want to write over the same database object). Since LINQ-to-SQL supports multiple mapping schemes (file-based vs attribute-based) we need to ask the MetaType for help here; we'll create a new vanilla object, and use these default property to reset the actual object:

public static object ResetResource(DataContext context, object resource)
{
Type type = resource.GetType();
object vanilla = Activator.CreateInstance(type);
MetaType metaType = context.Mapping.GetMetaType(type);
var identity = metaType.IdentityMembers;
foreach (MetaDataMember member in metaType.DataMembers)
{
if(member.IsPrimaryKey || member.IsVersion || member.IsDeferred
|| member.IsDbGenerated || !member.IsPersistent || identity.Contains(member))
{ // exclusions
continue;
}
MetaAccessor accessor = member.MemberAccessor;
accessor.SetBoxedValue(ref resource, accessor.GetBoxedValue(vanilla));
}
return resource;
}

Summary

Breath a sigh; we have now completely implemented IUpdatable, completing all of the raw CRUD operations required to underpin the framework, and all without too much inconvenience. Hopefully it should be fairly clear how this could be applied to other implementations. I promised to return to inheritance when creating records (and I will), and I'll also explore some of the other features of ADO.NET Data Services.

Saturday, 13 December 2008

Astoria and LINQ-to-SQL; associations

We can get now manipulate flat data - but we also need to model associates between data (read: foreign keys in RDBMS terms).

Lets start by modifying our client to pick two employees: we'll add one of the records as a subordinate of the other, verify, and then remove them. Note that the ADO.NET Data Services client code doesn't do much to automate this - as with "UpdateObject" we need to keep nudging it to ask it to mirror the changes we make. We also need to manually load child collections.

Implementing Collection (/Child) Associations

Here's our new client test code:

ctx.LoadProperty(boss, "Employees");

CountSubordinates(bossId);

boss.Employees.Add(grunt);
ctx.AddLink(boss, "Employees", grunt);
ctx.SaveChanges();

CountSubordinates(bossId);

boss.Employees.Remove(grunt);
ctx.DeleteLink(boss, "Employees", grunt);
ctx.SaveChanges();

CountSubordinates(bossId);

Here, the CountSubordinates creates a separate query context to validate the data independently. When we execute this, we immediately get errors in the data-service about "AddReferenceToCollection", then "RemoveReferenceFromCollection". To implement these methods, note that collection associations in LINQ-to-SQL are EntitySet instances, which implement IList:

public static void AddReferenceToCollection(
DataContext context, object targetResource,
string propertyName, object resourceToBeAdded)
{
IList list = (IList) GetValue(context,
targetResource, propertyName);
list.Add(resourceToBeAdded);
}

public static void RemoveReferenceFromCollection(
DataContext context, object targetResource,
string propertyName, object resourceToBeRemoved)
{
IList list = (IList)GetValue(context,
targetResource, propertyName);
list.Remove(resourceToBeRemoved);
}

Here we are using our existing "GetValue" method with a simple cast, then just adding the extra data.

Implementing Single (/Parent) Associations

Individual associations are handled differently to collection associations; this time, we'll set the manager of the subordinate directly on the subordinate:

CountSubordinates(bossId);

grunt.Manager = boss;
ctx.SetLink(grunt, "Manager", grunt.Manager);
ctx.SaveChanges();

CountSubordinates(bossId);

grunt.Manager = null;
ctx.SetLink(grunt, "Manager", grunt.Manager);
ctx.SaveChanges();

CountSubordinates(bossId);

The "SetLink" method reminds the data-context that we care... This time, the data-service breaks on the "SetReference" method. Fortunately, since we are treating resources and instances as interchangeable, this is trivial to implement with our existing "SetValue" method:

public static void SetReference(
DataContext context, object targetResource,
string propertyName, object propertyValue)
{
SetValue(context, targetResource,
propertyName, propertyValue);
}

With this in place, our Manager is updated correctly, which can be validated in the database.

Summary - and Why oh Why...

So; we've got associations working in either direction. But te "SetLink" etc nags at me. It seems very unusual to have to do this so manually. And annoyingly, the standard client-side code:

  • doesn't implement INotifyPropertyChanged - which means the objects don't work well in an "observer" setup
  • doesn't provide an OnPropertyChanged (or similar) partial method - which means we can't conveniently add the missing functionality

If either of these were done, I'm pretty certain we could hook some things together so that you (as the consumer) don't need to do all this work, perhaps using events to talk to the context (rather than referencing the context directly from the object, which has some POCO issues). There are lots of On{Foo}Changed partial methods, but you'd have to do them all manually.

Maybe PostSharp would help here. Or maybe ADO.NET Data Services should include these features; I might look at this later...

Friday, 12 December 2008

Astoria and LINQ-to-SQL; modifying data

Our ADO.NET Data Service can now act to create records... but how about changing existing data? Starting simple, we'll now look at removing records from the database. We'll update the test client:

// create it
Employee newEmp = new Employee {
FirstName = "Fred",
LastName = "Jones"};
ctx.AddToEmployees(newEmp);
Console.WriteLine(newEmp.EmployeeID);
ctx.SaveChanges();
Console.WriteLine(newEmp.EmployeeID);

// delete it
ctx.DeleteObject(newEmp);
ctx.SaveChanges();
Console.WriteLine(newEmp.EmployeeID);

Implementing Delete

This time, our DataService<NorthwindDataContext> code (from the previous article) explodes first on the mysterious "GetResource" method. This curious beast is used to resolve a resource from a query. Fortunately, since we are treating "resource" and "instance" as interchangeable, we can simply offload most of this work to the regular provider (LINQ-to-SQL in this case):

public static object GetResource(
DataContext context, IQueryable query,
string fullTypeName)
{
object execResult = query.Provider
.Execute(query.Expression);
return ((IEnumerable) execResult)
.Cast<object>().Single();
}

Here, "Provider.Execute" uses LINQ-to-SQL to build the query; we can't easily predict what data we will get back, but we ultimately expect it to be an enumerable set, with exactly one item - so LINQ-to-Objects can do the rest for us via Cast<T>() and Single().

With this implemented, we now get the largely anticipated error on "DeleteResource"; again this is fairly simple to implement:

public static void DeleteResource(
DataContext context, object targetResource)
{
ITable table = context.GetTable(
targetResource.GetType());
table.DeleteOnSubmit(targetResource);
}

And sure enough, it works; we can verify that the data is removed from the database.

When abstraction attacks

As a minor observation (yet more LOLA), I also notice that attempting (at the client) to query data that has been removed by primary key doesn't behave quite as we might expect:

var ctx = new NorthwindDataContext(BaseUri);
Employee emp = ctx.Employees
.Where(x => x.EmployeeID == id)
.SingleOrDefault();

With an invalid id, this breaks with "Resource not found for the segment 'Employees'."; this applies for any combination of First/Single, with/withot OrDefault, and with/without AsEnumerable() - basically, ADO.NET Data Services sees the primary key, and asserts that it must exist. Something else to remember!

Implementing Update

Interestingly, the combination of "GetResource" (which we implemented for delete) and "SetValue" (which we implemented for create) provides everything we need for simple updates. And sure enough, we can test this from the client. Note that the ADO.NET Data Services client requires us to be explicit about which records we want to update (there is no automatic change tracking):

newEmp.BirthDate = new DateTime(1980,1,1);
ctx.UpdateObject(newEmp); // mark for send
ctx.SaveChanges();

Summary

We're getting there; we now have our 4 fundamental CRUD operations. Next, we'll look at associations between data. We are whittling through the operations, though - we have remaining (not implemented):

  • Association 
    • AddReferenceToCollection
    • RemoveReferenceFromCollection
    • SetReference
  • Batch (rollback) 
    • ClearChanges
    • ResetResource

Astoria and LINQ-to-SQL; creating data

Previously, we looked at how to get LINQ-to-SQL talking to ADO.NET Data Services to query data - but how about editing data? This again just works for Entity Framework via the metadata model. So what about LINQ-to-SQL? Lets give it a whirl... we'll start with some simple code at the client to create a new Employee:

Employee newEmp = new Employee {
FirstName = "Fred",
LastName = "Jones"};
ctx.AddToEmployees(newEmp);
ctx.SaveChanges();

Give that a go, and boom! "The data source must implement IUpdatable to support updates.". IUpdatable is the ADO.NET Data Services interface that allows a REST service to talk back to the model... and it isn't trivial:

image

Hmm... lots to do. We should also note that the IUpdatable model uses two types of object:

  1. resources - an opaque, implementation-defined notion of an instance (anything you want that allows you to map to an instance)
  2. instances - actual entity objects (such as Customer)

LINQ-to-SQL is already highly object oriented (and to do anything useful with the instance we'll need the actual object from the DataContext), so for simplicity I'll simply treat these as one-and-the-same; our opaque resource will be the instance itself.

Defining our own Data Context

This is one of those cases where the simplest approach is to use inheritance - i.e. create a WebDataContext class (that uses explicit implementation to satisfy the interface) from which we can inherit our own DataContext. Note that since people may already have their own inheritance trees, I'll keep the implementation code separate, allowing us to use the code even if we haven't subclassed. Here's a very small part of it:

image

Luckily, LINQ-to-SQL allows you to control the base-class of the data-context, so change the dbml to point at our new WebDataContext (using the namespace-qualified name), and it will update the generated code accordingly. In the designer this is the "Base Class" when you don't have any table selected. In the xml, it is the "/Database/@BaseType" property.

Remembering that we haven't actually coded any of it yet, fire it up. This time, we get an error at the client: "The method or operation is not implemented.", which we expected; the debugger conveniently breaks at the offending methods, letting us locate them.

Implementing Create

The first thing that breaks is CreateResource; we need to implement this to create the new object to use. So we'll add a static method to our WebUpdateable class, and point our base-class at it. CreateResource supplies a "containerName" - this is essentially a property name for the data we are adding. Creating an object of a known type is easy enough - but what type? We could use the provided "fullTypeName" (and I'll come back to this later), but a simple approach might be simply to look at the container itself: if the container is an IQueryable<T>, we probably want to create a T. I say probably because LINQ-to-SQL supports discriminator-based inheritance, but we'll park that issue for now...

For re-use, it is useful to write a method that uses reflection to identify the type from IQueryable<T>:

static Type GetContainedType(
DataContext context, string containerName)
{
PropertyDescriptor property = TypeDescriptor
.GetProperties(context)[containerName];
if(property == null) throw new ArgumentException(
"Invalid container: " + containerName);

return (
from i in property.PropertyType.GetInterfaces()
where i.IsGenericType
&& i.GetGenericTypeDefinition() ==
typeof (IQueryable<>)
select i.GetGenericArguments()[0]
).Single();
}

With this in place, we can now create objects, and (importantly) add them to the table; fortunately, DataContext allows you to work with tables by type:

public static object CreateResource(
DataContext context, string containerName,
string fullTypeName)
{
Type type = GetContainedType(context,
containerName);
// TODO: anything involving inheritance
ITable table = context.GetTable(type);
object obj = Activator.CreateInstance(type);
table.InsertOnSubmit(obj);
return obj;
}

Then all we need to do is hook that into our WebDataContext base-class:

object IUpdatable.CreateResource(
string containerName, string fullTypeName)
{
return WebUpdatable.CreateResource(
this, containerName, fullTypeName);
}

Note that all of the interface implementations in WebDataContext will be shallow methods that forward to the static methods in WebUpdatable, so I won't repeat them.

Run that, and this time it breaks at SetValue; ADO.NET Data Services wants us to update the object-model with the values it supplies. Fortunately, this is very simple via reflection (or TypeDescriptor), so I'll show the implementation and the interface implementation together (and we'll cover GetValue at the same time):

public static void SetValue(
DataContext context, object targetResource,
string propertyName, object propertyValue)
{
TypeDescriptor.GetProperties(targetResource)
[propertyName].SetValue(targetResource,
propertyValue);
}

public static object GetValue(
DataContext context, object targetResource,
string propertyName)
{
return TypeDescriptor.GetProperties(
targetResource)[propertyName]
.GetValue(targetResource);
}

The next time we try the service, it breaks at SaveChanges; again, this is simple to implement with LINQ-to-SQL:

public static void SaveChanges(
DataContext context)
{
context.SubmitChanges();
}

After this, ResolveResource breaks; for data inserts, this can be implemented trivially:

public static object ResolveResource(
DataContext context, object resource)
{
return resource;
}

Validation

Finally, we can validate (at the client) that our data is saved:

Console.WriteLine(newEmp.EmployeeID);
ctx.SaveChanges();
Console.WriteLine(newEmp.EmployeeID);

And hoorah! It did indeed save; we can look in the database to see the new records.

Summary

We've now truly dipped our toe in the ADO.NET Data Services pool; we can create data! Always useful. Next, we'll look at modifying the records we have just added.

Brute force (but lazily)

Seemingly an oxymoron, but not in the world of code...

As part of my current mini-project, I recently needed to write some code to use brute force to solve a problem (i.e. test every combination to see what works). There might be 0, 1, or many answers to the problem, and the caller might:

  • just care that there is a solution, but not what it is
  • want any solution
  • want every solution
  • want the only solution
  • etc

Now, first off - brute force is a mixed approach; you need to understand your data and your problem to know whether such an approach will ever work, and what the performance will be. In this case, the candidates were in the crunchable range; it might take a bit of CPU, but it isn't going to be terrible, and we aren't doing it in a tight loop.

I was very happy with my approach to the problem, so I thought I'd share... in short: iterator blocks.

Basically, rather than write a method of the form:

// NOT actual C#!!!
Answer[] SolveProblem(question)
{
List<Answer> answers = new List()
for(lots of crunching, looping, etc)
{
if(answer is good) answers.Add(answer);
}
return answers.ToArray();
}

(or anything more complex involving the number of answers needed, etc), I wrote it as:

// NOT actual C#!!!
IEnumerable<Answer> SolveProblem(question)
{
for(lots of crunching, looping, etc)
{
if(answer is good) yield return answer;
}
}

This gives us lazy (on-demand) processing for free; and the caller can use regular LINQ extension methods to handle the results in the appropriate way; so taking the previous bullets in order (where "qry" is our IEnumerable<Answer>):

  • qry.Any()
  • qry.First()
  • qry.ToArray()
  • qry.Single()
  • everything else you can think of... Skip(), Take(), Where(), FirstOrDefault(), etc

And job done! Every query will read just as much data as it wants; no more, no less. So First() will stop crunching once the first answer is found, where-as ToArray() will run the process to exhaustion. Very tidy.

As a footnote, I should note that my particular problem was recursive (but not massively so); I solved this by simply injecting an extra loop:

foreach(Answer subAnswer
in SolveProblem(different question)
{
yield return subAnswer;
}

Wednesday, 10 December 2008

Astoria and LINQ-to-SQL; getting started

ADO.NET Data Services (previously known as "Astoria") is one the new .NET 3.5 SP1 tools for offering CRUD data access over the web.

In this article, I'll discuss using ADO.NET Data Services, looking first at Entity Framework, but (since I'm a firm believer in parity across implementations) more specifically, looking at LINQ-to-SQL.

Although ADO.NET Data Services uses WCF, it is at heart a RESTful stack (not a SOAP operation stack), that offers:

  • Simple (REST/CRUD) platform-independent access to data, including filtering etc, but limited to homogenous data and full object representations [no custom projections at the point of query]
  • A .NET client and object model, including change-tracking support, making it easy to make complex changes
  • Support for LINQ at the client, which is translated into requests that the REST layer understands

A very powerful tool!

Exposing data via Entity Framework

So: how can we use it? Unsurprisingly, it plays very nicely with Entity Framework: you simply create a data-service endpoint (using the "ADO.NET Data Service" template in VS 2008), and tell it about your object-context:

    public class EntityService : DataService<NorthwindEntities> // your object-context here...
{
public static void InitializeService(IDataServiceConfiguration config)
{
// specify permissions for entity-sets and operations
config.SetEntitySetAccessRule("*", EntitySetRights.All); // debug only; don't do this!
}
}

And that is it! Navigate to the .svc in a browser, and you should see the REST/ATOM-based response:

<service [snip]>
<workspace>
<atom:title>Default</atom:title>
<collection href="Categories">
<atom:title>Categories</atom:title>
</collection>
<collection href="CustomerDemographics">
<atom:title>CustomerDemographics</atom:title>
</collection>
[snip]

This shows the navigable entity-sets - so adding /Categories to our request would return the categories, etc. So: that's Entity Framework sorted... how about LINQ-to-SQL?

Exposing data via LINQ-to-SQL

You might be slightly surprised that things aren't nearly so tidy. It transpires that ADO.NET Data Services will respect either a metadata-model (ala Entity Framework), or it will support a per-implementation fallback model otherwise. And since LINQ-to-SQL pre-dates ADO.NET Data Services, LINQ-to-SQL doesn't qualify (out of the box) for either. We need to wire a few things together... we'll look at exposing data in this article, and then consider updating data as a separate piece of work.

So - lets be optimistic, and change NorthwindEntities (our Entity Framework object-context) to NorthwindDataContext (our LINQ-to-SQL data-context). Boom! We get a big fat "Request Error", "The server encountered an error processing the request. See server logs for more details.":

image

So the first thing we need is some better debugging... as a debugging aid (i.e. not on a production server), we can get a lot more information by tweaking our DataService:

    [ServiceBehavior(IncludeExceptionDetailInFaults = true)] // ### show error details in the response
public class EntityService : DataService<NorthwindDataContext> // your data-context here...
{
public static void InitializeService(IDataServiceConfiguration config)
{
config.UseVerboseErrors = true; // ### and tell us as much as you can
// specify permissions for entity-sets and operations
config.SetEntitySetAccessRule("*", EntitySetRights.All); // debug only; don't do this!
}
}

We've enabled exception-details in the response, and enabled verbose errors. It still doesn't work, but at least we get a clue why: "On data context type 'NorthwindDataContext', there is a top IQueryable property 'CustomerDemographics' whose element type is not an entity type. Make sure that the IQueryable property is of entity type or specify the IgnoreProperties attribute on the data context type to ignore this property.":

image

Hmm... we don't want to ignore the property (or we cant get at the data), and it isn't an EntityObject in the Entity Framework sense, so what do we do?

It turns out that what this message actually means here is "I can't find the primary key"; outside of metadata-models, it supports a few ways of doing this:

  • via the new [DataServiceKey] attribute
  • by looking for a "FooID" property, where the type is called "Foo"
  • by looking for an "ID" property

As it happens, CustomerDemographic uses CustomerTypeID for the primary key, so no surprise. Fortunately, the designers seem to have considered the (likely) scenario of generated code, and so we can specify [DataServiceKey] at the type-level in a partial class. This is fortunate, since you can't use a partial class to add an attribute to a member (such as a property) defined in a separate portion of a partial class. So we add (in our data-context namespace):

    [DataServiceKey("CustomerTypeID")]
partial class CustomerDemographic {}

And likewise for any other types that it isn't happy with. In general, the "FooID" pattern is so ubiquitous that it catches most use-cases. One minor gripe here is that it is case-sensitive: "FooId" won't work without a [DataServiceKey]. Something else to remember for that monthly "ID vs Id" squabble...

Interestingly, note that we haven't done anything LINQ-to-SQL specific yet; everything is just hanging off the exposed IQueryable<T> propertieson our data-context, so everything here would also work with LINQ-to-Objects.

Consuming the Data

Our service now shows in a browser without a big error - so  we can look at creating a client. We can do this in VS2008 simply using the "Add Service Reference..." dialog; it recognises REST/ATOM endpoints, and behaves accordingly:

image

Alternatively, you can use "datasvcutil" at the command-line, but (unusually, compared to wsdl.exe and svcutil.exe) you get a very limited set of command-line options here. After generating the client, you can use it very similarly to a LINQ-to-SQL data-context:

        static void Main()
{
var ctx = new NorthwindDataContext(BaseUri); // note not IDisposable
// enable crude logging
ctx.SendingRequest += (sender, e) => Debug.WriteLine(
e.Request.Method + ": " + e.Request.RequestUri);

ShowEmployees(ctx.Employees.Take(20));
ShowEmployees(from emp in ctx.Employees
where emp.FirstName.StartsWith("M")
select emp);
ShowEmployees(from emp in ctx.Employees
orderby emp.FirstName descending
select emp);
            Employee tmp = ctx.Employees.Where(x=>x.EmployeeID == 9).Single();
}
static void ShowEmployees(IQueryable<Employee> query)
{
foreach(var emp in query)
{
Console.WriteLine("{0}: {1}, {2}",
emp.EmployeeID, emp.LastName, emp.FirstName);
}
}

Which displays the correct employees, and also shows the HTTP request uris:

GET: http://localhost:28601/Restful.svc/Employees()?$top=20
GET: http://localhost:28601/Restful.svc/Employees()?$filter=startswith(FirstName,'M')
GET: http://localhost:28601/Restful.svc/Employees()?$orderby=FirstName desc
GET: http://localhost:28601/Restful.svc/Employees(9)

These queries give a lot of insight into how it all hangs together. Some points:

  • Count() is not supported, which is a shame as it is so universally useful.
  • Single(predicate) is not supported - you must use Where(predicate).Single(); this is doubly LOLA, since it contrasts with a bug in LINQ-to-SQL, where the identity-manager can only short-circuit primary-key lookups (avoid hitting the database) if you use .Single(predicate). Oh well; in Entity Framework it doesn't work *at all*, so perhaps I should be grateful.
  • We are limited to homogenous results - i.e. we can't do complex projections over the wire. We can, of course, do simple queries, then use LINQ-to-Objects (with .AsEnumerable() if necessary) to do the complex bits at the client - just remember that all the entity properties are coming back, even if you ignore \most of them.

Summary

Exposing LINQ-to-SQL isn't particularly hard; with a few simple tweaks, it generally works. The client tools provide a rich (but not quite complete) set of query tools for consuming ADO.NET Data Services.

Next, we'll see how deep the hole goes, by trying to submit changes to our LINQ-to-SQL ADO.NET Data Service.

Entity Framework in reality

I'm currently working on a mini-project with Microsoft (more news on this next year) - and the project team are keen for this project to be exemplar of recommended MS technologies. So (where appropriate), the system makes use of Entity Framework (EF) and ADO.NET Data Services - two technologies that I know a bit about, but have only previously toyed with (although I've done a lot with their cousins, LINQ-to-SQL and WCF).

Now, I've commented a few times in the past about the short-comings of EF, but this project has been a good opportunity to do something non-trivial start-to-end with it. And while I'm still not an EF evangelist, I'm now not quite so hostile towards it.

The truth is: if I hadn't seen LINQ-to-SQL, I'd probably be delighted with EF as a LINQ back-end. Forgetting about architectural purity concerns (POCO, etc), there are still a number of annoying glitches with EF, including:

LOLA - things that work with most other IQueryable<T> implementations, but not EF:

  • No support for Single()
  • No support for expression-composition (Expression.Invoke)
  • Skip()/Take() demand an ordered sequence (IOrderedQueryable<T>)

General annoyances:

  • Limited support for composable UDF usage
  • A few niggles with the detach mechanism
  • Some lazy loading issues
  • Lack of "delete all" - a pain for reset scripts, but probably not very helpful in production code ;-p

On the plus side, the ability to remove trivial many-to-many link tables from the conceptual model makes the programming experience much nicer - but this is the only thing I've used in EF that I couldn't have done just as easily with LINQ-to-SQL.

But: it gets the job done. If anything, the exercise has gone a long way to making me feel warm and cosy about the future of EF: if the team can take the flexibility of the EF model, but squeeze in the bits that LINQ-to-SQL gets so right, I'll be a happy coder.

(In my next post, I hope to discuss my experiences with ADO.NET Data Services)