Creating LINQ Data Provider for WP7 (Part 1)
As you all should be aware by now, the first release SL for the WP7 is not going to contain any structured data storage (SQL CE) functionality or LINQ data providers. However this fact should not preclude us from rolling out our own version. After all the LINQ to Objects is still supported and all what we need to come up with at this point is the ability to persist objects (or entities) to device's storage and then read from it. So let's set requirements for the first version of the LINQ data provider that should have the ability to:
- Persist data sets of objects in the device's storage by serializing its content.
- Deserialize data sets from the device's storage without loading them all into device's memory.
- Use the LINQ to query the data sets for required data.
So accordingly to our requirements we need ability to serialize/deserialize objects. The current set assemblies in the WP7 includes 3 type of serializers: XmlSerializer, DataContractSerializer and DataContractJsonSerializer. The most interesitng one at this point is the DataContractJsonSerializer which has an advantage over the XmlSerializer by outputing a more compact data. In order to use the DataContractJsonSerializer in your project you need to add references to a couple of assemblies: System.ServiceModel.Web and System.Runtime.Serialization. First we are going to create JsonDataReaderWriter class which is going to have responsibility to read and write object into a stream. To make things more flexible in the future let's declare the following interface:
public interface IDataReaderWriter<T>
{
void MoveFirst();
bool Read();
T ReadObject();
T ReadObject(long position);
long WriteObject(T instance);
}
And now the JsonDataReaderWriter class that implements this interface:
public class JsonDataReaderWriter<T> : IDataReaderWriter<T>
{
private BinaryReader reader;
private BinaryWriter writer;
private Stream stream;
private Type type;
private DataContractJsonSerializer serializer;
public JsonDataReaderWriter(Stream stream)
{
if (stream != null)
{
if (stream.CanWrite)
{
this.writer = new BinaryWriter(stream);
}
if (stream.CanRead)
{
this.reader = new BinaryReader(stream);
}
}
this.stream = stream;
this.type = typeof(T);
this.serializer = new DataContractJsonSerializer(this.type);
}
#region IFileDataReaderWriter<T> Members
public bool Read()
{
return this.stream.Position < this.stream.Length;
}
public T ReadObject()
{
object instance = this.Deserialize();
if (instance != null)
{
return (T)instance;
}
return default(T);
}
public T ReadObject(long position)
{
this.stream.Position = position;
T instance = this.Deserialize();
if (instance != null)
{
return (T)instance;
}
return default(T);
}
public long WriteObject(T instance)
{
long position = stream.Position;
this.Serialize(instance);
return position;
}
public void MoveFirst()
{
this.stream.Position = 0;
}
#endregion
#region helper methods
private void Serialize(T obj)
{
using (MemoryStream ms = new MemoryStream())
{
serializer.WriteObject(ms, obj);
byte[] data = ms.ToArray();
string retVal = Encoding.UTF8.GetString(data, 0, data.Length);
this.writer.Write(retVal);
}
}
private T Deserialize()
{
object obj = null;
string result = this.reader.ReadString();
using (MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(result)))
{
obj = (T)serializer.ReadObject(ms);
}
return (T)obj;
}
#endregion
}
As you can see from the code above the JsonDataReaderWriter has a pretty straightforward implementation. It creates instances of the BinaryReader and BinaryWriter from the Stream and then making calls to the ReadObject and WriteObjects on the JsonDataReaderWriter.
Now we should be ready to take careof the first requirement - "Persist data sets of objects in the device's storage by serializing its content." In order to do that we are going to create the following ObjectStore class:
public class ObjectStore
{
private string storeName;
public ObjectStore(string storeName)
{
this.storeName = storeName;
}
public void Persist<T>(IEnumerable<T> entities)
{
// Initialize isolated storage
IsolatedStorageFile isf =
IsolatedStorageFile.GetUserStoreForApplication();
// Create folder for the store
isf.CreateDirectory(storeName);
// Prepare data reader for the entity
string fileName = String.Format("{0}\\{1}.jdf", storeName,
typeof(T).Name);
IsolatedStorageFileStream fs = isf.OpenFile(fileName,
System.IO.FileMode.Create, System.IO.FileAccess.ReadWrite);
// Create an instance of the data reader
JsonDataReaderWriter<T> fileDataReader = new
JsonDataReaderWriter<T>(fs);
// Enumerate through entities and write them into the file
foreach (T entity in entities)
{
// Write entity
fileDataReader.WriteObject(entity);
}
// Clean up
fs.Flush();
fs.Close();
}
}
In the ObjectStore class we are making use of Isolated storage functionality to create a folder with the name corresponding to storage name and open or create a file for an entity. After that it's making a call to the WriteObject method of the JsonDataReaderWriter.
Next we are going to take care of creating a ObjectReader class that implements IEnumerable interface which is required in order to take advantage of all LINQ to Objects functionality:
public class ObjectReader<T> : IEnumerable<T>, IEnumerable
{
protected IDataReaderWriter<T> reader;
protected string commandText;
public ObjectReader(IDataReaderWriter<T> reader)
{
this.reader = reader;
}
#region IEnumerable<T> Members
public IEnumerator<T> GetEnumerator()
{
while (this.reader.Read())
{
T entity = reader.ReadObject();
yield return entity;
}
}
#endregion
#region IEnumerable Members
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
#endregion
}
The ObjectReader looks straightforward as well. All what it's doing is making a call to the ReadObject() of the data reader that we already created and "yields" the result into the GetEnumerator call.
Well... at this point we layed out a good foundation to be able to use them in the client application. To keep things constistent and more usable let's add a few more classes - ObjectQuery and ObjectContext:
public class ObjectQuery<T> : ObjectReader<T>
{
#region constructors
public ObjectQuery(IDataReaderWriter<T> reader)
: base(reader)
{
}
#endregion
}
The ObjectQuery is a simple class that is derived from the ObjectReader.
public class ObjectContext : IDisposable
{
internal Database database;
private IsolatedStorageFile storage;
internal string storeName;
public ObjectContext(string storeName)
{
this.storage = IsolatedStorageFile.GetUserStoreForApplication();
this.storeName = storeName;
this.database = new Database(storage, storeName);
}
public ObjectContext(IsolatedStorageFile storage, string storeName)
{
this.storage = storage;
this.database = new Database(storage, storeName);
}
public ObjectQuery<T> CreateQuery<T>()
{
ObjectQuery<T> objectQuery = new
ObjectQuery<T>(this.database.GetReader<T>());
return objectQuery;
}
#region IDisposable Members
public void Dispose()
{
throw new NotImplementedException();
}
#endregion
}
The ObjectContext as you can see have just a single method CreateQuery that creates and instance of the ObjectQuery class and returns it to the caller. There are a few other classes that ObjectContext references like Database and Connection which you can find in the project attached to this post. So how would you use these data provider classes? The usage should be pretty obvious. This is how you can persist data sets:
// Create an instance of the ObjectStore
ObjectStore northwindStore = new ObjectStore("Northwind");
// Persist customers
northwindStore.Persist<Customer>(this.GetCustomers());
// Persist Orders
northwindStore.Persist<Order>(this.GetOrders());
Where GetCustomers() and GetOrders() would be the methods that retrieve a list of customers or orders from a web service. And then you just use a reqular LINQ to query the results:
ObjectContext context = new ObjectContext("Northwind");
ObjectQuery<Customer> query = context.CreateQuery<Customer>();
var result = from c in query
where c.City == "London"
&& c.ContactTitle == "Sales Representative"
select c;
foreach (Customer customer in result)
{
Debug.WriteLine(customer.ContactName);
}
I believe that at this point we have implemented all requerements that I have come up with in the begining of the post.
You can download the sample WP7 application that utilizes the data provider. I'll leave an excercise of finding out how it works for you. Just a few notes about it. This sample makes use of the Northwind OData web service that is located at https://service.odata.org/Northwind/Northwind.svc. It retrieves the data from this service and uses the data provider to persist it to the storage and display it in the list. Keep in mind that the sample is still a work in progress.
Enjoy...
Comments
Anonymous
September 08, 2010
Thank you for your sample. There is a bug in JsonDataReaderWriter class. It does not close stream after it's done reading file, so you can not do another file operation on the same file.... Here is a fix: public bool Read() { bool canRead = this.stream.Position < this.stream.Length; if (!canRead) { this.stream.Close(); } return canRead; }Anonymous
September 08, 2010
One more fix for serialization. Use Newtonsoft.Json; works really well. private void Serialize(T obj) { string retVal = JsonConvert.SerializeObject(obj); this.writer.Write(retVal); } private T Deserialize() { string result = this.reader.ReadString(); return (T)JsonConvert.DeserializeObject(result, typeof(T)); }Anonymous
February 24, 2011
Please, I wanted to run the code file (Phone.Data.Entity.zip) but he would not run, many errors of missing references, I have version wp7 RTW, please it is the full version of wp7 thank you