-
-
Save JasonOffutt/1279738 to your computer and use it in GitHub Desktop.
using System.Collections.Generic; | |
using System.ServiceModel; | |
using System.ServiceModel.Web; | |
using Rock.Models.Crm; | |
[ServiceContract] | |
public interface IPersonService | |
{ | |
/// <summary> | |
/// 'GET' /api/v1/people.json | |
/// </summary> | |
/// <returns>A list of people</returns> | |
[OperationContract] | |
[WebInvoke(Method = "GET", UriTemplate = "v1/people.{format}")] | |
IEnumerable<Person> List(); | |
/// <summary> | |
/// 'GET' /api/v1/people/1.json | |
/// </summary> | |
/// <param name="id"></param> | |
/// <returns>A person</returns> | |
[OperationContract] | |
[WebInvoke(Method = "GET", UriTemplate = "v1/people/{id}.{format}")] | |
Person Show(int id = -1); | |
/// <summary> | |
/// 'POST' /api/v1/people | |
/// </summary> | |
/// <param name="person">Hash of the person to be created</param> | |
/// <returns>True of false based on successful person creation</returns> | |
[OperationContract] | |
[WebInvoke(Method = "POST", UriTemplate = "v1/people.{format}")] | |
bool Create(Person person); | |
/// <summary> | |
/// 'PUT' /api/v1/people/1 | |
/// </summary> | |
/// <param name="person">Hash of the person to update</param> | |
/// <param name="id">ID of the person to update</param> | |
/// <returns>True or false based on successful person update</returns> | |
[OperationContract] | |
[WebInvoke(Method = "PUT", UriTemplate = "v1/people/{id}.{format}")] | |
bool Update(Person person, int id = -1); | |
/// <summary> | |
/// 'DELETE' /api/v1/people/1 | |
/// </summary> | |
/// <param name="id">ID of the person to delete</param> | |
/// <returns>True or false based on successful person delete</returns> | |
[OperationContract] | |
[WebInvoke(Method = "DELETE", UriTemplate = "v1/people/{id}.{format}")] | |
bool Destroy(int id = -1); | |
} |
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Net; | |
using System.ServiceModel; | |
using System.ServiceModel.Web; | |
using System.Web.Security; | |
using Rock.Helpers; | |
using Rock.Models.Crm; | |
public class PersonService : IPersonService | |
{ | |
private readonly bool isAuthenticated; | |
private readonly MembershipUser currentUser; | |
public PersonService() | |
{ | |
currentUser = Membership.GetUser(); | |
isAuthenticated = currentUser != null; | |
} | |
public IEnumerable<Person> List() | |
{ | |
VerifyAuthentication(); | |
using (var uow = new UnitOfWorkScope()) | |
{ | |
uow.objectContext.Configuration.ProxyCreationEnabled = false; | |
var personService = new Rock.Services.Crm.PersonService(); | |
return personService.Queryable().Where(p => p.Authorized("View", currentUser)); | |
} | |
} | |
public Person Show(int id = -1) | |
{ | |
VerifyAuthentication(); | |
using (var uow = new UnitOfWorkScope()) | |
{ | |
uow.objectContext.Configuration.ProxyCreationEnabled = false; | |
var personService = new Rock.Services.Crm.PersonService(); | |
var person = personService.GetPerson(id); | |
if (person.Authorized("View", currentUser)) | |
{ | |
return person; | |
} | |
throw new FaultException("Unauthorized"); | |
} | |
} | |
public bool Create(Person person) | |
{ | |
VerifyAuthentication(); | |
using (var uow = new UnitOfWorkScope()) | |
{ | |
var ctx = WebOperationContext.Current; | |
try | |
{ | |
uow.objectContext.Configuration.ProxyCreationEnabled = false; | |
var personService = new Rock.Services.Crm.PersonService(); | |
personService.AttachPerson(person); | |
personService.Save(person, (int)currentUser.ProviderUserKey); | |
return true; | |
} | |
catch | |
{ | |
ctx.OutgoingResponse.StatusCode = HttpStatusCode.InternalServerError; | |
return false; | |
} | |
} | |
} | |
public bool Update(Person person, int id = -1) | |
{ | |
VerifyAuthentication(); | |
using (var uow = new UnitOfWorkScope()) | |
{ | |
var ctx = WebOperationContext.Current; | |
try | |
{ | |
uow.objectContext.Configuration.ProxyCreationEnabled = false; | |
var personService = new Rock.Services.Crm.PersonService(); | |
var existingPerson = personService.GetPerson(id); | |
if (existingPerson == null) | |
{ | |
ctx.OutgoingResponse.StatusCode = HttpStatusCode.NotFound; | |
return false; | |
} | |
if (existingPerson.Authorized("Edit", currentUser)) | |
{ | |
uow.objectContext.Entry(existingPerson).CurrentValues.SetValues(person); | |
personService.Save(existingPerson, (int)currentUser.ProviderUserKey); | |
} | |
else | |
{ | |
ctx.OutgoingResponse.StatusCode = HttpStatusCode.Forbidden; | |
return false; | |
} | |
return true; | |
} | |
catch (Exception) | |
{ | |
ctx.OutgoingResponse.StatusCode = HttpStatusCode.InternalServerError; | |
return false; | |
} | |
} | |
} | |
public bool Destroy(int id = -1) | |
{ | |
VerifyAuthentication(); | |
using (var uow = new UnitOfWorkScope()) | |
{ | |
var ctx = WebOperationContext.Current; | |
try | |
{ | |
uow.objectContext.Configuration.ProxyCreationEnabled = false; | |
var personService = new Rock.Services.Crm.PersonService(); | |
var person = personService.GetPerson(id); | |
if (person == null) | |
{ | |
ctx.OutgoingResponse.StatusCode = HttpStatusCode.NotFound; | |
return false; | |
} | |
if (person.Authorized("Edit", currentUser)) | |
{ | |
personService.DeletePerson(person); | |
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"); | |
} | |
} | |
} |
OK, the MEF idea builds, but I'm going to write some tests to make sure my crazy hair-brained idea is going to work. 2:00 AM is too late to code. :)
MEF sounds like it might solve the requirement Jon mentioned (making it easily extensible). Looking forward to seeing what you've come up with.
Yeah, I think the MEF technique is really solid. I think it will deliver the desired effect of a more extensible and easy to maintain API. It's still not quite fully baked yet though.
There's some pretty hard core functionality composition happening. And it actually builds. I'm using a lot of generics, though, to keep my solution as DRY as possible. The downside is that in the implementation, I'm having to cast to ISecured often to check security (that won't effect the community dev who's looking to append their own service, however, as long as their custom entities implement ISecured).
Today, I'm going to write some tests to double check my assertions. Hopefully I'll have something for you guys to look at soon.
OK, update time.
I've hit a bit of a snag. It seems WCF doesn't offer a very good means of dynamically setting up endpoints. You still have to enter each service individually into the web.config. I'll post another Gist with what I've got so far, so you guys can see what I've been doing.
After implementing my ideas in code, there are definitely some usability concerns with the current single-document service design. So having multiple partial class/interface files across the implementation, but it starts to get pretty confusing as the code base grows. I think I've got a nice, extensible pattern so far, but I want to let it bake a little bit longer before I post it up.
What do you guys think about registering multiple classes within the service API? In order to keep the global.asax clean, we can use MEF to compose our service routes at runtime. This will eliminate the need to maintain a huge list of routes... (going to go try this right now :)