Jump to content

REST in Practice: How to Implement AtomPub in .NET

0
  chco's Photo
Posted Oct 25 2010 06:17 AM

This excerpt from REST in Practice walks you through the fulfillment service of a coffee shop called Restbucks to show how AtomPub can be implemented in .NET.
In this section, we show how Restbucks has implemented a simple version of the fulfillment service using Windows Communication Foundation (WCF). WCF provides a service hosting runtime that takes care of some of the low-level plumbing, allowing us to concentrate on the overall design of our service. Since we tend to work in a rapid, test-driven manner, we’ve decoupled our service implementation from the WCF runtime. This enables us to deliver functionality quickly by specifying and testing specific HTTP interactions without having to start and stop a service instance.

Because the fulfillment protocol is built on top of AtomPub, our solution needs to implement AtomPub collection and member protocol resources. We’ll start by describing how we’ve built these collections and members using some of the .NET Framework’s syndication classes. After that, we’ll look at how to add domain logic that instantiates these classes in line with the fulfillment protocol. Next, we’ll look at the simple Test Driven Development–centric framework we use to decouple our service implementation from the WCF runtime. Last, we’ll examine the commands that we use to handle requests.

Implementing AtomPub Collections and Members

The .NET Framework’s System.ServiceModel.Syndication namespace contains a number of classes that can be used to implement feeds and entries. For example, by adding a list of SyndicationItem objects to a SyndicationFeed and then outputting that feed using an Atom10FeedFormatter, we can easily generate an Atom feed.

AtomPub, as we’ve seen, adds several extension elements to Atom’s basic feed format. Our fulfillment service uses a member’s <app:edited> element and its <app:control> and <app:draft> extension elements to coordinate the fulfillment of coffee orders. In addition, the fulfillment collection itself exposes an <app:collection> element that advertises which media types a cashier can use to initiate the fulfillment process. To use these elements in our solution, we must extend the framework classes.

The .NET Framework allows SyndicationFeed and SyndicationItem classes to be subclassed. To illustrate how we can implement AtomPub extensions by subclassing syndication classes, we’ll look in detail at how we extend the SyndicationItem class to provide EditedDateTime and Draft properties corresponding to the <app:edited> and <app:draft> extension elements. Example 8-19 shows the implementation of our Member class.

Example 8-19. Subclassing SyndicationItem to implement AtomPub extension elements

public class Member : SyndicationItem
{
  private const string EditedElementName = "edited";
  private const string ControlElementName = "control";
  private const string DateTimeFormat = "yyyy-MM-ddTHH:mm:ssZ";

  private static readonly DataContractSerializer ControlSerializer =
    new DataContractSerializer(typeof (ControlExtension));

  private ControlExtension control;
  private DateTimeOffset? editedDateTime;
  public Member()
  {
    control = new ControlExtension {Draft = DraftStatus.No};
  }

  public DraftStatus Draft
  {
    get { return control.Draft; }
    set { control.Draft = value; }
  }

  public DateTimeOffset EditedDateTime
  {
    get
    {
      if (editedDateTime == null)
      {
        editedDateTime = LastUpdatedTime;
      }
      return editedDateTime.Value;
    }
    set { editedDateTime = value; }
  }

  protected override bool TryParseElement(XmlReader reader, string version)
  {
    if (reader.LocalName.Equals(ControlElementName) &&
      reader.NamespaceURI.Equals(Namespaces.AtomPub))
    {
      control = (ControlExtension) ControlSerializer.ReadObject(reader);
      return true;
    }
    if (reader.LocalName.Equals(EditedElementName) &&
      reader.NamespaceURI.Equals(Namespaces.AtomPub))
    {
      editedDateTime = reader.ReadElementContentAsDateTime();
      return true;
    }
    return base.TryParseElement(reader, version);
  }

  protected override void WriteElementExtensions(XmlWriter writer, string version)
  {
    writer.WriteStartElement(EditedElementName, Namespaces.AtomPub);
    writer.WriteValue(FormatDateTime(EditedDateTime));
    writer.WriteEndElement();
    if (control != null)
    {
      ControlSerializer.WriteObject(writer, control);
    }

    base.WriteElementExtensions(writer, version);
  }

