Over the course of several years Standard Mold has
collected various types of customer-related data. They started out with a
simple data model, but different departments added data based on other
pre-defined and ad-hoc models, some of which were specific to
departmental systems. The resulting disparity led to redundant data and
even different terminology used by different departments to refer to the
same types of data.
To resolve these
problems, several information modeling workshops were arranged and
attended by departmental business experts. After some debate and effort,
an agreement was reached as to a standard taxonomy used to describe the
common business entities and attributes. The resulting entity
relationship model is shown in Figure 1.
Standard Mold architects
identify the need for a Customer entity service that can be positioned
to provide centralized processing of customer-related data and
functions. During a service modeling effort, SOA analysts define a
Customer service based on the previously defined entity relationship
model.
Architects choose to build
the Customer service as a REST service to make the service
functionality available to consumer programs that do not support SOAP
and necessary WS-* standards, and to leverage HTTP caches for
performance reasons.
Figure 2 illustrates the planned interaction between the Customer REST service and service consumers.
The service interaction is described as follows:
1. | The service consumer requests a list of customers using a pre-defined address (URI).
|
2. | In the response from the Customer service, links are embedded that point at resources with the detailed customer information.
|
3. | The consumer traverses these links and uses the link with relationship type set to “details” to request detailed customer data.
|
4. | The response from the Customer service contains a link to meetings for the selected customer.
|
5. | The
consumer again traverses the links in the response and uses the link
marked with “meetings” to request the meeting resource for the current
customer.
|
Before the conversation
between the Customer service and the service consumer begins, the
Customer service only provides a service entry point. The consumer then
traverses links in the response documents to find related resources.
This gives the Customer service the freedom to change both how related
resources are named and arranged. Even the servers and domains of the uri:s
can be changed without prior notice. The only thing that the Customer
service cannot change without communicating with its consumers in
advance is the URI of the entry point.
Standard Mold architects
and developers carried out several specific steps to ensure that the
Customer REST service behaves as depicted in Figure 1.
First, they created a service contract using WCF attributes:
Example 1.
[ServiceContract] public interface ICustomerServiceRest { [OperationContract] [WebInvoke( Method = "PUT", BodyStyle = WebMessageBodyStyle.Bare, ResponseFormat = WebMessageFormat.Xml, UriTemplate = "/customers/create" )] void CreateCustomer(Customer customer); [OperationContract] [WebInvoke( Method = "POST", BodyStyle = WebMessageBodyStyle.Bare, ResponseFormat = WebMessageFormat.Xml, UriTemplate = "/customers/{customerId}" )] void UpdateCustomer(string customerId,Customer customer); [OperationContract] [WebInvoke( Method = "DELETE", UriTemplate = "/customers/{customerId}" )] void DeleteCustomer(string customerId); [OperationContract] [WebGet(UriTemplate = "/customers/{customerId}")] Atom10FeedFormatter GetCustomerDetails(string customerId); [OperationContract] [WebGet(UriTemplate = "/customers")] System.ServiceModel.Syndication. Atom10FeedFormatter GetCustomers(); [OperationContract] [WebGet(UriTemplate = "/customers/{customerId}/meetings")] Atom10FeedFormatter GetCustomerMeetings(string customerId); }
|
The
GetCustomers method acts as the entry point to the service and is called
using the “customers” relative URI and the GET verb:
Example 2.
public Atom10FeedFormatter GetCustomers() { List<Models.Customer> customers = DB.GetCustomers(); if (customers != null) { Uri uri = OperationContext.Current. IncomingMessageHeaders.To; Atom10FeedFormatter formatter = null; SyndicationFeed feed = new SyndicationFeed(); feed.LastUpdatedTime = DateTime.Now; feed.Id = WebOperationContext.Current.IncomingRequest. UriTemplateMatch.RequestUri.ToString(); feed.Title = new TextSyndicationContent("Customers"); feed.AddSelfLink(uri); List<SyndicationItem> items = new List<SyndicationItem>(); foreach (var customer in customers) { SyndicationItem item = new SyndicationItem(); item.Title = new TextSyndicationContent (customer.CompanyName); item.Content = SyndicationContent. CreateXmlContent(customer); SyndicationLink detailsLink = new SyndicationLink ( new Uri ( OperationContext.Current. IncomingMessageHeaders.To.AbsoluteUri + "/" + customer.CustomerId ) ); detailsLink.RelationshipType = "details"; detailsLink.Title = "Details"; item.Links.Add(detailsLink); items.Add(item); } feed.Items = items; formatter = new Atom10FeedFormatter(feed); WebOperationContext.Current. OutgoingResponse.ContentType = Microsoft.ServiceModel.Web.ContentTypes.Atom; return formatter; } WebOperationContext.Current. OutgoingResponse.SetStatusAsNotFound(); return null; }
|
The DB.GetCustomers method fetches a list of customer records from the database. In this list, only two of the customer fields (CompanyName and CustomerId)
are populated. This is a performance optimization technique used by
Standard Mold developers, because if all details of the Customer entity
are returned in a list, it would likely introduce unnecessary runtime
processing and bandwidth consumption.
After retrieving the
customer list, the Atom Publishing Protocol is used to represent the
resource. This protocol was chosen because it allows for the embedding
of links in resource descriptions in a standardized fashion so that they
can be easily found by service consumers.
A details link is added to the
customer list that consumers can use to obtain detailed information
about a given customer. The XML output from the GetCustomers method
looks like this:
Example 3.
<feed xmlns="http://www.w3.org/2005/Atom"> <title type="text">Customers</title> <id>http://standardmold/customerservice/customers</id> <updated>2010-01-10T17:34:20+01:00</updated> <link rel="self" type="application/atom+xml" href= "http://standardmold/customerservice/customers"/> <entry> <id>uuid:445cf20f-4eb0-4772-bad5-a44b51c48145;id=91</id> <title type="text">WoodGroove Ltd</title> <updated>2010-01-10T16:34:20Z</updated> <link rel="details" title="Details" href= "http://standardmold/customerservice/customers/91"/> <content type="text/xml"> <Customer xmlns= "http://schemas.datacontract.org/2004/07/ EntityService.Models"xmlns:i= "http://www.w3.org/2001/XMLSchema-instance"> <CompanyName>WoodGroove Ltd</CompanyName> <CustomerId>91</CustomerId> </Customer> </content> </entry> <entry> <id>uuid:445cf20f-4eb0-4772-bad5-a44b51c48145;id=92</id> <title type="text">Microsoft </title> <updated>2010-01-10T16:34:20Z</updated> <link rel="details" title="Details" href= "http://standardmold/customerservice/customers/92"/> <content type="text/xml"> <Customer xmlns="http://schemas.datacontract.org /2004/07/EntityService.Models" xmlns:i= "http://www.w3.org/2001/XMLSchema-instance"> <CompanyName>Microsoft</CompanyName> <CustomerId>92</CustomerId> </Customer> </content> </entry> <entry> <id>uuid:445cf20f-4eb0-4772-bad5-a44b51c48145;id=93</id> <title type="text">First Office</title> <updated>2010-01-10T16:34:20Z</updated> <link rel="details" title="Details" href= "http://standardmold/customerservice/customers/93"/> <content type="text/xml"> <Customer xmlns="http://schemas.datacontract.org /2004/07/EntityService.Models" xmlns:i= "http://www.w3.org/2001/XMLSchema-instance"> <CompanyName>First Office</CompanyName> <CustomerId>93</CustomerId> </Customer> </content> </entry> <entry> <id>uuid:445cf20f-4eb0-4772-bad5-a44b51c48145;id=94</id> <title type="text">Bosna</title> <updated>2010-01-10T16:34:20Z</updated> <link rel="details" title="Details" href= "http://standardmold/customerservice/customers/94"/> <content type="text/xml"> <Customer xmlns="http://schemas.datacontract.org /2004/07/EntityService.Models" xmlns:i= "http://www.w3.org/2001/XMLSchema-instance"> <CompanyName>Bosna</CompanyName> <CustomerId>94</CustomerId> </Customer> </content> </entry> </feed>
|
Sample service consumer code is developed to test the traversing of the XML to find the details link.
First the GetDetailsLink method is created:
Example 4.
private static Uri GetDetailsLink(Uri entryPoint, string customerId) { using (AtomPubClient atomHttpClient = new AtomPubClient()) { SyndicationFeed feeds = atomHttpClient.GetFeed(entryPoint); foreach (SyndicationItem customerFeedItem in feeds.Items) { foreach (SyndicationLink customerItemLink in customerFeedItem.Links) { if ( customerItemLink.RelationshipType. Equals("details") && customerItemLink.Uri.ToString(). EndsWith(customerId) ) { return customerItemLink.Uri; } } } } return null; }
|
A
test run of the service consumer retrieves the details link for customer
94 by calling the method with the following parameters:
Example 5.
Uri customerDetailsLink = GetDetailsLink(new Uri("http://standardmold/customerservice/customers"), "94");
|
When the service
consumer uses this link to request customer details, the
GetCustomerDetails method of the Customer service is executed. The
implementation of this method is similar to the GetCustomers method in
that it uses the Atom protocol and adds links to the customer details
data.
Example 6.
public Atom10FeedFormatter GetCustomerDetails(string customerId) { Customer customer = DB.GetCustomerById (Int32.Parse(customerId)); if (customer != null) { List<SyndicationItem> items = new List<SyndicationItem>(); SyndicationItem item = new SyndicationItem(); item.Title = new TextSyndicationContent (customer.CompanyName); item.Content = SyndicationContent. CreateXmlContent(customer); SyndicationLink editLink = new SyndicationLink ( WebOperationContext.Current. IncomingRequest.UriTemplateMatch.RequestUri ); editLink.RelationshipType = "edit"; editLink.Title = "Edit"; item.Links.Add(editLink); SyndicationLink meetingLink = new SyndicationLink ( new Uri ( WebOperationContext.Current.IncomingRequest. UriTemplateMatch.RequestUri.ToString() + "/meetings" ) ); meetingLink.RelationshipType = "meetings"; item.Links.Add(meetingLink); items.Add(item); SyndicationFeed feed = new SyndicationFeed { Id = WebOperationContext.Current.IncomingRequest. UriTemplateMatch.RequestUri.ToString(), Title = new TextSyndicationContent (customer.CompanyName) }; feed.AddSelfLink (WebOperationContext.Current. IncomingRequest.GetRequestUri()); feed.Items = items; Atom10FeedFormatter formatter = new Atom10FeedFormatter(feed); WebOperationContext.Current. OutgoingResponse.ContentType = Microsoft.ServiceModel.Web.ContentTypes.Atom; return formatter; } WebOperationContext.Current. OutgoingResponse.SetStatusAsNotFound(); return null; }
|
This
time, several links are added to the only entry in the feed. This entry
contains detailed customer information and the link to related meetings.
Here is the XML output from this method:
Example 7.
<feed xmlns="http://www.w3.org/2005/Atom"> <title type="text">Bosna</title> <id>http://standardmold/customerservice/customers/94</id> <updated>2010-01-10T16:36:46Z</updated> <link rel="self" type="application/atom+xml" href="http://standardmold/customerservice/ customers/94"/> <entry> <id>uuid:445cf20f-4eb0-4772-bad5-a44b51c48145;id=94</id> <title type="text">Bosna Jedina</title> <updated>2010-01-10T16:36:46Z</updated> <link rel="edit" title="Edit" href="http://standardmold/customerservice/ customers/94"/> <link rel="meetings" href="http://standardmold/customerservice/ customers/94/meetings"/> <content type="text/xml"> <Customer xmlns="http://schemas.datacontract.org/ 2004/07/EntityService.Models" xmlns:i="http://www.w3.org/ 2001/XMLSchema-instance"> <Cellular>+387(0)63912910</Cellular> <CompanyName>Bosna</CompanyName> <CustomerId>94</CustomerId> <Fax>+387(0)63912911</Fax> <Phone>+387(0)79900900</Phone> <TimeStamp>AAAAAAAAD6I=</TimeStamp> </Customer> </content> </entry> </feed>
|
The following method is created to get a meetings link from a customer details response:
Example 8.
private static Uri GetMeetingsLink(Uri customerDetailsUri) { using (AtomPubClient atomHttpClient = new AtomPubClient()) { SyndicationFeed feeds = atomHttpClient.GetFeed (customerDetailsUri); foreach (SyndicationItem customerFeedItem in feeds.Items) { foreach (SyndicationLink customerItemLink in customerFeedItem.Links) { if (customerItemLink.RelationshipType.Equals("meetings")) { return customerItemLink.Uri; } } } } return null; }
|
To obtain this link, the consumers use the URI returned by the GetDetailsLink method:
Uri meetingsUri = GetMeetingsLink(customerDetailsLink);
The URI points at the meetings resource for company 94, and the consumer is able to get the desired list of meetings.
The XML output by this method is as follows:
Example 9.
<feed xml:base="http://standardmold/customerservice/ customers/94/meetings" xmlns="http://www.w3.org/2005/Atom"> <title type="text">Customers Metting, CustomerId=94</title> <id>http://standardmold/customerservice/ customers/94/meetings</id> <updated>2010-01-10T17:38:13+01:00</updated> <entry> <id>uuid:445cf20f-4eb0-4772-bad5-a44b51c48145;id=7</id> <title type="text">Meeting Id:1</title> <updated>2010-01-10T16:38:13Z</updated> <content type="text/xml"> <Meeting xmlns="http://schemas.datacontract.org/ 2004/07/EntityService.Models" xmlns:i="http://www.w3.org/2001/ XMLSchema-instance"> <customerId>94</customerId> <dateofmeeting>2010-10-10T00:00:00</dateofmeeting> <meetingId>1</meetingId> <notes>
|
It’s
our experience that taking personal notes at meetings somehow breeds
respect and approval from superiors, and managers enjoy having a simple
list of bullet points and action items at-hand. The long and short—if
you have to suffer through the meeting anyways, make it a habit to
provide a valuable account to meeting organizers... you will be rewarded
with respect and trust.
Example 10.
</notes> </Meeting> </content> </entry> <entry> <id>uuid:445cf20f-4eb0-4772-bad5-a44b51c48145;id=8</id> <title type="text">Meeting Id:2</title> <updated>2010-01-10T16:38:13Z</updated> <content type="text/xml"> <Meeting xmlns="http://schemas.datacontract.org/ 2004/07/EntityService.Models" xmlns:i="http://www.w3.org/2001/ XMLSchema-instance"> <customerId>94</customerId> <dateofmeeting>2010-11-11T00:00:00</dateofmeeting> <meetingId>2</meetingId> <notes> We started on time with 3 members and the other team members arrived shortly thereafter </notes> </Meeting> </content> </entry> </feed>
|
A method that can obtain this XML from the Customer service and return a list of Meeting objects looks like this:
Example 11.
private static List<Meeting> GetCustomerMeetingList(Uri meetingsUri) { var customerMeetings = new List<Meeting>(); using (AtomPubClient atomHttpClient = new AtomPubClient()) { if (meetingsUri != null) { SyndicationFeed customerMeetingsFeed = atomHttpClient.GetFeed(meetingsUri); foreach (var item in customerMeetingsFeed.Items) { var customerMeetingXmlContent = item.Content as XmlSyndicationContent; if (customerMeetingXmlContent != null) { var customerMeeting = customerMeetingXmlContent. ReadContent<Meeting>(); customerMeetings.Add(customerMeeting); } } } } return customerMeetings; }
|
To
actually get the list of meetings, the service consumer calls the method
using the meeting list URI that was returned from the GetMeetingsList:
List<Meeting>meetingList=GetCustomerMeetingList(meetingsUri);
With a preliminary
version of the service architecture completed, Standard Mold architects
decide to make further improvements by ensuring that customer data is
not inadvertently overwritten in the database. A timestamp column is
added and updated via a SQL server stored procedure that updates
customer information only if the timestamp in the database remains
unchanged:
Example 12.
CREATE PROCEDURE [dbo].[UpdateCustomer] @id as uniqueidentifier, @cName as nvarchar(50), @phone as nvarchar(50), @fax as nvarchar(50), @cell as nvarchar(50), @updateTag as timestamp AS BEGIN BEGIN TRY SET NOCOUNT ON; --do not update if [TimeStamp] has been changed UPDATE dbo.Customer WITH (ROWLOCK) SET [CompanyName] = @cName, [Phone] = @phone, [Fax] = @fax, [Cellular] = @cell WHERE [CustomerId] = @id AND [TimeStamp] = @updateTag END TRY
BEGIN CATCH SELECT ERROR_NUMBER(), ERROR_LINE(),ERROR_MESSAGE() END CATCH END
|
With
a simple check in the data access code, it is possible to find out how
many rows are affected. If the result is 0 rows, then something went
wrong. Most likely the customer was changed by another request before
the current request could make the update.
The code that allows this
to be discovered can be found inside the UpdateCustomer method. It uses
the Enterprise Library Data Access Block in order to make the database
access logic more compact than it would be had it been written using
ADO.NET directly:
Example 13.
public bool UpdateCustomer(Customer customer) { using ( var dbCommand = Database.GetStoredProcCommand (StoredProcedures.UPDATECUSTOMER) ) { Database.AddInParameter ( dbCommand, "@id", DbType.Guid, customer.CustomerId ); Database.AddInParameter ( dbCommand, "@cName", DbType.String, customer.CompanyName ); Database.AddInParameter ( dbCommand, "@phone", DbType.String, customer.Phone ); Database.AddInParameter ( dbCommand, "@fax", DbType.String, customer.Fax ); Database.AddInParameter ( dbCommand, "@cell", DbType.String, customer.Cellular ); Database.AddInParameter ( dbCommand, "@updateTag", DbType.Binary, customer.TimeStamp ); int rowsAffected = Database.ExecuteNonQuery(dbCommand); return rowsAffected == 1; } }
|
If the
UpdateCustomer method returns “false,” the update was not successful.
This should normally be communicated back to the service consumer, as
follows:
Example 14.
public void UpdateCustomer (EntityService.Models.Customer customer) { try { bool updated = DB.UpdateCustomer(customer); if (updated) WebOperationContext.Current. OutgoingResponse.StatusCode = System.Net.HttpStatusCode.OK; else WebOperationContext.Current. OutgoingResponse.StatusCode = System.Net.HttpStatusCode.Conflict; } catch { ... exception handling code ... WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.InternalServerError; } }
|
The consumer of the Customer service will get an HTTP response with the following status if the optimistic lock failed:
This is exactly what is
expected of a REST service in this situation. Standard Mold developers,
however, choose to add more information to the response so that service
consumers can understand the nature of the conflict.
If an unexpected
exception occurs, the service consumer will instead receive the
following status, which communicates that there is a problem in the
service:
500 Internal server error
If everything proceeds as expected the consumer will get the response code: