After a great presentation by Jason Sheehan at MDC about RestSharp, I decided to implement it.
RestSharp is a .Net framework for consuming restful data sources via either Json or XML.
My first step was to put together a Restful data source for RestSharp to consume. Staying entirely withing .Net, I decided to use Microsoft's oData implementation, built on System.Data.Services.DataServices. Natively, these support Json, or atom+pub xml. (XML with a few bells and whistles added on)
There are three main steps for creating an oData data source:
1) override CreateDSPMetaData
This is where the metadata data is returned. The meta data defines the structure of the data to return. The structure contains the relationships between data objects, along with what properties the objects expose. The meta data can and should be somehow cached so that the structure is not rebuild with every data request.
2) override CreateDataSource
The context contains the data the data source will publish. This method is the conduit which will populate the metadata objects to be returned to the requestor.
3) implement static InitializeService
At this point we can set up security, along with setting up properties of the web service (versioning, etc)
Here is a web service which publishes stock prices for various Products (stocks) in various Categories.
namespace RestService
{
public class RestServiceImpl : DSPDataService<DSPContext>
{
private static DSPContext _context;
private static DSPMetadata _metadata;
/// <summary>
/// Populate traversable data source
/// </summary>
/// <returns></returns>
protected override DSPContext CreateDataSource()
{
if (_context == null)
{
_context = new DSPContext();
Category utilities = new Category(0);
utilities.Name = "Electric";
Category financials = new Category(1);
financials.Name = "Financial";
IList products = _context.GetResourceSetEntities("Products");
Product electric = new Product(0, utilities);
electric.Name = "ABC Electric";
electric.Description = "Electric Utility";
electric.Price = 3.5;
products.Add(electric);
Product water = new Product(1, utilities);
water.Name = "XYZ Water";
water.Description = "Water Utility";
water.Price = 2.4;
products.Add(water);
Product banks = new Product(2, financials);
banks.Name = "FatCat Bank";
banks.Description = "A bank that's almost too big";
banks.Price = 19.9; // This will never get to the client
products.Add(banks);
IList categories = _context.GetResourceSetEntities("Categories");
categories.Add(utilities);
categories.Add(financials);
utilities.Products.Add(electric);
utilities.Products.Add(electric);
financials.Products.Add(banks);
}
return _context;
}
/// <summary>
/// Setup rules describing published data structure - relationships between data,
/// key field, other searchable fields, etc.
/// </summary>
/// <returns></returns>
protected override DSPMetadata CreateDSPMetadata()
{
if (_metadata == null)
{
_metadata = new DSPMetadata("DemoService", "DataServiceProviderDemo");
// Define entity type product
ResourceType product = _metadata.AddEntityType(typeof(Product), "Product");
_metadata.AddKeyProperty(product, "ProductID");
// Only add properties we wish to share with end users
_metadata.AddPrimitiveProperty(product, "Name");
_metadata.AddPrimitiveProperty(product, "Description");
EntityPropertyMappingAttribute att = new EntityPropertyMappingAttribute("Name",
SyndicationItemProperty.Title, SyndicationTextContentKind.Plaintext, true);
product.AddEntityPropertyMappingAttribute(att);
att = new EntityPropertyMappingAttribute("Description",
SyndicationItemProperty.Summary, SyndicationTextContentKind.Plaintext, true);
product.AddEntityPropertyMappingAttribute(att);
// Define products as a set of product entities
ResourceSet products = _metadata.AddResourceSet("Products", product);
// Define entity type category
ResourceType category = _metadata.AddEntityType(typeof(Category), "Category");
_metadata.AddKeyProperty(category, "CategoryID");
_metadata.AddPrimitiveProperty(category, "Name");
_metadata.AddPrimitiveProperty(category, "Description");
// Define categories as a set of category entities
ResourceSet categories = _metadata.AddResourceSet("Categories", category);
att = new EntityPropertyMappingAttribute("Name",
SyndicationItemProperty.Title, SyndicationTextContentKind.Plaintext, true);
category.AddEntityPropertyMappingAttribute(att);
att = new EntityPropertyMappingAttribute("Description",
SyndicationItemProperty.Summary, SyndicationTextContentKind.Plaintext, true);
category.AddEntityPropertyMappingAttribute(att);
// A product has a category, a category has products
_metadata.AddResourceReferenceProperty(product, "Category", categories);
_metadata.AddResourceSetReferenceProperty(category, "Products", products);
}
return _metadata;
}
/// <summary>
/// Based on the requesting user, can set up permissions to Read, Write, etc.
/// </summary>
/// <param name="config"></param>
public static void InitializeService(DataServiceConfiguration config)
{
config.SetEntitySetAccessRule("*", EntitySetRights.All);
config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
config.DataServiceBehavior.AcceptProjectionRequests = true;
}
}
}
The objects prefixed with DSP come from the samples on the oData site: http://www.odata.org/developers
The products and categories objects are POCO business objects with no special modifiers.
Three main options are available for defining the MetaData of data sources in .Net:
1) Generate Entity Data model (Potentially directly from SQL Server database). This requires the least amount of manual interaction, and uses the edmx WYSIWYG editor to generate a data model. This can be directly tied to the SQL Server database and generated from the database if you want a data access layer tightly coupled with your database.
2) Object model decorations. If you already have a POCO data layer, you can decorate your objects with properties to statically inform the compiler how the objects are related. The disadvantage is there are now tags strewn about your business layer that need to be updated as the business rules change.
3) Programmatically construct metadata object. This is the object illustrated above in CreateDSPMetaData. This puts all relationship information into one central programmatic location. Here business rules are constructed when the DSPMetaData response object is returned.
Once you have your service up and running, RestSharp is designed for XML / Json, along with the native Microsoft library. There are currently some differences between how Jason made RestSharp expect XML with how atom+pub works, so I found better results currently with the Json implementation - modifying the RestSharp XML parser to make an atom+pub parser is fairly trivial though, so use what implementation works best for you.
I put together a sample console app which calls the RestSvcImpl.svc service defined above (and assumes it to be running on port 2000). I used both RestSharp as a client, and also the default Microsoft oData client tools.
namespace RestConsole
{
class Program
{
private static DataServiceContext _ctx;
private enum DemoType
{
Xml,
Json
}
static void Main(string[] args)
{
// Microsoft implementation
_ctx = new DataServiceContext(new System.Uri("http://localhost:2000/RestServiceImpl.svc"));
var msProducts = RunQuery<Product>("Products").ToList();
var msCategory = RunQuery<Category>("/Products(0)/Category").AsEnumerable().Single();
var msFilteredProducts = RunQuery<Product>("/Products?$filter=length(Name) ge 4").ToList();
// RestSharp implementation
DemoType demoType = DemoType.Json;
var client = new RestClient("http://localhost:2000/RestServiceImpl.svc");
client.ClearHandlers(); // Remove all available handlers
// Set up handler depending on what situation dictates
if (demoType == DemoType.Json)
client.AddHandler("application/json", new RestSharp.Deserializers.JsonDeserializer());
else if (demoType == DemoType.Xml)
{
client.AddHandler("application/atom+xml", new RestSharp.Deserializers.XmlDeserializer());
}
var request = new RestRequest();
if (demoType == DemoType.Json)
request.RootElement = "d"; // service root element for json
else if (demoType == DemoType.Xml)
{
request.XmlNamespace = "http://www.w3.org/2005/Atom";
}
// Return all products
request.Resource = "/Products?$orderby=Name";
RestResponse<List<Product>> productsResp = client.Execute<List<Product>>(request);
List<Product> products = productsResp.Data;
// Find category for product with ProductID = 1
request.Resource = string.Format("/Products(1)/Category");
RestResponse<Category> categoryResp = client.Execute<Category>(request);
Category category = categoryResp.Data;
// Specialized queries
request.Resource = string.Format("/Products?$filter=ProductID eq {0}", 1);
RestResponse<Product> productResp = client.Execute<Product>(request);
Product product = productResp.Data;
request.Resource = string.Format("/Products?$filter=Name eq '{0}'", "XYZ Water");
productResp = client.Execute<Product>(request);
product = productResp.Data;
}
private static IEnumerable<TElement> RunQuery<TElement>(string queryUri)
{
try
{
return _ctx.Execute<TElement>(new Uri(queryUri, UriKind.Relative));
}
catch (Exception ex)
{
throw ex;
}
}
}
}
Feel free to step through the code a few times and to attach a debugger to the service as well to see how and where the context and metadata objects are constructed and returned. Pay special attention to the response object being returned by the oData service - There are several properties of the RestRequest that can be used to help troubleshoot when the structure of the response is not exactly what would be expected.