  private static string FormatDateTime(DateTimeOffset dateTime)
  {
    return dateTime.ToUniversalTime().ToString(DateTimeFormat);
  }
}


Member has two properties, Draft and EditedDateTime, each of which is backed by a member variable. The first of these, Draft, is of type ControlExtension, which is a simple serializable class, as shown in Example 8-20.

Example 8-20. ControlExtension represents the app:control and app:draft elements

[DataContract(Name = "control", Namespace = Namespaces.AtomPub)]
public class ControlExtension
{
  [DataMember(Name = "draft")]
  private string draft;

  public DraftStatus Draft
  {
    get
    {
      if (string.IsNullOrEmpty(draft))
      {
        return DraftStatus.No;
      }
      return DraftStatus.Parse(draft);
    }
    set { draft = value.Value; }
  }
}


To ensure that these elements are serialized and deserialized correctly, we override SyndicationItem’s WriteElementExtensions(…) and TryParseElement(…) methods, respectively. When serializing a member, WriteElementExtensions(…) writes the <app:edited> value to the supplied XML writer directly, and then uses a static ControlSerializer to serialize a ControlExtension instance into <app:control> and <app:draft> elements. When deserializing a member from an XML document, TryParseElement(…) parses these same elements from the supplied XML reader and instantiates the corresponding .NET classes.


Note: Member is initialized with a draft status of DraftStatus.No. This is to cater to situations where the received XML representation of a member does not include <app:control> and <app:draft> elements. As per the AtomPub specification, a member without an <app:draft> element is assumed to be in a published (not draft) state.



Our Collection class extends the SyndicationFeed class in a similar manner to Member. Collection provides a CollectionExtension property that indicates which media types a client can use to initiate the fulfillment process.

Using Collections and Members for Order Fulfillment

The fulfillment service uses AtomPub members to represent instances of fulfillment (both outstanding and in-process), and AtomPub collections to represent lists of fulfillment instances. To ensure that collections and members are created with the correct property values according to our domain business rules, we create Fulfillment and FulfillmentCollection classes. Fulfillment encapsulates a single member. It controls access to this member according to our fulfillment business rules. FulfillmentCollection does the same for an AtomPub collection.

When a cashier submits a new order to the service, the service creates a new Fulfillment object. This fulfillment instance initializes a Member object with the Atom and AtomPub metadata used throughout the fulfillment process, as shown in Example 8-21.

Example 8-21. The Fulfillment constructor initializes a Member object with the necessary metadata

public Fulfillment(Guid id, DateTimeOffset createdDateTime,
  SyndicationContent content, Uri baseUri, string author)
{
  member = new Member
    {
      Title = SyndicationContent.CreatePlaintextContent("order"),
      Id = new UniqueId(id).ToString(),
      LastUpdatedTime = createdDateTime,
      EditedDateTime = createdDateTime,
      Draft = DraftStatus.Yes,
      Content = content
    };
  member.Authors.Add(new SyndicationPerson {Name = author});
  member.Links.Add(new EditLink(baseUri, id).ToSyndicationLink());
}


In a similar fashion, FulfillmentCollection initializes a Collection object with service-specific metadata. After it has been constructed, a fulfillment collection can be populated with AtomPub members by calling its Add(…) method and supplying a list of member objects. The implementation of Add(…) is shown in Example 8-22.

Example 8-22. FulfillmentCollection’s Add(…) method adds members to the underlying collection

public void Add(IEnumerable<Fulfillment> newMembers)
{
  foreach (Fulfillment member in newMembers)
  {
    member.DoAction(i => members.Add(i));
  }

  members.Sort((x, y) =>
    ((Member) y).EditedDateTime.CompareTo(
      ((Member) x).EditedDateTime));

  if (members.Count > 0)
  {
    collection.LastUpdatedTime = ((Member) members.First()).EditedDateTime;
  }
}


Add(…) can’t assume that the supplied members are in any particular order, so after it has added the new members to the existing list, it sorts the entire list. This ensures that the members are ordered correctly based on their app:edited values. Once the list has been sorted, the collection’s LastUpdatedTime property is set to the EditedDateTime of the first (i.e., the most recent) member.

