Creating and Consuming RESTful Web Services with WCF
-
Upload
julian-kline -
Category
Documents
-
view
30 -
download
0
description
Transcript of Creating and Consuming RESTful Web Services with WCF
Creating and Consuming RESTful Web Services with WCF
Creating and Consuming RESTful Web Services with WCFRon JacobsSr. Technical EvangelistPlatform EvangelismMicrosoft Corporation
AgendaAgenda
What is REST?Is REST SOA?Key REST principlesAdventure Works REST API WCF ExampleSummary
71 Slides5 DemosI must be insane!
WHAT IS REST?WHAT IS REST?
“Representational state transfer (REST) is a style of software architecture for distributed hypermedia systems such as the World Wide Web.”http://en.wikipedia.org/wiki/Representational_State_Transfer
What is REST?What is REST?
Application state and functionality are resources Every resource has a URIAll resources share a uniform interface
HTTP
IS REST SOA?IS REST SOA?
“Protocol independence is a bug, not a feature”.
- Mark Baker
SOAP REST
WCF Test Client Notepad
Internet Explorer
IS REST SOA?IS REST SOA?
REST is an architectural style that allows you to implement services with broad reach SOA is about services
SOA is not about protocol, transport, format etc.
5 HTTP Messages18,604 bytes“You entered: 1”
KEY REST PRINCIPLESKEY REST PRINCIPLES
“The promise is that if you adhere to REST principles while designing your application, you will end up with a system that exploits the Web’s architecture to your benefit.”
-Stefan Tilkovhttp://www.infoq.com/articles/rest-introduction
Key REST PrinciplesKey REST Principles
Give every “thing” an ID Link things together Use standard methods Resources with multiple representations Communicate statelessly
Give every “thing” an IDGive every “thing” an ID
Expose thing or collection things with a scheme that is ubiquitousEmbrace the URI
How to get it (http:// or net.tcp:// or net.msmq:// etc.)Where to get it (example.com)What to get (customer 1234)
Give every “thing” an IDGive every “thing” an ID
Customer C = GetCustomer(1234);
http://example.com/customers/1234
An API like this
Can be represented like this
Link Things TogetherLink Things Together
Hypermedia as the engine of application stateJust means that you should link things togetherPeople or apps can transition state by following linksLinks can be to the same app or to some other app
Link Things TogetherLink Things Together
<CustomerData> <Self>http://adventure-works.com/customer/1</Self> <CompanyName>A Bike Store</CompanyName> <CustomerID>1</CustomerID> <EmailAddress>[email protected]</EmailAddress> <FirstName>Orlando</FirstName> <LastName>Gee</LastName> <Orders>http://adventure-works.com/customer/1/orders</Orders> <RowID>3f5ae95e-b87d-4aed-95b4-c3797afcb74f</RowID></CustomerData>
http://search.live.com/results.aspx?q=Ron+Jacobs&first=11...
Use Standard MethodsUse Standard Methods
public class Resource {
Resource(Uri u); Response Get(); Response Post(Request r); Response Put(Request r); Response Delete();Response Head();
}
Shift to Resource ThinkingShift to Resource Thinking
Operation SQL Command HTTP Verb
Create a resource INSERT POST(a), PUT
Read a resource SELECT GET
Update a resource UPDATE PUT, POST(p)
Delete a resource DELETE DELETE
Query Metadata (Systables) HEAD
INSERT INTO CUSTOMERS (...) VALUES (...)
SQL
(POST) http://example.com/customers<Customer>...</Customer>
REST
Shift to Resource ThinkingShift to Resource Thinking
Operation SQL Command HTTP Verb
Create a resource INSERT POST
Read a resource SELECT GET
Update a resource UPDATE PUT (POST)
Delete a resource DELETE DELETE
Query Metadata (Systables) HEAD
SELECT FROM CUSTOMERS WHERE ID=567
SQL
(GET) http://example.com/customers/567
REST
Resources as operationsResources as operations
The result of an operation can be considered a resource
var result = CalculateShipping(“Redmond”, “NYC”);
API
http://example.com/calculateShipping?from=“Redmond”&to=“NYC”
REST
Content NegotiationContent Negotiation
Allow the client to ask for what they want“I want XML”
“I want JSON”
“I want …” (HTML, CSV, etc.)
GET /customers/1234 HTTP/1.1Host: example.com Accept: text/xml
GET /customers/1234 HTTP/1.1Host: example.com Accept: text/json
JSR 311 features the ideaof extensions as a wayto do content negotiationwithout the headers as in/customers.xml /customers.json
Communicate StatelesslyCommunicate Statelessly
Stateless means that every request stands alone
Session is not requiredCan be bookmarked
Application State lives on the ClientEverything that is needed to complete the request must be included in the request
Resource State lives on the server(s)Stored in durable storage (typically)May be cached
ADVENTURE WORKS REST APIADVENTURE WORKS REST API
Implementation Time
AdventureWorks Customer APIAdventureWorks Customer API
URI Method
Collection Operation
/customers POST Customers Create
/customers/{custId} GET Customers Read
/customers/{custId} PUT Customers Update
/customers/{custId} DELETE Customers Delete
/customers/{custId}/Orders
GET Sales Read customer orders
HTTP GETHTTP GET
“Remember that GET is supposed to be a “safe” operation, i.e. the client does not accept any obligations (such as paying you for your services) or assume any responsibility, when all it does is follow a link by issuing a GET.”
-Stefan Tilkov http://www.infoq.com/articles/tilkov-rest-doubts
GET CUSTOMER DEMOGET CUSTOMER DEMO
WebGet AttributeUriTemplateQuery String Parameters
http://rojacobsxps/AdventureWorksDev/api/customer/1
WebGet AttributeWebGet Attribute
WebGet Indicates you want to use an HTTP GET for this methodMethod name is resource nameArguments are query string parameters
// GET a customer[OperationContract][WebGet]CustomerData GetCustomer(string customerId);
http://localhost/service.svc/GetCustomer?customerId=1
WebGet UriTemplateWebGet UriTemplate
UriTemplate maps the URI to parameters in your method
Using parameters in the Uri makes them mandatory, query string parameters are optional.
// GET a customer[OperationContract][WebGet(UriTemplate = "customer/{customerId}")]CustomerData GetCustomer(string customerId);
http://localhost/service.svc/Customer/1
Making your first RESTful ServiceMaking your first RESTful Service
Create a WCF Service LibraryAdd a reference / usingSystem.ServiceModel.Web
Decorate your method with WebGetModify configuration
Change the binding from wsHttpBinding to webHttpBindingAdd the webHttp endpoint behavior to the endpoint
Note: WCF will start up without this behavior though it is not very useful configuration
Get CustomersGet Customers
Returns a collection of customers from the databaseIssues
Security – you can only see orders you are allowed to seePaging – stateless requests decide where to start
REST API
SOAP API
http://adventure-works.com/customer
Customer[] GetCustomers()
PagingPaging
Allows clients to request a subset of the collectionUse Query String parameters to specify start index and count
http://adventure-works.com/customer?start=200&count=25
Gotcha! Gotcha!
// GET customers[OperationContract] [WebGet(UriTemplate="customer?start={start}&count={count}")]CustomerGroupingData GetCustomers(int start, int count);
// POST to customers[OperationContract][WebInvoke(UriTemplate = "customer")]CustomerData AppendCustomer(CustomerData customer);
405 Method not allowedhttp://adventure-works.com/customer
Why?Why?
The template matching engine tries to find the best matchThe more specific a match is, the betterWhen the URL contains just the resource “customer”The match for “customer” is a POST method
Return 405 Method not allowed
Why?Why?
SolutionDon’t include the query string parameters in the UriTemplateGet them instead from the WebOperationContext.CurrentUriTemplate is now just “customer” for both GET and POST
Solution Solution
// GET customers[OperationContract] [WebGet(UriTemplate = "customer")]CustomerGroupingData GetCustomers(int start, int count);
// POST to customers[OperationContract][WebInvoke(UriTemplate = "customer")]CustomerData AppendCustomer(CustomerData customer);
200 Ok
http://localhost/AdventureWorksDev/api/customer
Query String ParametersQuery String Parameters
private string GetQueryString(string argName){ UriTemplateMatch match =
WebOperationContext.Current.IncomingRequest.UriTemplateMatch;
try { return match.QueryParameters[argName]; } catch (KeyNotFoundException) { return null; }}
Query String Parametersare found in here
CachingCaching
Use HttpRuntime.Cache to cache items on the server if it makes sense to do so
// Check the cacheCustomerData customerData = (CustomerData)HttpRuntime.Cache[requestUri.ToString()];
// Not found in the cacheif (customerData == null){ // Try to get the customer data customerData = CustomersCollection.GetCustomer(custId);
// Still not found if (customerData == null) { outResponse.SetStatusAsNotFound(string.Format("Customer Id {0} not found", customerId)); } else // found { // Set the headers outResponse.LastModified = customerData.LastModified; outResponse.ETag = customerData.ETag.ToString(); CacheCustomer(requestUri, customerData); }}
Client CachingClient Caching
Add Expires or Cache-Control headers to provide clients with hints on cachingWCF Default: Cache-Control: private
No caching of private results
// Allow client to cache for 5 minutesoutResponse.Headers.Add("Cache-Control", "300");
Conditional GETConditional GET
Headers used by clients to save bandwidth if they hold cached dataIf-Modified-Since: (Date)
Return the data only if it has been modified since (Date)
Conditional GETConditional GET
If-None-Matches: (Etag)Return the data only if there are no records matching this tag
If the data exists but has not been modified return 304 “Not Modified”
The server still has to verify that the resource exists and that it has not changed
Supporting If-Modified-SinceSupporting If-Modified-Since
Your data should have a LastModified valueUpdate it whenever the data is written
// Set the headersoutResponse.LastModified = customerData.LastModified;
Supporting If-None-MatchesSupporting If-None-Matches
Your data should have a row versionThis data is returned in an Etag header as an opaque string
// Set the headersoutResponse.ETag = customerData.ETag.ToString();
Conditional GET CheckConditional GET Check
private static void CheckModifiedSince( IncomingWebRequestContext inRequest, OutgoingWebResponseContext outResponse, CustomerData customerData){ // Get the If-Modified-Since header DateTime? modifiedSince = GetIfModifiedSince(inRequest);
// Check for conditional get If-Modified-Since if (modifiedSince != null) { if (customerData.LastModified <= modifiedSince) { outResponse.SuppressEntityBody = true; outResponse.StatusCode = HttpStatusCode.NotModified; } }} Not Modified?
Suppress bodyReturn 304 “Not
Modified”
GET ResponseGET Response
200 OKGET successful
304 Not ModifiedConditional GET did not find new data
400 Bad RequestProblem with the request of some kind
404 Not FoundResource was not found
500 Internal Server ErrorEverything else
HTTP POSTHTTP POST
“You can use it to create resources underneath a parent resource and you can use it to append extra data onto the current state of a resource.”
- RESTful Web Services
HTTP POSTHTTP POST
POST is ambiguously defined in the HTTP specPOST is the second most used RESTful verbOften referred to as POST(a) for “Append”
Posting to a collection means to append to that collectionAllows the server to determine the ultimate URI
HTTP POSTHTTP POST
ProblemHow to detect duplicate POST requests?
SolutionsUse PUT (it’s Idempotent by nature)Schemes involving handshaking of some kind between the client and serverClient generated identifier for POST
POST to CustomersPOST to Customers
Appends a new customer to the collectionIssues
Security – Are you allowed to create a customer?Idempotency – is this a duplicate POST request?
REST API
SOAP API(POST) http://adventure-works.com/customers
CustomerData AppendCustomer(CustomerData customer);
POST ExamplePOST Examplepublic CustomerData AppendCustomer(CustomerData customer){ OutgoingWebResponseContext outResponse = WebOperationContext.Current.OutgoingResponse;
try { CustomerData newCustomer = CustomersCollection.AppendCustomer(customer);
if (newCustomer.CustomerID != 0) { outResponse.SetStatusAsCreated(
BuildResourceUri("customer", newCustomer.CustomerID.ToString())); }
return newCustomer; } catch (CustomerRowIDExistsException) { outResponse.StatusCode = HttpStatusCode.Conflict; outResponse.StatusDescription = "RowID exists, it must be unique"; return null; } catch (Exception ex) { Log.Write(ex); throw; }}
POST(a) ResponsePOST(a) Response
200 OKPOST successful
400 Bad RequestProblem with the request of some kind
409 ConflictResource already exists
500 Internal Server ErrorEverything else
Testing POST MethodsTesting POST Methods
Fiddler – http://www.fiddler2.com HTTP proxyUse machine name instead of localhost in URI
IIS hosting helps with this
Build a requestDrag a GET request to the Request BuilderOpen a GET in Visual Studio for easy formatting of XML
Set the Request type to POSTSet the request body to valid XMLSet the Content-Type: text/xml
HTTP PUTHTTP PUT
PUT is an idempotent way to create / update a resource
HTTP PUTHTTP PUT
Creates or Updates the resourceCompletely replaces whatever was there before with the new contentUpdate the cache with new resource
Idempotent by designCreating or Updating record 123 multiple times should result in the same valueDo NOT do some kind of relative calculation
PUT and IDsPUT and IDs
If you can allow the client to define an ID within a context that is unique, PUT can insert, otherwise PUT is used to update resourcesREST API
SOAP API
Note: the first arg comes from the URI, the customer data comes from the request body
(PUT) http://adventure-works.com/customers/123
CustomerData PutCustomer(string customerId, CustomerData customer);
PUT ResponsePUT Response
200 OKUpdate successful
201 CreatedInsert Successful
400 Bad RequestProblem with the request of some kind
404 Not FoundResource to update was not found
500 Internal Server ErrorEverything else
HTTP DELETEHTTP DELETE
DELETE is for uh... well... um... deleting things
DELETEDELETE
Used to delete a resourceIssues
Security – can you delete a resourceCache – need to remove it from the server cache
DELETE ResponseDELETE Response
200 OKDelete successful
400 Bad RequestProblem with the request of some kind
404 Not FoundResource to delete was not found
500 Internal Server ErrorEverything else
URI MappingURI Mapping
URI Method Maps To
api/customer GET api.svc/customer
api/customer POST api.svc/customer
api/customer/{ID} GET api.svc/customer/{ID}
api/customer/{ID} DELETE api.svc/customer/{ID}
api/customer/{ID} PUT api.svc/customer/{ID}
api/customer/{ID} HEAD api.svc/customer/{ID}
UriMapper HTTP ModuleUriMapper HTTP Module
RESTful people do not like ugly URIs
ScottGu has Many ways to rewrite URIsHttpModules can rewrite the URIs as they come inJon Flanders blog
Using WCF WebHttpBinding and WebGet with nicer Urls Modified it a bit to support my scenario
http://adventure-works.com/service.svc/customer/1
Gotchca! Gotchca!
I installed my HttpModule in web.config under <system.web><httpModules>Not workingSearched blogs to find out that for IIS 7 you must install under <system.webServer><modules>If your HttpModule does not rewrite the URL correctly you will get 404 errors and have a hard time understanding why
Content NegotiationContent Negotiation
Trend is toward an extension syntax
Unfortunately you must specify the response format in the WebGet, WebInvoke attributeYou can dynamically choose your format by making your service return a stream and then serializing your content directly to the stream
http://adventure-works.com/customers.xhtmlhttp://adventure-works.com/customers.xmlhttp://adventure-works.com/customers.json
Two ServicesTwo Services
apixml.svc for XMLapijson.svc for JSONUriMapper code looks for an extension on the resource and maps it to the appropriate service
default is XML
2 .SVC files means two classes that implement 2 different contracts
Class DiagramClass Diagram
URI MappingURI Mapping
URI Method Maps To
api/customer GET apixml.svc/customer
api/customer POST apixml.svc/customer
api/customer/{ID} GET apixml.svc/customer/{ID}
api/customer/{ID} DELETE apixml.svc/customer/{ID}
api/customer/{ID} PUT apixml.svc/customer/{ID}
api/customer/{ID} HEAD apixml.svc/customer/{ID}
api/customer.json GET apijson.svc/customer
api/customer.json POST apijson.svc/customer
api/customer/{ID}.json
GET apijson.svc/customer/{ID}
api/customer/{ID}.json
DELETE apijson.svc/customer/{ID}
api/customer/{ID}.json
PUT apijson.svc/customer/{ID}
api/customer/{ID}.json
HEAD apijson.svc/customer/{ID}
Content Negotiation DemoContent Negotiation Demo
http://rojacobsxps/AdventureWorksDev
Consuming a RESTful ServiceConsuming a RESTful Service
Use WCF or HttpWebRequestNeed a ServiceContract interface
Copy from serviceBuild from scratch
Build UriTemplates that will match up to the service
Data Contract SchemaData Contract Schema
Export Schema from Assemblysvcutil foo.dll /dconly
Generate data contracts from xsd filessvcutil *.xsd /dconly
Using WCF on the ClientUsing WCF on the Clientpublic CustomerData GetCustomer(string customerId, Guid? eTag, DateTime? modifiedSince){ using (var factory = new WebChannelFactory<IAdventureWorksServiceXml>("AdventureWorks")) { IAdventureWorksServiceXml service = factory.CreateChannel();
using (OperationContextScope scope = new OperationContextScope( (IContextChannel)service)) { OutgoingWebRequestContext request = WebOperationContext.Current.OutgoingRequest;
if (eTag != null) request.IfNoneMatch = eTag.ToString();
if (modifiedSince != null) { DateTimeFormatInfo formatInfo = CultureInfo.InvariantCulture.DateTimeFormat;
// RFC1123Pattern request.IfModifiedSince = modifiedSince.Value.ToString("r", formatInfo); } return service.GetCustomer(customerId); } }}
You must createa scope to access the
context
SummarySummary
RESTful services extend the reach of HTTP to your SOARESTful design is harder than you might thinkImplementation has some tricky issues to overcome