Udostępnij za pośrednictwem


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...

 

 

Phone.Data.Entity.zip

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