To illustrate how Fulfillment implements some of our domain logic, let’s look at what happens when a fulfillment instance is updated. When a cashier or barista PUTs a revised member to the service, the corresponding fulfillment instance is retrieved from a repository and updated via its Edit(…) method, as shown in Example 8-23.

Example 8-23. Updating a fulfillment instance

public Fulfillment Edit(Member editedMember, DateTimeOffset editedDateTime)
{
  if (member.Draft.Equals(DraftStatus.No))
  {
    throw new InvalidOperationException("Fulfillment can no longer be edited.");
  }

  member.EditedDateTime = editedDateTime;
  member.Draft = editedMember.Draft;
  member.Content = editedMember.Content;
  return this;
}


In the simplest version of our fulfillment protocol, a fulfillment instance can no longer be modified once it has been claimed by a barista. At the AtomPub level, this means that a member can no longer be edited once it has been published. If the member belonging to an existing fulfillment instance has been published, Edit(…) throws an exception; otherwise, it updates the fulfillment instance’s member properties with values from the member contained in the request.

Testing WCF REST Services

Before we look at how we’ve implemented the fulfillment service itself, we’ll examine the mechanism we’ve used to separate the service implementation from the WCF runtime.

The WCF runtime acts as a service factory. This factory creates new service instances based on a declarative (config-, code- or attribute-based) service specification. When a WCF service starts, the runtime assembles a channel stack based on this specification. The channel stack takes care of a lot of common infrastructure tasks, including serializing, encoding, and dispatching messages to .NET methods. Importantly, for web applications, the channel stack also initializes an instance of WCF’s WebOperationContext helper class, which provides access to the HTTP request and response context. This helper object is available to service instances through the WebOperationContext.Current property.

With the HTTP context so tightly coupled to the WCF runtime, to test a service we must first start a service instance and then send it requests using an HTTP client. This adds unnecessary complexity to every test, as well as slowing down the execution of a large suite of tests—both of which hamper development. Because access to the HTTP context is critical to our service implementation, we decided to create our own wrapper around this context so that we could isolate our code from any runtime dependencies.

With this approach, our service logic is written against a request interface that we define, and populates a response object that we own. At runtime, we pass the service an implementation of our request interface that delegates to the WCF request instance. In our tests, however, we use a fake request. This allows us fine-grained control over all parts of the request, including the URI, headers, and entity body. When the service is finished handling a request, it creates a response object, which once again belongs to our decoupling framework. At runtime, this response populates a WCF response context; in our tests, it populates a fake context.

Example 8-24 shows our IRequest interface. Note that for requests containing an entity body we’ve also created a generic interface that derives from this base interface. This latter interface is parameterized with the deserialized type associated with the entity body in the service implementation.

Example 8-24. IRequest abstracts the runtime HTTP request context

public interface IRequest
{
  Uri Uri { get; }
  IRequestHeaders Headers { get; }
}

public interface IRequest<T> : IRequest
{
  T EntityBody { get; }
}


At runtime, service instances are given an instance of WcfRequest, the implementation of which is shown in Example 8-25.

Example 8-25. WcfRequest wraps the WCF request context at runtime

public class WcfRequest : IRequest
{
  private readonly Uri uri;
  private readonly IRequestHeaders headers;
  private readonly string method;

  public WcfRequest(OperationContext operationContext,
    WebOperationContext webOperationContext)
  {
    uri = GetUri(operationContext);
    headers = GetHeaders(webOperationContext);
    method = GetMethod(webOperationContext);
  }

  public Uri Uri
  {
    get { return uri; }
  }

  public IRequestHeaders Headers
  {
    get { return headers; }
  }

  protected static Uri GetUri(OperationContext context)
  {
    return context.EndpointDispatcher.EndpointAddress.Uri;
  }

  protected static IRequestHeaders GetHeaders(WebOperationContext context)
  {
    return new WcfRequestHeaders(context.IncomingRequest.Headers);
  }

  protected static string GetMethod(WebOperationContext context)
  {
    return context.IncomingRequest.Method;
  }
}

public class WcfRequest<T> : WcfRequest, IRequest<T>
{
  private readonly T entityBody;

