Skip to content

Instantly share code, notes, and snippets.

@JasonOffutt
Created October 17, 2011 15:42
Show Gist options
  • Save JasonOffutt/1292898 to your computer and use it in GitHub Desktop.
Save JasonOffutt/1292898 to your computer and use it in GitHub Desktop.
First attempt at dynamic web service definition
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition.Hosting;
using System.Data.Services;
using System.Linq;
using System.ServiceModel.Activation;
using System.Web;
using System.Web.Routing;
using System.Web.Security;
using System.Web.SessionState;
using Rock.Cms;
using Rock.Cms.Security;
using Rock.Framework.ServiceApi.v1;
using Rock.Models.Cms;
using Rock.Services.Cms;
namespace RockWeb
{
public class Global : System.Web.HttpApplication
{
// ...
private void RegisterRoutes( RouteCollection routes )
{
PageRouteService pageRouteService = new PageRouteService();
// find each page that has defined a custom routes.
foreach ( PageRoute pageRoute in pageRouteService.Queryable())
{
// Create the custom route and save the page id in the DataTokens collection
Route route = new Route( pageRoute.Route, new RockRouteHandler() );
route.DataTokens = new RouteValueDictionary();
route.DataTokens.Add( "PageId", pageRoute.PageId.ToString() );
route.DataTokens.Add( "RouteId", pageRoute.Id.ToString() );
routes.Add( route );
}
// Use MEF to dynamically compose all REST service classes and add routes to each based on type
AggregateCatalog catalog = new AggregateCatalog();
// Not sure if this will actually work. Since all .NET objects inherit from object, technically
// it should work, but we might not get the expected behavior. I'm also a little concerned that
// if we just passed in IRestService, the service contract might not get passed through.
catalog.Catalogs.Add(new AssemblyCatalog(typeof(IRestService<object>).Assembly));
CompositionContainer container = new CompositionContainer(catalog);
// TODO: Add logic to prevent type collisions. Filter out duplicates by Priority.
// Filtering by priority should make the service layer extensible, as devs will be able
// to override default functionality simply by exporting an existing type.
var services = container.GetExportedValues<IRestService>();
var factory = new WebServiceHostFactory();
foreach (var service in services)
{
routes.Add(new ServiceRoute(service.RoutePrefix, factory, service.Type));
}
//var factory = new WebServiceHostFactory();
//routes.Add(new ServiceRoute("Rest.svc", factory, typeof (WCF.RestService)));
// Add a default page route
routes.Add( new Route( "page/{PageId}", new RockRouteHandler() ) );
// Add a default route for when no parameters are passed
routes.Add( new Route( "", new RockRouteHandler() ) );
}
}
}
using System;
using System.Linq;
namespace Rock.Framework.Services
{
/// <summary>
/// Generic service business class for all services to implement.
/// This will allow the framework to treat all services polymorphically.
/// </summary>
/// <typeparam name="TEntity">Generic entity type</typeparam>
public interface IEntityService<TEntity>
{
IQueryable<TEntity> Queryable();
TEntity GetByID(int id);
TEntity GetByGuid(Guid guid);
void Attach(TEntity entity);
void Add(TEntity entity);
void Delete(TEntity entity);
void Save(TEntity entity, int? id);
}
}
using System.Collections.Generic;
using System.ServiceModel.Web;
using Rock.Models.Crm;
namespace Rock.Framework.ServiceApi.v1
{
/// <summary>
/// Declaration of service methods and associated url routes to respond to.
/// </summary>
public interface IPeopleService :IRestService<Person>
{
[WebInvoke(Method = "GET", UriTemplate = "people", ResponseFormat = WebMessageFormat.Json)]
[WebInvoke(Method = "GET", UriTemplate = "people.json", ResponseFormat = WebMessageFormat.Json)]
IEnumerable<Person> ListJson();
[WebInvoke(Method = "GET", UriTemplate = "people.xml", ResponseFormat = WebMessageFormat.Xml)]
IEnumerable<Person> ListXml();
[WebInvoke(Method = "GET", UriTemplate = "people/{id}", ResponseFormat = WebMessageFormat.Json)]
[WebInvoke(Method = "GET", UriTemplate = "people/{id}.json", ResponseFormat = WebMessageFormat.Json)]
Person ShowJson(int id);
[WebInvoke(Method = "GET", UriTemplate = "people/{id}.xml", ResponseFormat = WebMessageFormat.Xml)]
Person ShowXml(int id);
[WebInvoke(Method = "PUT", UriTemplate = "people/{id}", ResponseFormat = WebMessageFormat.Json)]
[WebInvoke(Method = "PUT", UriTemplate = "people/{id}.json", ResponseFormat = WebMessageFormat.Json)]
void UpdateJson(int id, Person person);
[WebInvoke(Method = "PUT", UriTemplate = "people/{id}.xml", ResponseFormat = WebMessageFormat.Xml)]
void UpdateXml(int id, Person person);
[WebInvoke(Method = "POST", UriTemplate = "people", ResponseFormat = WebMessageFormat.Json)]
[WebInvoke(Method = "POST", UriTemplate = "people.json", ResponseFormat = WebMessageFormat.Json)]
void CreateJson(Person person);
[WebInvoke(Method = "POST", UriTemplate = "people.xml", ResponseFormat = WebMessageFormat.Xml)]
void CreateXml(Person person);
[WebInvoke(Method = "DELETE", UriTemplate = "people/{id}", ResponseFormat = WebMessageFormat.Json)]
[WebInvoke(Method = "DELETE", UriTemplate = "people/{id}.json", ResponseFormat = WebMessageFormat.Json)]
void DestroyJson(int id);
[WebInvoke(Method = "DELETE", UriTemplate = "people/{id}.xml", ResponseFormat = WebMessageFormat.Xml)]
void DestroyXml(int id);
}
}
using System;
using System.Collections.Generic;
using System.ServiceModel;
using Rock.Framework.Helpers;
namespace Rock.Framework.ServiceApi.v1
{
/// <summary>
/// Base type that all Rock ChMS RESTful web services will inherit from.
/// Contains some general service information for dynamic composition.
/// </summary>
[ServiceContract]
public interface IRestService
{
Priority Priority { get; set; }
string RoutePrefix { get; set; }
Type Type { get; }
}
/// <summary>
/// Type-agnostic service interface to define service contract.
/// </summary>
/// <typeparam name="T">Generic Entity type</typeparam>
public interface IRestService<T> : IRestService
{
[OperationContract]
IEnumerable<T> ListJson();
[OperationContract]
IEnumerable<T> ListXml();
[OperationContract]
T ShowJson(int id);
[OperationContract]
T ShowXml(int id);
[OperationContract]
void UpdateJson(int id, T person);
[OperationContract]
void UpdateXml(int id, T person);
[OperationContract]
void CreateJson(T person);
[OperationContract]
void CreateXml(T person);
[OperationContract]
void DestroyJson(int id);
[OperationContract]
void DestroyXml(int id);
}
}
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using Rock.Framework.Helpers;
using Rock.Framework.Services.Crm;
using Rock.Models.Crm;
namespace Rock.Framework.ServiceApi.v1
{
/// <summary>
/// Implementation of WCF REST webservice
/// </summary>
[Export(typeof(IRestService))]
public class PeopleService : RestServiceBase, IPeopleService
{
public Priority Priority { get; set; }
public string RoutePrefix { get; set; }
public Type Type { get { return this.GetType(); } }
public PeopleService()
{
Priority = Priority.Low;
RoutePrefix = "people";
}
public IEnumerable<Person> ListJson()
{
return List(new TestPersonService());
}
public IEnumerable<Person> ListXml()
{
return List(new TestPersonService());
}
public Person ShowJson(int id)
{
return GetByID(new TestPersonService(), id);
}
public Person ShowXml(int id)
{
return GetByID(new TestPersonService(), id);
}
public void UpdateJson(int id, Person person)
{
Update(new TestPersonService(), person, id);
}
public void UpdateXml(int id, Person person)
{
Update(new TestPersonService(), person, id);
}
public void CreateJson(Person person)
{
Create(new TestPersonService(), person);
}
public void CreateXml(Person person)
{
Create(new TestPersonService(), person);
}
public void DestroyJson(int id)
{
Destroy(new TestPersonService(), id);
}
public void DestroyXml(int id)
{
Destroy(new TestPersonService(), id);
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Rock.Models.Core;
using Rock.Models.Crm;
using Rock.Repository.Crm;
using Rock.Services;
using Rock.Services.Core;
namespace Rock.Framework.Services.Crm
{
/// <summary>
/// Business object to handle abstraction of access to entities and repository layer.
/// </summary>
public class PersonService : Service, IEntityService<Person>
{
private readonly IPersonRepository _repository;
public TestPersonService() : this(new EntityPersonRepository())
{ }
public TestPersonService(IPersonRepository PersonRepository)
{
_repository = PersonRepository;
}
public IQueryable<Person> Queryable()
{
return _repository.AsQueryable();
}
public Person GetByID(int id)
{
return _repository.FirstOrDefault(p => p.Id == id);
}
public Person GetByGuid(Guid guid)
{
return _repository.FirstOrDefault(p => p.Guid == guid);
}
public void Attach(Person entity)
{
_repository.Attach(entity);
}
public void Add(Person entity)
{
_repository.Add(entity);
}
public void Delete(Person entity)
{
_repository.Delete(entity);
}
public void Save(Person entity, int? id)
{
List<EntityChange> entityChanges = _repository.Save(entity, id);
if (entityChanges != null)
{
EntityChangeService entityChangeService = new EntityChangeService();
foreach (EntityChange entityChange in entityChanges)
{
entityChange.EntityId = entity.Id;
entityChangeService.AddEntityChange(entityChange);
entityChangeService.Save(entityChange, id);
}
}
}
}
}
using System;
using System.Linq;
using System.Net;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
using System.Web.Security;
using Rock.Cms.Security;
using Rock.Framework.Services;
using Rock.Helpers;
using Rock.Models;
namespace Rock.Framework.ServiceApi.v1
{
/// <summary>
/// Base class for all RESTful services. Includes base functionality for
/// delegating authorization and entity access to underlying service
/// business objects.
/// </summary>
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class RestServiceBase
{
private readonly bool isAuthenticated;
private readonly MembershipUser currentUser;
public RestServiceBase()
{
currentUser = Membership.GetUser();
isAuthenticated = currentUser != null;
}
protected IQueryable<T> List<T>(IEntityService<T> service)
{
VerifyAuthentication();
using (var uow = new UnitOfWorkScope())
{
uow.objectContext.Configuration.ProxyCreationEnabled = false;
return service.Queryable().Where(p => ((ISecured) p).Authorized("View", currentUser));
}
}
protected T GetByID<T>(IEntityService<T> service, int id)
{
VerifyAuthentication();
using (var uow = new UnitOfWorkScope())
{
uow.objectContext.Configuration.ProxyCreationEnabled = false;
var entity = service.GetByID(id);
if (((ISecured) entity).Authorized("View", currentUser))
{
return entity;
}
throw new FaultException("Unauthorized");
}
}
protected T GetByGuid<T>(IEntityService<T> service, Guid guid)
{
VerifyAuthentication();
using (var uow = new UnitOfWorkScope())
{
uow.objectContext.Configuration.ProxyCreationEnabled = false;
var entity = service.GetByGuid(guid);
if (((ISecured) entity).Authorized("View", currentUser))
{
return entity;
}
throw new FaultException("Unauthorized");
}
}
protected bool Create<T>(IEntityService<T> service, T entity)
{
VerifyAuthentication();
using (var uow = new UnitOfWorkScope())
{
var ctx = WebOperationContext.Current;
try
{
uow.objectContext.Configuration.ProxyCreationEnabled = false;
service.Attach(entity);
service.Save(entity, (int) currentUser.ProviderUserKey);
return true;
}
catch
{
ctx.OutgoingResponse.StatusCode = HttpStatusCode.InternalServerError;
return false;
}
}
}
protected bool Update<T>(IEntityService<T> service, T entity, int id)
{
VerifyAuthentication();
using (var uow = new UnitOfWorkScope())
{
var ctx = WebOperationContext.Current;
try
{
uow.objectContext.Configuration.ProxyCreationEnabled = false;
var existing = service.GetByID(id);
if (existing == null)
{
ctx.OutgoingResponse.StatusCode = HttpStatusCode.NotFound;
return false;
}
if (((ISecured) existing).Authorized("Edit", currentUser))
{
uow.objectContext.Entry(existing as Model).CurrentValues.SetValues(entity);
service.Save(existing, (int)currentUser.ProviderUserKey);
}
else
{
ctx.OutgoingResponse.StatusCode = HttpStatusCode.Forbidden;
return false;
}
return true;
}
catch (Exception)
{
ctx.OutgoingResponse.StatusCode = HttpStatusCode.InternalServerError;
return false;
}
}
}
protected bool Destroy<T>(IEntityService<T> service, int id)
{
VerifyAuthentication();
using (var uow = new UnitOfWorkScope())
{
var ctx = WebOperationContext.Current;
try
{
uow.objectContext.Configuration.ProxyCreationEnabled = false;
var entity = service.GetByID(id);
if (entity == null)
{
ctx.OutgoingResponse.StatusCode = HttpStatusCode.NotFound;
return false;
}
if (((ISecured) entity).Authorized("Edit", currentUser))
{
service.Delete(entity);
return true;
}
ctx.OutgoingResponse.StatusCode = HttpStatusCode.Forbidden;
return false;
}
catch (Exception)
{
ctx.OutgoingResponse.StatusCode = HttpStatusCode.InternalServerError;
return false;
}
}
}
private void VerifyAuthentication()
{
if (isAuthenticated)
{
// TODO: Consider returning a 401 (unauthenticated) or 403 (forbidden) rather than throwing an exception
throw new FaultException("Must be logged in");
}
}
}
}
<!-- ... -->
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="">
<serviceMetadata httpGetEnabled="true"/>
<serviceDebug includeExceptionDetailInFaults="false"/>
</behavior>
</serviceBehaviors>
</behaviors>
<!--<serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true"/>-->
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true">
<serviceActivations>
<!--
NOTE: This could be our stumbling block. Each web service endpoint would need to be defined here.
-->
<add factory="System.ServiceModel.Activation.ServiceHostFactory" relativeAddress="./RestService.svc" service="Rock.Framework.ServiceApi.RestServiceBase"/>
</serviceActivations>
</serviceHostingEnvironment>
</system.serviceModel>
<!-- ... -->
@JasonOffutt
Copy link
Author

There's some pretty hard core composition awesomeness happening here, but I've got a few concerns about this solution:

  1. WCF doesn't seem to be built for the problem we're trying to solve. It expects to know about all its services at compile time. Though it doesn't enforce that via the compiler, I think we're going to run into some issues with making the service API truly extensible. Check out the web.config. Under <serviceActivations>, we have to declare every service specifically. So if somebody wanted to include a custom one, they'd need to update the config file in order to register it.
  2. While it builds, I haven't had time to actually attempt testing it in the browser yet. My gut tells me it's probably not going to work the way I've been hoping. Even though we're doing dynamic composition with MEF to compose things dynamically, I'm a little concerned about having to declare a type when instantiating the catalog. And I'm a little concerned that padding IRestService<object> isn't going to yield the result I'm looking for.

So while this approach seems a little kludgy so far, if we can make it work, I think it'll be a great way to expose service hooks for the dev community to extend. I chose to implement a priority notion on IRestService to allow our default services to be overridden. I haven't written that logic yet to avoid type collisions (didn't want to get too ahead of myself). I'd like to see this working first. I figured it'd be best to get it out there for you guys to look at. Maybe a fresh pair of eyes could help unstick me. :)

Let me know if you have any questions, thoughts, suggestions.

@jonedmiston
Copy link

Not sure I like the idea of having to modify the web.config. That will be nearly impossible to support through upgrades as you won't know what's been added.

@JasonOffutt
Copy link
Author

I agree completely. I'm really not a fan of that idea either. I'll keep digging to see if I can come up with another solution. I have an alternate idea that should give us the extensibility that we're after, but I want to make sure I explore WCF thoroughly before I look elsewhere.

@JasonOffutt
Copy link
Author

I'm still going to play around with it this week/weekend. I've got an idea to use dynamics rather than generics for easier service composition at runtime. I just hope the web.config issue isn't going to be the silver bullet that kills this idea. I'll keep this updated with my progress.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment