WCF Web Services & iOS - Part 4
By Mike Gledhill
Right then, so far, we have created a couple of "GET" Web Services for Reading records, and we have a POST Web Service for Updating a Customer record.
http://localhost:15021/Service1.svc/getAllCustomers
http://localhost:15021/Service1.svc/getOrdersForCustomer/ANATR
http://localhost:15021/Service1.svc/updateOrderAddress
In this lesson, we'll complete our CRUD (Create, Read, Update, Delete) functionality, by
adding services for Creating a new [Customer] record and Deleting a [Customer] record.
By the end of this lesson, we'll have the following two services:
http://localhost:15021/Service1.svc/createCustomer
http://localhost:15021/Service1.svc/deleteCustomer/ALFKI
Creating a "GET" WCF Web Service to Delete a [Customer] record
Let's start with the easier of the two functions.
Hopefully, you can guess that we'll be creating will be a "GET" WCF Web Service, which takes the ID of a [Customer] record,
and deletes that record from our database. Here's what our URL will look like:
http://localhost:15021/Service1.svc/deleteCustomer/ALFKI
We'll include a bit of error-checking, by setting the return value to 0 if it's successful, or 1 for an error.
Can you work out how to do this yourself ?
Just like with the getOrdersForCustomer service, our first stop is the IService1.cs file, and to tell it the format of the URL we'll be expecting. Add the following lines below your existing [OperationContract] declarations:
[OperationContract]
[WebInvoke(Method = "GET", ResponseFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.Wrapped, UriTemplate = "deleteCustomer/{customerID}")]
int DeleteCustomer(string customerID);
Next, hop across to the Service1.svc.cs file, and add the DeleteCustomer function:
public int DeleteCustomer(string customerID)
{
try
{
NorthwindDataContext dc = new NorthwindDataContext();
Customer cust = dc.Customers.Where(s => s.CustomerID == customerID).FirstOrDefault();
if (cust == null)
{
// We couldn't find a [Customer] record with this ID.
return -3;
}
dc.Customers.DeleteOnSubmit(cust);
dc.SubmitChanges();
return 0; // Success !
}
catch (Exception ex)
{
return -1; // Failed.
}
}
Nothing too painful here.
We attempt to find a [Customer] record with the ID which we've passed to the service, and it returns a value of -3
if we
can't find such a record.
If we did find a matching [Customer] record, then we attempt to delete it from our database,
and return 0
if it was successfully deleted.
Question time
Now, this all seems pretty straightforward, so why did I bother to wrap the function in a try..catch block ?
Answer
Sometimes, the Delete might fail. Let's try running our service.
http://localhost:15021/Service1.svc/deleteCustomer/ALFKI
Ah. That isn't good.
It's returned a -1
, which means that an exception occurred. The problem is, we can't tell what went wrong.
Let's add one line of code, to see what is going wrong.
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine(ex.Message);
return 1; // Failed.
}
Try running the URL again, and have a look at what gets added to the Output window in Visual Studio.
The DELETE statement conflicted with the REFERENCE constraint "FK_Orders_Customers". The conflict occurred in database "Northwind", table "dbo.Orders", column 'CustomerID'.
The statement has been terminated.
Ah. Of course.
We can't delete [Customer]
records, if that Customer has [Order]
records
referring to it's ID. SQL Server won't let us.
And that is why we wrapped the Delete in a try..catch
block, rather than allowing such problems to throw an exception and crash
our service.
Okay, okay, I chose a bad example, to demonstrate the importance of error-checking, but hopefully in your real-world services, you'll avoid such issues.
By the way, there are several ways to fix this problem.
1. Add extra code, to delete dependant tables.
The simplest fix in this particular example is to make sure that we delete the CustomerCustomerDemo and Orders tables before attempting to delete the Customer table. But, of course, this just fixes this one issue. It doesn't help if, for example, we call this service and the SQL Server database is temporarily down.
2. Modify the SQL Server foreign keys to use cascading deletes
Again, if you're having issues with deletes failing, due to dependencies on records in other tables, you can alter the FOREIGN KEY declarations to automatically delete those dependent records.
There are plenty of references on the internet describing how to do this, but the key to such problems is to find the Foreign Key in SQL Server, right-click on it, select Modify, expand "INSERT and UPDATE Specification", then set the "Delete Rule" to "Cascade".
With this change in place (for all foreign keys which would be affected by a delete), you should find the delete now works successfully.
3. Make our service return an exception string.
A cleaner solution is to get our service to return a very
simple class, containing a Success
boolean, and an Exception
string. This ensures that if the code
hits any kind of unexpected database error, we can report details of the error back to the user,
rather than just returning a Yes/No value, saying if the Delete was successful or not.
Let's try this.
Add a new class file (.cs file) to our project called SQLResult.cs, and in it,
define a wsSQLResult class.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Runtime.Serialization;
namespace JSONWebService
{
[DataContract]
public class wsSQLResult
{
[DataMember]
public int WasSuccessful { get; set; }
[DataMember]
public string Exception { get; set; }
}
}
Next, change the DeleteCustomer function to return a wsSQLResult record as it's result.
public wsSQLResult DeleteCustomer(string customerID)
{
wsSQLResult result = new wsSQLResult();
try
{
NorthwindDataContext dc = new NorthwindDataContext();
Customer cust = dc.Customers.Where(s => s.CustomerID == customerID).FirstOrDefault();
if (cust == null)
{
// We couldn't find a [Customer] record with this ID.
result.WasSuccessful = -3;
result.Exception = "Could not find a [Customer] record with ID: " + customerID.ToString();
return result;
}
dc.Customers.DeleteOnSubmit(cust);
dc.SubmitChanges();
result.WasSuccessful = 1;
result.Exception = "";
return result;
// Success !
}
catch (Exception ex)
{
result.WasSuccessful = -1;
result.Exception = "An exception occurred: " + ex.Message;
return result;
// Failed.
}
}
Oh, and don't forget to change the service declaration in the IService1.cs file.
[OperationContract]
[WebInvoke(Method = "GET", ResponseFormat = WebMessageFormat.Json, BodyStyle = WebMessageBodyStyle.Wrapped, UriTemplate = "deleteCustomer/{customerID}")]
wsSQLResult DeleteCustomer(string customerID);
And now if we call our DeleteCustomer
service again...
http://localhost:15021/Service1.svc/deleteCustomer/ALFKI
...it will return details of what actually went wrong.
As you can see, this is a much safer method of checking for errors, and allowing whatever code is calling your service to report details about what the problem/exception was.
Creating a "POST" WCF Web Service to Insert a [Customer] record
Hopefully, if you've got this far, you'll have some idea of how to write a Web Service to insert a new record.
It's going to have to be a "POST" web service, as we'll be sending details about a new
record to the service, rather than just calling a "GET" service and passing it a parameter.
And it's likely to look a lot like the UpdateOrderAddress
service which we created earlier.
We'll also provide a return type of wsSQLResult
, so if anything goes
wrong, we can give the user details about the error.
So, our first stop is the IService1.cs file, to declare what our service will look like to the outside world.
[ServiceContract]
public interface IService1
{
...
[OperationContract]
[WebInvoke(Method = "POST", ResponseFormat = WebMessageFormat.Json, UriTemplate = "createCustomer")]
wsSQLResult CreateCustomer(Stream JSONdataStream);
...
So, our service will take a Stream parameter, and our code will need to deserialize it into
a variable of type wsCustomer
, which we defined earlier.
As usual, our next step is to add the function to our Service1.svc.cs file:
public wsSQLResult CreateCustomer(Stream JSONdataStream)
{
wsSQLResult result = new wsSQLResult();
try
{
// Read in our Stream into a string...
StreamReader reader = new StreamReader(JSONdataStream);
string JSONdata = reader.ReadToEnd();
// ..then convert the string into a single "wsCustomer" record.
JavaScriptSerializer jss = new JavaScriptSerializer();
wsCustomer cust = jss.Deserialize<wsCustomer>(JSONdata);
if (cust == null)
{
// Error: Couldn't deserialize our JSON string into a "wsCustomer" object.
result.WasSuccessful = 0;
result.Exception = "Unable to deserialize the JSON data.";
return result;
}
NorthwindDataContext dc = new NorthwindDataContext();
Customer newCustomer = new Customer()
{
CustomerID = cust.CustomerID,
CompanyName = cust.CompanyName,
City = cust.City
};
dc.Customers.InsertOnSubmit(newCustomer);
dc.SubmitChanges();
result.WasSuccessful = 1;
result.Exception = "";
return result;
}
catch (Exception ex)
{
result.WasSuccessful = 0;
result.Exception = ex.Message;
return result;
}
}
Wasn't that simple ?
We take the JSON stream, convert it into a single wsCustomer record, then use the regular LINQ commands to create a new Customer record, and insert it into our database table.
With this in place, we can use our TestPostWebService project or Fiddler
to call our new Insert web service.
You'll need to just provide this URL:
http://localhost:15021/Service1.svc/createCustomer
...and some JSON data, such as this...
{
"CustomerID": "ABC12",
"CompanyName": "Mikes Company",
"City": "Zurich"
}
...and you should now find the insert works as expected:
Summary
And that's it.
We've now gone from a basic web service, turned it into a JSON Web Service, and
added services to Create, Read, Update and Delete records.
Now... at long last, it's time to start
looking at how to use them on an iOS device.
But, before we get there, we need to do two things:
Comments