This is the fifteenth in a series of posts on how to build a LINQ IQueryable provider. If you have not read the previous posts you might try searching for the audio tapes on www.Bing.com. That would be a lot easier than reading. You won't find any, but you'll feel better for having tried.
Complete list of posts in the Building an IQueryable Provider series
Getting this version of the toolkit together has taken a lot of sleepless nights. Thank goodness for Netflix or I'd have had to spend those sleepless nights actually working on the Toolkit.
Okay, enough with the flavor text, let's get to the crunch.
What's inside:
More Providers - MySQL and SQLite join the previous MS only line up. Transactions - Use ADO transactions to control the isolation of your queries & updates. Entity Providers - The provider concept is expanded to include tables of entities Entity Sessions - The session concept adds identity caching, change tracking and deferred updates via SubmitChanges Provider Factory - Create providers on the fly w/o knowing anything more than the database name and mapping. Madness
More Providers - MySQL and SQLite join the previous MS only line up.
Transactions - Use ADO transactions to control the isolation of your queries & updates.
Entity Providers - The provider concept is expanded to include tables of entities
Entity Sessions - The session concept adds identity caching, change tracking and deferred updates via SubmitChanges
Provider Factory - Create providers on the fly w/o knowing anything more than the database name and mapping.
Madness
The full source code and redistributable DLL's can be found at:
http://www.codeplex.com/IQToolkit
I'm surprised no one's called me on this before. The DbQueryProvider and its ilk have been suspiciously lacking in support for transactions. The providers work, LINQ queries are converted into ADO Commands and executed, yet those ADO Command objects are never assigned an ADO transaction, even if you started one explicitly. Of course, the official word from Microsoft is to stop using the ADO transactions altogether and instead use the System.Transactions.TransactionScope object, that is newer, better and enables automatic use of distributed transactions, etc, etc, etc. And if you did use TransactionScope, then the problem I'm referring to would not be a problem. SqlCommand object's would implicitly enlist in the transation without me having to specify anything. Unfortunately, TransactionScope is mired with many problems and is not supported by all ADO providers so ADO transactions are still a necessity. You can now use ADO transactions with query providers in a manner similar to LINQ to SQL. provider.Transaction = provider.Connection.BeginTransaction(); // use the provider here to execute queries and updates, etc. provider.Transaction.Commit(); provider.Transaction = null; The provider will use whatever transaction object you give it when it creates new ADO command objects.
I decided to formalize the pairing of query providers with a Table object that enables updates and other facilities. The definition of an entity provider is now defined by these three interfaces.
public interface IEntityProvider : IQueryProvider { IEntityTable<T> GetTable<T>(string tableId); IEntityTable GetTable(Type type, string tableId); } public interface IEntityTable : IQueryable, IUpdatable { new IEntityProvider Provider { get; } string TableId { get; } object GetById(object id); int Insert(object instance); int Update(object instance); int Delete(object instance); int InsertOrUpdate(object instance); } public interface IEntityTable<T> : IQueryable<T>, IEntityTable, IUpdatable<T> { new T GetById(object id); int Insert(T instance); int Update(T instance); int Delete(T instance); int InsertOrUpdate(T instance); }
You can now always get to a table directly from a provider. The two concepts are coupled together. An entity table also has explicit CRUD methods and implements IUpdatable, so no more separation between normal tables and updatable tables. In my mind this simplifies things quite a bit. Of course, this caused me to want to rename DbQueryProvider. Ooops. This will likely cause you some grief as any of your existing code that was using DbQueryProvider directly is now not going to compile. The new name for this class is now DbEntityProvider. It might not matter so much now that there is a nifty IEntityProvider interface.
One thing missing from the Toolkit so far has been all of that context stuff that LINQ to SQL and LINQ to Entities have. When you use LINQ to SQL you have a change tracking service that detects when your objects change, and sends the updates for you all at the same time when you call SubmitChanges. An entity session is all of this change-tracking, deferred updating stuff packaged up together. It is distinctly different from an entity provider in these ways, yet similar to one in many others. An entity session is defined below:
public interface IEntitySession { IEntityProvider Provider { get; } ISessionTable<T> GetTable<T>(string tableId); ISessionTable GetTable(Type elementType, string tableId); void SubmitChanges(); } public interface ISessionTable : IQueryable { IEntitySession Session { get; } IEntityTable ProviderTable { get; } object GetById(object id); void SetSubmitAction(object instance, SubmitAction action); SubmitAction GetSubmitAction(object instance); } public interface ISessionTable<T> : IQueryable<T>, ISessionTable { new IEntityTable<T> ProviderTable { get; } new T GetById(object id); void SetSubmitAction(T instance, SubmitAction action); SubmitAction GetSubmitAction(T instance); } public enum SubmitAction { None, Update, PossibleUpdate, Insert, InsertOrUpdate, Delete } public static class SessionTableExtensions { public static void InsertOnSubmit<T>(this ISessionTable<T> table, T instance) { table.SetSubmitAction(instance, SubmitAction.Insert); } public static void InsertOnSubmit(this ISessionTable table, object instance) { table.SetSubmitAction(instance, SubmitAction.Insert); } public static void InsertOrUpdateOnSubmit<T>(this ISessionTable<T> table, T instance) { table.SetSubmitAction(instance, SubmitAction.InsertOrUpdate); } public static void InsertOrUpdateOnSubmit(this ISessionTable table, object instance) { table.SetSubmitAction(instance, SubmitAction.InsertOrUpdate); } public static void UpdateOnSubmit<T>(this ISessionTable<T> table, T instance) { table.SetSubmitAction(instance, SubmitAction.Update); } public static void UpdateOnSubmit(this ISessionTable table, object instance) { table.SetSubmitAction(instance, SubmitAction.Update); } public static void DeleteOnSubmit<T>(this ISessionTable<T> table, T instance) { table.SetSubmitAction(instance, SubmitAction.Delete); } public static void DeleteOnSubmit(this ISessionTable table, object instance) { table.SetSubmitAction(instance, SubmitAction.Delete); } }
As you can see, an entity session has tables, just like a provider. Yet, those tables are not directly updatable. Instead you can assign entity instances submit actions. These are the actions that take place later when you call SubmitChanges. There are a bunch of extension methods defined to add the LINQ to SQL like InsertOnSubmit() methods to the interface. These simply call the SetSubmitAction() method for you. Also note that a session is not a provider. It is a service used in conjunction with a provider. You can use multiple different sessions with the same provider instance. There is one current implementation of an entity session in the Toolkit called (you guessed it) DbEntitySession. You create a DbEntitySession by giving it an existing DbEntityProvider. The DbEntitySession hooks the provider in such a way that it gets first crack at all materialized objects before they are returned to you. In this way, the DbEntitySession can employ an identity cache so queries that retrieve the same entity will always return the same entity instance, and it can start automatic change tracking on all entities returned. You are also not locked into the session's behavior. At any time you can interact with the underlying provider instead for retrieving entities without passing through the identity cache or being changed tracked. You can even get to the provider's table directly off a session table.
Now with so many providers and one single way to write queries you'd think it would be easy to switch between them. In reality it is not. You have to pick the provider you want, reference its library (IQToolkit.Data.XXX), reference its corresponding ADO library (System.Data.XXX), create the ADO connection, the mapping object and construct the provider. var connection = new SqlConnection("..."); var mapping = new AttributeMapping(typeof(Northwind)); var provider = new SqlProvider(connection, mapping, QueryPolicy.Default, null); var db = new Northwind(provider); You can hide this all inside your database context class (or whatever you want to call yours), so you only have to write it once, but then your context class is tied to a specific provider. Instead, you could wrap this code up into a factory method of your own devising, but then calls to the factory would be spread throughout your codebase. There no good way to defer all this work to some configuration setting. Until now. Introducing the new factory methods built into DbEntityProvider.public static DbEntityProvider FromApplicationSettings(); public static DbEntityProvider From(string filename, string mappingId); public static DbEntityProvider From(string provider, string connectionString, string mappingId);
Now with so many providers and one single way to write queries you'd think it would be easy to switch between them. In reality it is not. You have to pick the provider you want, reference its library (IQToolkit.Data.XXX), reference its corresponding ADO library (System.Data.XXX), create the ADO connection, the mapping object and construct the provider. var connection = new SqlConnection("..."); var mapping = new AttributeMapping(typeof(Northwind)); var provider = new SqlProvider(connection, mapping, QueryPolicy.Default, null); var db = new Northwind(provider); You can hide this all inside your database context class (or whatever you want to call yours), so you only have to write it once, but then your context class is tied to a specific provider. Instead, you could wrap this code up into a factory method of your own devising, but then calls to the factory would be spread throughout your codebase. There no good way to defer all this work to some configuration setting. Until now. Introducing the new factory methods built into DbEntityProvider.
public static DbEntityProvider FromApplicationSettings(); public static DbEntityProvider From(string filename, string mappingId); public static DbEntityProvider From(string provider, string connectionString, string mappingId);
These methods allow you to get up and running with only knowing a few bits of information. You don't have to hard link you application to any particular provider. The FromApplicationSettings method creates you a new instance of a provider from information found in the config file. It looks for the "Provider", "Connection" and "Mapping" properties in the configuration and feeds them to the other factories. It is also possible to look this information up in web settings, but I have not formalized that one yet. The provider argument is a string that refers to the name of an assembly that contains the query provider. These are generally of the form IQToolkit.Data.XXX. If that assembly is not loaded, it will be loaded dynamically. This assembly can be in the assembly cache or in the same directory as your app (or other places that the runtime might look.) From this assembly it will look for a type in the same namespace (as the name of the assembly) that derives from DbEntityProvider. The connectionString and filename arguments are really the same thing. You can specify either the name of a database file or a full ADO connection string. If a file is specified, a correct connection string is obtained by calling the static GetConnectionString(string) method on the provider. A provider may be inferred from the file extension of a database file if none is specified. The mappingId can either refer to the name of a context class (like Northwind) that has mapping attributes on it or the name of an xml file. So now you can write code like this to get your provider. var db = new Northwind(DbEntityProvider.From(somedbfile, somemapfile)); Or better yet, you can use the FromApplicationSettings() method in the constructor of your context and still be configurable at runtime.
Of course, it wouldn't be a new toolkit release without some additional crazy changes. One significant change is namespaces again. This time its not going to conflict with your code too much. Most of the classes that where in IQToolkit.Data have been demoted into the namespace IQToolkit.Data.Common. This includes most all classes that are implementation detail or base classes. Mapping attributes and the like are now in IQToolkit.Data.Mapping. This makes the namespace clean and obvious when you start looking for things via intellisense. DbEntityProvider and DbEntitySession are the only classes sitting in IQToolkit.Data, as these are the ones you'll likely need to reference when writing code. IEntityProvider and IEntitySession are in IQToolkit namespace, because they are not specific to ADO (System.Data classes).
I hope you find this version feature rich enough to either build application directly on top of it, or model your own provider or data layer by using these techniques.
Don't forget the audio tapes.