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

It's worth noting that I haven't figured out the whole url schema with the "path/to/resource.format" (e.g. - '/api/v1/people.json') notation in WCF. I'm not even sure if it can do it, but I'd like to explore making that happen.

Just wanted to submit this for conversation before I really start digging.

@jonedmiston
Copy link

So how would one use the person list call? Would you be able to query one it? Sorry this is probably a dumb question...

@JasonOffutt
Copy link
Author

Not a dumb question at all.  :)

You'd call it like this to get a list:

For JSON: https://sparkdev.org/api/v1/people.json
For XML: https://sparkdev.org/api/v1/people.xml

We could default to one or the other if there's not an extension present.

To get a single person you'd do something like this:

https://sparkdev.org/api/v1/people/1.json

I'd like to support other formats rather than plain old XML. It'd be cool to be able to do .atom or .rss to get those formats as well. This is one of the cool ideas I picked up from Rails that I think could make the Rock API stand out. 

I don't have all the details worked out on the best way to implement this, and I'm not sure the best way to do this in WCF yet. Just brainstorming and painting the ideal picture as I see it in my head. But it's something I'll be working on over the next week or two to show our API some love.

What do you think?

@jonedmiston
Copy link

Me like... so https://sparkdev.org/api/v1/people.json would return all people in the DB? What if I only wanted people with the last name of 'Offutt'? Would that be a new method?

@JasonOffutt
Copy link
Author

Exactly. I was thinking filtering based on fields would use the query string, so filtering by last name might look like:

https://sparkdev.org/api/v1/people.json?lastname=Offutt

I'd like to revisit the standards that MS set with oData as well, because you could do some pretty crazy filtering with their API by appending meta-queries with a dollar sign. Ultimately, though, I want to make the API pattern predictable and consistent (in addition to being fully RESTful).

I'd also like to explore oauth as the primary means to gain access to the API, but I want to get this first round of issues figured out first. So if you guys like it, I'll bury my nose in blogs on SOA and WCF. :)

@azturner
Copy link

This doesn't seem too far off from what we've implemented already. I do like the idea of having the implementation of each object type in it's own class. They are all in one now because I didn't want to have to add a ServiceRoute for each one (in the Global.asax.cs) file, but perhaps we can get around that using partial classes.

Currently the url's work like this:
http://localhost:63311/RockWeb/rest.svc/page/2 (returns JSON)
http://localhost:63311/RockWeb/rest.svc/page/2/xml (returns XML)

We can easily replace the "rest.svc" with "api/v1" by updating the ServiceRoute in the Global.ascx.cs file. We can also use the dot notation if we'd like by updating the UriTemplate settings in the RestService.svc.cs file from "page/{id}/xml" to "page/{id}.xml"

The current implementation has support for Get, Update, Create and Delete, but it doesn't have the List functionality.

The next steps seem like they should be

  1. See if the RestService.svc.cs file can be broken into multiple partial classes
  2. Add List functionality (with the filtering capability you mentioned)
  3. Look at other authorization (OAuth or OAuth2) mechanisms. That would be good for third party access, but we would still want the current mechanism for our intenanal use also.

What do you think?

@JasonOffutt
Copy link
Author

Sounds good. I was looking for a way to have a single method that dynamically returns the format that gets passed in (this is how Rails does it, though Rails kind of just "knows" how to do it), simply to keep things DRY, but the more I read about WCF, it seems that it's not quite possible yet. So I think the approach you're using in the existing service implementation would be our best bt so far. Just use separate overloads for each supported data format, and call a single private method to actually handle the request.

I'll start there and keep refactoring as I go.

@JasonOffutt
Copy link
Author

So I've been digging around off and on today as ideas pop into my head and I've come across a couple interesting little nuggets of information:

  1. .NET 4.0 allows us to do what they call "file-less service activation". This basically means that we can completely ditch the RestService.svc file completely. We just need a little magical snippet of xml in the web.config (see linked blog post).
  2. With file-less activation, we can break the implementation out of the App_Code folder and into another more appropriate project.
  3. We can still use routing to point to the appropriate type, because routing doesn't require a file.
  4. I believe it is possible to achieve partial interfaces/classes to accomplish this goal.

This is all looking good so far. One thing that concerns me is versioning. As I'm out about on the interwebs, I see a lot of versioned web service API's. Nick and I were discussing it and thought it might not be a bad idea to start with versioning ours. However, if we're intentionally limiting ourselves to a single web project and a single external .dll, what should be the ideal approach to versioning a service API if we're to stick to the current project structure?

@jonedmiston
Copy link

Keep in mind how our pattern will allow 3rd party developers to add their services into the mix and possible extend or event override the current ones.

@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