Created
October 17, 2011 15:42
-
-
Save JasonOffutt/1292898 to your computer and use it in GitHub Desktop.
First attempt at dynamic web service definition
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() ) ); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | |
} | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- ... --> | |
<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> | |
<!-- ... --> |
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.
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.
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
There's some pretty hard core composition awesomeness happening here, but I've got a few concerns about this solution:
<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.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.