  public WcfRequest(OperationContext operationContext,
    WebOperationContext webOperationContext)
    : base(operationContext, webOperationContext)
  {
    entityBody = GetEntityBody(operationContext);
  }

  public T EntityBody
  {
    get { return entityBody; }
  }

  private T GetEntityBody(OperationContext context)
  {
    var storedMessage = context.Extensions.Find<StoredMessage>();
    return storedMessage.Message.GetBody<T>();
  }
}


Most of the implementation of WcfRequest is quite straightforward. The WCF channel stack creates instances of OperationContext and WebOperationContext for each request. These instances are passed to the WcfRequest constructor, where they are used to initialize its property values.

The one piece of code that requires further explanation is the implementation of GetEntityBody(…). Here we retrieve a copy of the entity body from the operation context’s Extensions collection. We do this so that we can incorporate the entity body into a cohesive request object.


Note: The entity body is also made available to our service logic through a service method parameter. For example, the order fulfillment service’s CreateMember(…) method has a member parameter (of type Atom10ItemFormatter) that represents the entity body. In deciding to ignore this parameter in favor of a copy of the entity body that has been pushed onto the operation context, we made a deliberate decision to reinforce the cohesiveness of our request object (the request brings together headers and the entity body) at the expense of a small degree of additional complexity.



WCF extensions provide a mechanism for sharing state between different stages in the processing of a message. With our framework, as a message progresses through the message pipeline created by the WCF channel stack, we intercept it and put a copy into a StoredMessage extension object. We then attach this extension object to the current operation context. The extension is retrieved by a WcfRequest instance once the runtime has handed control to our service implementation.

To copy and store the received message in the operation context, we create a message inspector. WCF provides many extensibility points; message inspectors plug into one of these extensibility points, allowing us to examine or modify messages prior to being handed off to the service implementation. Example 8-26 shows the implementation of our StoreMessage message inspector.

Example 8-26. StoreMessage copies a received message and attaches it to the operation context

public class StoreMessage : IDispatchMessageInspector
{
  public object AfterReceiveRequest(ref Message request,
    IClientChannel channel, InstanceContext instanceContext)
  {
    MessageBuffer buffer = request.CreateBufferedCopy(Int32.MaxValue);
    OperationContext.Current.Extensions.Add(
      new StoredMessage(buffer.CreateMessage()));
    request = buffer.CreateMessage();
    buffer.Close();

    return null;
  }

  public void BeforeSendReply(ref Message reply, object correlationState)
  {
    //Do nothing
  }
}


The IDispatchMessageInspector interface here is defined by WCF. As you can see, the interface’s methods allow us to handle received messages prior to them being deserialized into our service’s typed objects, as well as deal with outgoing responses once they’ve been serialized into WCF messages. We take advantage only of the former capability, creating a copy of the received message, putting the copy into a new StoredMessage, and adding this stored message to the current operation context’s Extensions collection.

We insert the message inspector into the dispatching runtime using a service behavior, which is attached to the service host using a custom class attribute on the service implementation. This custom attribute, WcfDecouplingSupportAttribute, is shown in Example 8-27.

Example 8-27. WcfDecouplingSupportAttribute attaches a message inspector to the dispatch runtime

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public class WcfDecouplingSupportAttribute : Attribute, IServicebehavior
{
  public void Validate(ServiceDescription serviceDescription,
    ServiceHostBase serviceHostBase)
  {
    //Do nothing
  }

  public void AddBindingParameters(ServiceDescription serviceDescription,
    ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints,
    BindingParameterCollection bindingParameters)
  {
    //Do nothing
  }

  public void ApplyDispatchbehavior(ServiceDescription serviceDescription,
    ServiceHostBase serviceHostBase)
  {
    IEnumerable<DispatchRuntime> runtimes =
      (from ChannelDispatcher cd in serviceHostBase.ChannelDispatchers
       from EndpointDispatcher e in cd.Endpoints
       select e.DispatchRuntime);

    foreach (DispatchRuntime dr in runtimes)
    {
      dr.MessageInspectors.Add(new StoreMessage());
    }
  }
}


