I've been playing over Xmas with exposing arbitrary data from an ADO.NET Data Service and I wanted to try and write it up here although I don't have it 100% nailed down or correct at this point - still some things to figure out.
Note that a lot of this is already covered by Jonathan up here but I want to also look a little at read/write access to data in subsequent posts.
What I thought I'd do is to try and expose;
- relational data held in SQL Server and accessed with ADO.NET Entity Framework
- object data held in memory
using Data Services.
In this post I'll just get to the point of being able to read data and then perhaps in subsequent posts I can have a stab at insert, update, delete.
I'm going to deal with a very simple set of data containing entities Author and Book.
Relational Data with Entity Framework
I go ahead and create a couple of tables;
create table author
(
id int identity primary key not null,
firstName nvarchar(30) not null,
lastName nvarchar(30) not null
)
create table book
(
id int identity primary key not null,
authorId int foreign key references author(id) null,
title nvarchar(100) not null,
price money not null
)
insert author values('Charles', 'Dickens')
insert author values('Thomas', 'Hardy')
insert author values('Ernest', 'Hemingway')
insert book values(1, 'Great Expectations', 5.99)
insert book values(2, 'The Mayor of Casterbridge', 7.50)
insert book values(3, 'For Whom the Bell Tolls', 6.99)
and then create myself a new WebSite project (WebSite6), add into it a new "ADO.NET Entity Data Model" (I called it demoEntities) for my Author and Book tables. Then I add in a new "ADO.NET Data Service" (I called it EFService) and I change the code to read;
public class EFService : WebDataService<demoEntities>
{
public static void InitializeService(IWebDataServiceConfiguration config)
{
config.SetResourceContainerAccessRule("*", ResourceContainerRights.All);
}
}
and I'm done.
Object Data Held in Memory
I added to my WebSite6 project a couple of new classes;
public abstract class KeyedEntity
{
public KeyedEntity()
{
}
public KeyedEntity(int id)
{
this.ID = id;
}
[DataWebKey]
public int ID { get; set; }
} public class Author : KeyedEntity
{
public Author()
{
Books = new SynchronizedCollection<Book>();
}
public string FirstName { get; set; }
public string LastName { get; set; }
public SynchronizedCollection<Book> Books { get; set; }
} public class Book : KeyedEntity
{
public string Title { get; set; }
public decimal Price { get; set; }
// Imagine a book has 1 author
public Author Author { get; set; }
}
I then went and added a class that I called BookContext which looks like this - note that this is overkill at the moment because it tries to be thread-safe around its two collections Authors and Books by associating a ReaderWriterLock with each collection and copying collections when requested rather than just returning them. I use a little internal class, CollectionInfo, to bring together a collection and the lock protecting it and then store a Dictionary of those keyed on the type of the collection.
public class BookContext
{
static BookContext()
{
List<Author> authors = new List<Author>()
{
new Author() { ID = 1, FirstName = "Charles", LastName = "Dickens" },
new Author() { ID = 2, FirstName = "Thomas", LastName = "Hardy" },
new Author() { ID = 3, FirstName = "Ernest", LastName = "Hemingway" }
};
List<Book> books = new List<Book>()
{
new Book() { ID = 1, Title = "Great Expectations",
Author = authors[0], Price = 5.99m },
new Book() { ID = 2, Title = "The Mayor of Casterbridge",
Author = authors[1], Price = 7.50m },
new Book() { ID = 3, Title = "For Whom the Bell Tolls",
Author = authors[2], Price = 6.99m }
};
authors[0].Books = new SynchronizedCollection<Book>() { books[0] };
authors[1].Books = new SynchronizedCollection<Book>() { books[1] };
authors[2].Books = new SynchronizedCollection<Book>() { books[2] };
collections = new Dictionary<Type, CollectionInfo>();
collections[typeof(Book)] =
new CollectionInfo() { List = books, Lock = new ReaderWriterLock() };
collections[typeof(Author)] =
new CollectionInfo() { List = authors, Lock = new ReaderWriterLock() };
}
public IQueryable<Book> Books
{
get
{
return (GetCollection<Book>());
}
}
public IQueryable<Author> Authors
{
get
{
return (GetCollection<Author>());
}
}
private static IQueryable<T> GetCollection<T>()
{
CollectionInfo ci = collections[typeof(T)];
List<T> copy = null;
try
{
ci.Lock.AcquireReaderLock(-1);
copy = new List<T>(ci.List as List<T>);
}
finally
{
ci.Lock.ReleaseReaderLock();
}
return (copy.AsQueryable());
}
private class CollectionInfo
{
public ReaderWriterLock Lock { get; set; }
public object List { get; set; }
}
private static Dictionary<Type, CollectionInfo> collections;
}
With that in place, I can go and add myself a new "ADO.NET Data Service" (I called it MemoryService) to the project;
public class MemoryService : WebDataService<BookContext>
{
public static void InitializeService(IWebDataServiceConfiguration config)
{
config.SetResourceContainerAccessRule("*", ResourceContainerRights.All);
}
}
Querying the 2 Models
Having got my 2 Data Services in place I should be able to hit them with some queries. From a browser I can do stuff like;
http://localhost:32767/WebSite6/EFService.svc/author(2)/book
http://localhost:32767/WebSite6/MemoryService.svc/Authors(2)/Books
and, aside from one set of names ending up being pluralised, they both look pretty much the same. I could write AJAX code or .NET code against either of these as a client program using webdatagen.exe in order to create the client side pieces in the .NET case.
So, creating two lots of proxy code;
webdatagen.exe /uri:http://localhost:32767/WebSite6/EFService.svc /outobjectlayer:efservice.cs /mode:clientclassgeneration
webdatagen.exe /uri:http://localhost:32767/WebSite6/MemoryService.svc /outobjectlayer:memoryservice.cs /mode:clientclassgeneration
Now, as I found out here the second set of code-generation won't work 100% in that the proxy generated from the in memory service will not correctly reflect that Author has a collection of Book and that Book has a reference to its Author. So, I've manually hacked the proxy code to make those changes myself. I can then write code like;
// EF Service
demoEntities efService = new demoEntities("http://localhost:32767/WebSite6/EFService.svc");
efService.MergeOption = Microsoft.Data.WebClient.MergeOption.AppendOnly;
var efQuery = from a in efService.author
where a.firstName == "Charles"
select a;
foreach (author a in efQuery)
{
efService.LoadProperty(a, "book");
foreach (book b in a.book)
{
Console.WriteLine("Book {0}, {1}", b.title, b.price);
}
}
// Memory Service
BookContext memService = new BookContext("http://localhost:32767/WebSite6/MemoryService.svc");
memService.MergeOption = Microsoft.Data.WebClient.MergeOption.AppendOnly;
var memQuery = from a in memService.Authors
where a.FirstName == "Charles"
select a;
foreach (Author a in memQuery)
{
memService.LoadProperty(a, "Books");
foreach (Book b in a.Books)
{
Console.WriteLine("Book {0}, {1}", b.Title, b.Price);
}
}and query against the Entity Framework provided service and the in-memory service in pretty much the same way.