Why don't you rip with me, the maniac psycho Cuz when I pull out my jammy, get ready cuz it might go Blauh!In my last post on LinqToSql, I covered the DataContext and likened it to the Great Gazoo and Phoebe Cates - possibly the worst analogies in any form of tech writing, ever. But that's what you've come to expect so I won't disappoint. This aint no Peanut Butter Jelly Time or Hamster Dance ya know... In this post I'm going to take it a step further and show you how you can polish the chrome on LinqToSql so that you can cleanly separate your What from your How. I'm also going to go a little bit into how SubSonic fits into the whole "LINQ thang". Don't call it a comeback For the last week I've been beating on LinqToSql because, quite frankly, I want to know just where SubSonic fits in. If the ORM tool that ships with .NET 3.x does what we do, there's no reason to duplicate. SubSonic's all about improving YOUR experience, and we can just as easily wrap our loving arms around L2S just as easily as any other data access system. The challenge here isn't the technology - it's how it's supposed to be used. In other words - I have a problem with LINQ and how easy it is to stick data access code where it shouldn't go. It's twice as tempting with LinqToSql. When first created, your model practically BEGS you to let it run loose in your code behind, batting it's eyes innocently at you as it sticks the fake ID in its back pocket, ready to crawl out of your application come midnight. We've tried to make SubSonic as malleable as we can so that you, as the architect, can use its goodness wherever you please. LinqToSql sort of does this, but it's not straightforward from the get-go. I been here for years, Rockin' my peers ORM's been around a long time, and it bears mentioning right up front that
This is ORM, the Microsoft WayI'll explain more as I go along - but keep that thought in your mind. The best explanations involve lots of code, so let's jump right in. The first thing you want to do is create a separate project. This will make sure that the protection patters we use below can be enforced. So right-click, add a class library project, remove Class1.cs (why do they put that in there?), and then right-click your project, selecting "Add Linq to SQL classes". It's a good idea to name it something relevant - like NorthwindSchema or something. When finished, you get a nice Class Diagram that lets you get your ORM on:
NorthwindContext db=new NorthwindContext();
Gridview1.DataSource=db.Products;
Gridview1.DataBind();
Man, that's easy! It's actually too easy - and will tempt you into some bad habits; namely writing the code above.
Puttin' suckers in fear
Every project is "small, quick and simple" at some point - even for the prototype. That's not a license to be sloppy, and often the compromises made to run out a quick prototype never get refactored out like they should (guilty as charged). While I completely agree with the Premature Optimization theory, it's still important to make sure that your skeleton approach is sound so you don't build crap on a pile of crap, wrapped crappily in crap coating.
Things to pay attention to up front:
NorthwindContext db=new NorthwindContext();
List<Product> prods=db.Products.Where(p => p.CategoryID==5).ToList<Product>();
This is OK most of the time, but it's a bit of a pain to work with Lists directly - especially if you want to extend them. An example of extending List<> might be for sorting, or overriding the index method. You also might want to add some business logic to a List<> - say OrderItems.AdjustQuantity() for instance.
Before I work with the model, I'm going to abstract the List<> by entity - in other words I'm going to create Collection classes for the classes that need it (I probably don't need collection classes for all my tables). For this post that means the product table. Being nice and tidy, I'll create a folder called "Collections" and in it, I'll add a new class called "ProductCollection":
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Northwind.Data; namespace Northwind.Data { public class ProductCollection:List<Product> { } }Doing it this way locks things down nice and tight, and we've effectively erased LINQ's footsteps from our application :). Don't ever compare me to the rest; They'll all get sliced and diced In my last LinqToSql post I mentioned there was some weirdness with the way Update() works (Rick goes into it a lot here), and how the DataContext was attached to the model at the hip. This is important so I'll go into that now. Up above, in this post, I mentioned this is ORM done Microsoft's way. That's a very important thing to consider when working with the DataContext and updating data. Built into LinqToSql is automatic change tracking, with Optimistic Concurrency checks turned on. What this means is that if you pull a record from the DB, work on it, and someone else comes and updates the same record in the DB, your changes are likely to be invalid (or else piss someone else off extremely). This all works nicely in the DataContext, but I don't want that thing lurking all over my application - I want to do things in one place. This is where ORM has a shortfall - when you try to go outside it's rules it doesn't like you anymore. LinqToSql let's you work outside it's context, but if you want to keep playing with it you need to be a good developer and consider concurrency issues in your model. There are two ways to do this:
public static class NorthwindService { //static single-point context builder static Northwind.NorthwindContext db { get { return new NorthwindContext(); } } #region Products public static Product ProductGet(int id){ return db.Products.Single(p => p.ProductID==id); } public static ProductCollection ProductsByCategory(int categoryID){ List<Product> prods=db.Products.Where(p => p.CategoryID==categoryID).ToList<Product>(); return prods as ProductCollection; } public static int ProductSave(Product p){ NorthwindContext context=db; bool isNew=p.Version==null; if(isNew){ context.Products.Add(p); }else{ context.Products.Attach(p, true); } context.SubmitChanges(); return p.ProductID; } #endregion #region Employees public static Employee EmployeeGet(int id) { return db.Employees.Single(e => e.EmployeeID == id); } public static int EmployeeSave(Employee e) { NorthwindContext context = db; bool isNew = e.EmployeeID == 0; if (isNew) { context.Employees.Add(e); } else { context.Employees.Attach(e, true); } context.SubmitChanges(); return e.EmployeeID; } #endregion }Notice the ProductSave() method - by using a timestamp field called "version", I know whether it's been newly created. I could test the primary key value as well, but this type of thing doesn't always hold. The Customers table, for instance, uses a char(5) value for customerID. Since I have that timestamp field for Products, I can run the attach() method and Linq wont' complain. Also, since I told Linq to cheese off with regards to checking the Employees table, EmployeeSave also works, but I still have an issue. The check of the Primary Key is not really a good way to gaurantee that the passed in class is new. Currently this type of state checking isn't available in LinqToSql (that I've seen). To get around this I can create a set of partial classes for all of my objects and hook into their events to set some properties - but that's Fudge (note the capital F). The other thing I can do is to set something that LinqToSql should have set for me in the first place - the Read Only property for the primary key (it should be set to true - it's not by default):
Technorati Tags: LINQ, LinqToSql, Rob Conery, SubSonic
Please help me.
Your collection:
namespace Northwind.Data
{
public class ProductCollection:List {}
}
Use:
public static ProductCollection ProductsByCategory(int categoryID)
{
List prods=db.Products.Where(p => p.CategoryID==categoryID).ToList();
return prods as ProductCollection;
}
Returns null.
If you use like this
return (ProductCollection)prods;
Throws
Unable to cast object of type 'System.Collections.Generic.List`1[Northwind.Data.Product]' to type 'ProductCollection'.
I have been playing with your example as I have had loads of problems with databasecontext and attach
I have kept it super simple and used your code - with minor changes so it works with RTM
but I still get an error when I write ... (I have added the version field (timestamp) as you suggested
Product p = NorthwindService.ProductGet(1);
p.ProductName = "testing";
NorthwindService.ProductSave(p);
I get
NUnitTests.NUnitTests.Test2 : System.NotSupportedException : An attempt has been made to Attach or Add an entity that is not new, perhaps having been loaded from another DataContext. This is not supported.
I can "fix" the problem by keeping a single databasecontext alive but this is just a simple test
I want to get a product - then later allow it to be updated and the only way to keep a single databasecontext available
I have read numerous other blogs about this and cannot seem to find a clear way forward.
I am(was !) just about to teach this stuff to a group of Masters students but am not sure now - might go back to Wilson ORM or Subsonic
Thanks for any help
Mike
It would seem you have stirred some discussion with this post. Good work as always.
The image links on your post is broken; I think they would help me understand some of the directions here.
Best of luck!
Bill
Great articles, I was wondering if you could update the images. They seem to be broken.
Thanks again!
Donn