Again, IServicebehavior is an interface defined by WCF. When a service host starts, it calls the behavior’s interface methods at the appropriate point in the service startup life cycle. When ApplyDispatchbehavior(…) is called, the attribute instance adds a StoreMessage message inspector to each dispatch runtime.

The last parts of the WCF decoupling framework are the Request.Handle<>(…) and RequestWithEntityBody.Handle<>(…) methods. Each method accepts a function that takes a request object and parameters collection, and which returns a response. The methods are responsible for creating WcfRequest objects, invoking the service logic in the supplied function, and applying the response to the WCF response context.

WCF Service Implementation

With our AtomPub Collection and Member classes and our domain-specific Fulfillment and FulfillmentCollection classes in place, we’re ready to implement AtomPub.


Note: Our solution implements only a subset of AtomPub, because order fulfillment itself requires only a subset of the protocol.



Our first task is to create a C# service contract interface whose methods represent AtomPub transitions, as shown in Example 8-28.

Example 8-28. IOrderFulfillmentService exposes AtomPub resources and state transitions

[ServiceKnownType(typeof (Atom10FeedFormatter))]
[ServiceKnownType(typeof (Atom10ItemFormatter))]
public interface IOrderFulfillmentService
{
  [OperationContract]
  [WebGet(UriTemplate = "/")]
  Atom10FeedFormatter<Collection> GetCollection();

  [OperationContract]
  [WebGet(UriTemplate = "/{id}")]
  Atom10ItemFormatter<Member> GetMember(string id);

  [OperationContract]
  [WebInvoke(Method = "POST", UriTemplate = "/")]
  Atom10ItemFormatter<Member> CreateMember(Atom10ItemFormatter member);

  [OperationContract]
  [WebInvoke(Method = "PUT", UriTemplate = "/{id}")]
  void UpdateMember(string id, Atom10ItemFormatter<Member> member);

  [OperationContract]
  [WebInvoke(Method = "DELETE", UriTemplate = "/{id}")]
  void DeleteMember(string id);
}


This interface is attributed with two ServiceKnownType attributes. These attributes indicate to the runtime that it can expect to serialize and deserialize Atom10FeedFormatter and Atom10ItemFormatter types when handling requests and returning responses. In addition, each interface method has an OperationContract attribute, and either a WebGet or WebInvoke attribute. WebGet associates the attributed method with GET requests that match the supplied URI template. These requests will be dispatched to that attributed method’s implementation at runtime. WebInvoke associates any other HTTP verb (and the specified URI template) with the attributed method.

We now create a service class, OrderFulfillmentService, which implements this interface contract, as shown in Example 8-29.

Example 8-29. OrderFulfillmentService handles requests at runtime

[Servicebehavior(IncludeExceptionDetailInFaults = false,
  ConcurrencyMode = ConcurrencyMode.Single,
  InstanceContextMode = InstanceContextMode.PerCall)]
[WcfDecouplingSupport]
[AllowXmlSubTypeExtension]
public class OrderFulfillmentService : IOrderFulfillmentService
{
  private readonly CommandFactory commands;

  public OrderFulfillmentService(CommandFactory commands)
  {
    this.commands = commands;
  }

  public Atom10FeedFormatter<Collection> GetCollection()
  {
    return Request.Handle(
      (request, parameters) =>
        commands.GetFulfillmentCollection()
          .Execute(request, parameters));
  }

  public Atom10ItemFormatter<Member> GetMember(string id)
  {
    return Request.Handle(
      (request, parameters) =>
        commands.GetFulfillment()
          .Execute(request, parameters));
  }

  public Atom10ItemFormatter<Member> CreateMember(Atom10ItemFormatter member)
  {
    return RequestWithEntityBody.Handle<Atom10ItemFormatter,
      Atom10ItemFormatter<Member>>(
      (request, parameters) =>
        commands.CreateFulfillment()
        .Execute(request, parameters));
  }

  public void UpdateMember(string id, Atom10ItemFormatter<Member> member)
  {
    RequestWithEntityBody.Handle<Atom10ItemFormatter<Member>>(
      (request, parameters) =>
        commands.UpdateFulfillment()
          .Execute(request, parameters));
  }

