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

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