Skip to content

Instantly share code, notes, and snippets.

@JasonOffutt
Created October 11, 2011 23:02
Show Gist options
  • Save JasonOffutt/1279738 to your computer and use it in GitHub Desktop.
Save JasonOffutt/1279738 to your computer and use it in GitHub Desktop.
WCF REST Service API proposal
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");
}
}
}
@JasonOffutt
Copy link
Author

10-4

I've actually got an interesting pattern in mind that should allow us to register services in the API with very few lines of code. Though it might take a little hacking at some of the core stuff to make it work generically (shouldn't include any breaking changes, though). I had an epiphany on my way home from work. The kid's asleep now. Time to start hacking. :)

I'll post some more code samples when I have a proof of concept.

@JasonOffutt
Copy link
Author

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 :)

@JasonOffutt
Copy link
Author

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. :)

@azturner
Copy link

MEF sounds like it might solve the requirement Jon mentioned (making it easily extensible). Looking forward to seeing what you've come up with.

@JasonOffutt
Copy link
Author

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.

@JasonOffutt
Copy link
Author

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.

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