  public void DeleteMember(string id)
  {
    Request.Handle(
      (request, parameters) =>
        commands.DeleteFulfillment().Execute(request, parameters));
  }
}


This class has been attributed with our framework’s WcfDecouplingSupport attribute. It has also been attributed with AllowXmlSubTypeExtension, which is another custom service behavior. AllowXmlSubTypeExtension configures the service to accept representations whose Content-Type header value ends with +xml. Above these two custom attributes, the Servicebehavior attribute controls the execution behavior of a service instance. As indicated by the PerCall value of the InstanceContextMode property of this Servicebehavior attribute, WCF creates a new instance of the service class for each request, ensuring that the service doesn’t retain any state between requests.

Looking at the body of OrderFulfillmentService, the interesting thing is that it doesn’t contain any service logic. Each service method simply creates a command, which it then executes to handle the request. The code that creates and executes a command is written as a lambda expression, which the service method passes to a static Handle<>(…) method from our own WCF decoupling framework. Handle<>(…) is parameterized with the types of the request and response entity bodies.

The service logic, then, is encapsulated in a number of commands, each of which can be developed and tested in isolation from the service infrastructure and runtime. Each command is responsible for validating a request, constructing a domain object (or retrieving one from a repository), and invoking its functionality. Example 8-30 shows the UpdateFufillment command implementation.

Example 8-30. UpdateFulfillment command

public class UpdateFulfillment
{
  private readonly IRepository repository;
  private readonly IDateTimeProvider dateTimeProvider;

  public UpdateFulfillment(IRepository repository,
    IDateTimeProvider dateTimeProvider)
  {
    this.repository = repository;
    this.dateTimeProvider = dateTimeProvider;
  }

  public Response Execute(IRequest<Atom10ItemFormatter<Member>> request,
    NameValueCollection parameters)
  {
    if (request.Headers.IfMatch == null)
    {
      return new Response(Status.PreconditionFailed);
    }

    if (!MediaTypes.AtomEntry.IsTypeAndSubtypeMatch(
      request.Headers.ContentType))
    {
      return new Response(Status.UnsupportedMediaType);
    }

    try
    {
      ETaggedEntity eTaggedEntity = repository.Get(new Guid(parameters["id"]));

      if (!eTaggedEntity.EntityTag.Equals(request.Headers.IfMatch))
      {
        return new Response(Status.PreconditionFailed);
      }

      Fulfillment fulfillment = eTaggedEntity.Fulfillment.Edit(
        (Member)request.EntityBody.Item, dateTimeProvider.Now);
      repository.Update(fulfillment, request.Headers.IfMatch);
    }
    catch (MemberDoesNotExistException)
    {
      return new Response(Status.NotFound);
    }
    catch (OptimisticUpdateFailedException)
    {
      return new Response(Status.PreconditionFailed);
    }
    catch (InvalidOperationException)
    {
      return new Response(Status.Conflict);
    }

    return new Response(Status.OK);
  }
}


This command first validates that the request contains an If-Match header and a Content-Type header with the correct content type (application/atom+xml). If either test fails, it returns a response with the appropriate 4XX status code. If both guard clauses pass, the command then retrieves the latest version of the fulfillment instance from the repository.

If the entity tag associated with the latest version doesn’t match the value supplied in the If-Match header, the command returns 412 Precondition Failed. If the values do match, it updates the latest version with the member supplied in the request’s entity body, and then updates the fulfillment instance in the repository. If at any point an error occurs, the command returns a 4XX status code; otherwise, it returns a response with 200 OK.

Cover of REST in Practice
Learn more about this topic from REST in Practice. 

REST continues to gain momentum as the best method for building web services, leaving many web architects to consider whether and how to include this approach in their SOA and SOAP-dominated world. In this insightful book, three SOA experts provide a down-to-earth explanation of REST and demonstrate how you can develop simple and elegant distributed hypermedia systems by applying the Web's guiding principles to common enterprise computing problems. You'll learn techniques for implementing specific Web technologies and patterns to solve the needs of a typical company as it grows from modest beginnings to become a global enterprise.

Learn More Read Now on Safari


Tags:
0 Subscribe


0 Replies