# Monday, November 29, 2010

In May of 2010, Google released a new 2.5 version of the Google Checkout API.  The most compelling feature of this version of the API is that it no longer requires https/SSL to retrieve notifications.  They didn't update the .NET sample code to show how to use the new API though (it's now November, so it's been six months).

Below I have not only included the missing sample code for notification handling with GCO 2.5, but I did it using ASP.NET MVC instead of ASP.NET Classic WebForms.  I believe this is the first published Google Checkout ASP.NET MVC sample.

You will want to check the Integration Console in the Google Checkout Sandbox website constantly while testing.  Some errors will only show up there.  You will also want to catch and log errors somewhere like a database, which the below sample does not do.  You may also want to send yourself an e-mail when an order comes through.  You'll have to code that feature yourself, it's not too difficult.


First, Download the Google Checkout .NET DLLs (at least version 2.5.0.3, published 10-16-2010)
Unzip the Google Checkout .NET DLLs and put them somewhere you can find them later


Next, Start Visual Studio 2008

File -> New -> Project
 Visual C# -> Web -> ASP.NET MVC Web Application
  GcoNotifHandlerForSandbox
  OK
 No, do not create a unit test project
  OK

In Solution Explorer -> Right Click References -> Add Reference...
 Select the "GCheckout.dll" you downloaded and unzipped above
 OK

Open web.config for editing:
 Replace "<appSettings/>" with:

<appSettings>
  <
add key="GoogleMerchantID" value="YourMerchantID" />
  <
add key="GoogleMerchantKey" value="YourMerchantKey" />
  <
add key="GoogleEnvironment" value="Sandbox" />
</
appSettings>

Be sure to substitute in your proper sandbox merchant ID and key.

In Solution Explorer -> Right Click Controllers -> Add -> Controller...
 Controller Name: GcoNotifHandlerController
 Add

Open Global.asax.cs for editing:
 Add a new route before the default route:

routes.MapRoute(
  "GcoNotifHandler",
  "{controller}/{action}/{id}",
  new { controller = "GcoNotifHandler", action = "Index", id = "" }
);

In Solution Explorer -> Right Click Models -> Add -> Class...
 Name: GoogleCheckoutHelper.cs
 Add

Add the following new static methods to GoogleCheckoutHelper:

public static string GetGoogleOrderNumber(string serialNumber)
{
  return serialNumber.Substring(0, serialNumber.IndexOf('-'));
}

private static void HandleAuthorizationAmountNotification(GCheckout.AutoGen.AuthorizationAmountNotification inputAuthorizationAmountNotification)
{
  // TODO: Add custom processing for this notification type
}

private static void HandleChargeAmountNotification(GCheckout.AutoGen.ChargeAmountNotification inputChargeAmountNotification)
{
  // TODO: Add custom processing for this notification type
}

private static void HandleNewOrderNotification(GCheckout.AutoGen.NewOrderNotification inputNewOrderNotification)
{
  // Retrieve data from MerchantPrivateData
  GCheckout.AutoGen.anyMultiple oneAnyMultiple = inputNewOrderNotification.shoppingcart.merchantprivatedata;
  System.Xml.
XmlNode[] oneXmlNodeArray = oneAnyMultiple.Any;
  string hiddenMerchantPrivateData = oneXmlNodeArray[0].InnerText;
  // TODO: Process the MerchantPrivateData if provided

  foreach (GCheckout.AutoGen.Item oneItem in inputNewOrderNotification.shoppingcart.items)
  {
    // TODO: Get MerchantItemId from shopping cart item (oneItem.merchantitemid) and process it
  }

  // TODO: Add custom processing for this notification type
}

private static void HandleOrderStateChangeNotification(GCheckout.AutoGen.OrderStateChangeNotification inputOrderStateChangeNotification)
{
  // Charge Order If Chargeable
  if ((inputOrderStateChangeNotification.previousfinancialorderstate.ToString().Equals("REVIEWING")) && (inputOrderStateChangeNotification.newfinancialorderstate.ToString().Equals("CHARGEABLE")))
  {
    GCheckout.OrderProcessing.
ChargeOrderRequest oneChargeOrderRequest = new GCheckout.OrderProcessing.ChargeOrderRequest(inputOrderStateChangeNotification.googleordernumber);
    GCheckout.Util.
GCheckoutResponse oneGCheckoutResponse = oneChargeOrderRequest.Send();
  }

  // Update License If Charged
  if ((inputOrderStateChangeNotification.previousfinancialorderstate.ToString().Equals("CHARGING")) && (inputOrderStateChangeNotification.newfinancialorderstate.ToString().Equals("CHARGED")))
  {
    // TODO: For each shopping cart item received in the NewOrderNotification, authorize the license
  }

  // TODO: Add custom processing for this notification type
}

private static void HandleRiskInformationNotification(GCheckout.AutoGen.RiskInformationNotification inputRiskInformationNotification)
{
  // TODO: Add custom processing for this notification type
}

public static void ProcessNotification(string serialNumber)
{
  string googleOrderNumber = GetGoogleOrderNumber(serialNumber);

  List<string> listOfGoogleOrderNumbers = new List<string> { googleOrderNumber };

  GCheckout.OrderProcessing.NotificationHistoryRequest oneNotificationHistoryRequest = new GCheckout.OrderProcessing.NotificationHistoryRequest(listOfGoogleOrderNumbers);

  GCheckout.OrderProcessing.NotificationHistoryResponse oneNotificationHistoryResponse = (GCheckout.OrderProcessing.NotificationHistoryResponse)oneNotificationHistoryRequest.Send();

  // oneNotificationHistoryResponse.ResponseXml contains the complete response

  // Iterate through the notification history for this order looking for the notification that exactly matches the given serial number
  foreach (object oneNotification in oneNotificationHistoryResponse.NotificationResponses)
  {
    if (oneNotification.GetType().Equals(typeof(GCheckout.AutoGen.NewOrderNotification)))
    {
      GCheckout.AutoGen.
NewOrderNotification oneNewOrderNotification = (GCheckout.AutoGen.NewOrderNotification)oneNotification;
      if (oneNewOrderNotification.serialnumber.Equals(serialNumber))
      {
        HandleNewOrderNotification(oneNewOrderNotification);
      }
    }
    else if (oneNotification.GetType().Equals(typeof(GCheckout.AutoGen.OrderStateChangeNotification)))
    {
      GCheckout.AutoGen.
OrderStateChangeNotification oneOrderStateChangeNotification = (GCheckout.AutoGen.OrderStateChangeNotification)oneNotification;
      if (oneOrderStateChangeNotification.serialnumber.Equals(serialNumber))
      {
        HandleOrderStateChangeNotification(oneOrderStateChangeNotification);
      }
    }
    else if (oneNotification.GetType().Equals(typeof(GCheckout.AutoGen.RiskInformationNotification)))
    {
      GCheckout.AutoGen.
RiskInformationNotification oneRiskInformationNotification = (GCheckout.AutoGen.RiskInformationNotification)oneNotification;
      if (oneRiskInformationNotification.serialnumber.Equals(serialNumber))
      {
        HandleRiskInformationNotification(oneRiskInformationNotification);
      }
    }
    else if (oneNotification.GetType().Equals(typeof(GCheckout.AutoGen.AuthorizationAmountNotification)))
    {
      GCheckout.AutoGen.
AuthorizationAmountNotification oneAuthorizationAmountNotification = (GCheckout.AutoGen.AuthorizationAmountNotification)oneNotification;
      if (oneAuthorizationAmountNotification.serialnumber.Equals(serialNumber))
      {
        HandleAuthorizationAmountNotification(oneAuthorizationAmountNotification);
      }
    }
    else if (oneNotification.GetType().Equals(typeof(GCheckout.AutoGen.ChargeAmountNotification)))
    {
      GCheckout.AutoGen.
ChargeAmountNotification oneChargeAmountNotification = (GCheckout.AutoGen.ChargeAmountNotification)oneNotification;
      if (oneChargeAmountNotification.serialnumber.Equals(serialNumber))
      {
        HandleChargeAmountNotification(oneChargeAmountNotification);
      }
    }
    else
    {
      throw new ArgumentOutOfRangeException("Unhandled Type [" + oneNotification.GetType().ToString() + "]!; serialNumber=[" + serialNumber + "];");
    }
  }
}


Open Controllers\GcoNotifHandlerController.cs for editing:
 Replace the Index method with:

public ActionResult Index()
{
  string serialNumber = null;

  // Receive Request
  System.IO.Stream requestInputStream = Request.InputStream;
  string requestStreamAsString = null;
  using (System.IO.StreamReader oneStreamReader = new System.IO.StreamReader(requestInputStream))
  {
    requestStreamAsString = oneStreamReader.ReadToEnd();
  }

  // Parse Request to retreive serial number
  string[] requestStreamAsParts = requestStreamAsString.Split(new char[] { '=' });
  if (requestStreamAsParts.Length >= 2)
  {
    serialNumber = requestStreamAsParts[1];
  }

  // Call NotificationHistory Google Checkout API to retrieve the notification for the given serial number and process the notification
  GcoNotifHandlerForSandbox.Models.GoogleCheckoutHelper.ProcessNotification(serialNumber);

  string notifAckBegin = "<notification-acknowledgment xmlns=\"http://checkout.google.com/schema/2\"";
  string notifAckSerialNumAttrBegin = " serial-number=\"";
  string notifAckSerialNumAttrEnd = "\"";
  string notifAckEnd = " />";
  return this.Content(notifAckBegin + notifAckSerialNumAttrBegin + serialNumber + notifAckSerialNumAttrEnd + notifAckEnd);
}


That's all there is for building the basic notification handling web service.

You need to publish your notification handling web service to a public website and configure your Google Checkout sandbox account (at https://sandbox.google.com/checkout/sell/ -> Settings -> Integration -> API Callback URL) to use the URL of your web service (http://www.<yourdomain>.com/GcoNotifHandlerForSandbox/).

The Callback contents should be set to "Notification Serial Number" and the API Version should be set to "Version 2.5".


To test your notification web service, you'll need a website with a Google Checkout button.  Instructions for building that are available here:

http://code.google.com/apis/checkout/samples/Google_Checkout_Sample_Code_NET.html


(Note: I have successfully tested this with both ASP.NET MVC 1 & 2 for Visual Studio 2008.)


Good luck!

Monday, November 29, 2010 12:06:24 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [0]  | 
# Saturday, August 07, 2010

Consuming OData from Silverlight 4 can be a very frustrating experience for people like me who are just now joining the Silverlight party.

When it doesn't work, it can fail silently and/or with incorrectly worded warnings.

This is a perfect example of where a blog post can hopefully fill in some of the gaps left by the Microsoft documentation.

First, let's setup a very simple Silverlight 4 application against the odata.org Northwind service.

Note: You may need to install the Silverlight 4 Tools for VS 2010 for this sample.
 http://www.bing.com/search?q=Microsoft+Silverlight+4+Tools+for+Visual+Studio+2010

In Visual Studio 2010:

File -> New -> Project
 Visual C# -> Silverlight -> Silverlight Application -> OK
  New Silverlight Application wizard
   Uncheck "Host the Silverlight application in a new website"
   Silverlight Version: Silverlight 4
   OK

Right Click References in Solution Explorer -> Add Service Reference...
 Address: http://services.odata.org/Northwind/Northwind.svc/
 Namespace: RemoteNorthwindServiceReference
 OK

From the toolbox, drag and drop a DataGrid from the "Common Silverlight Controls" section onto MainPage.xaml
 Change the AutoGenerateColumns property to True
 Change the Margin to 0,0,0,0
 Change the Width to 400

In MainPage.xaml.cs:
 Add the following using statement:

  using System.Data.Services.Client;

 Add the following class variable:

  private DataServiceCollection<RemoteNorthwindServiceReference.Shipper> _shippers;

 Add the following to the bottom of the MainPage constructor method:

  RemoteNorthwindServiceReference.NorthwindEntities remoteNorthwindService =
    new RemoteNorthwindServiceReference.NorthwindEntities(
      new Uri("http://services.odata.org/Northwind/Northwind.svc/"));

  _shippers = new DataServiceCollection<RemoteNorthwindServiceReference.Shipper>();
  _shippers.LoadCompleted +=
new EventHandler<LoadCompletedEventArgs>(_shippers_LoadCompleted);

  var query = from shippers in remoteNorthwindService.Shippers select shippers;
  _shippers.LoadAsync(query);

 Add the following new method:

  private void _shippers_LoadCompleted(object sender, LoadCompletedEventArgs e)
  {
    if (_shippers.Continuation != null)
    {
      _shippers.LoadNextPartialSetAsync();
    }
    else
    {
      this.dataGrid1.ItemsSource = _shippers;
      this.dataGrid1.UpdateLayout();
    }
  }

Debug -> Start Without Debugging
 You should get a dialog box titled "Silverlight Project" that says:
  "The Silverlight project you are about to debug uses web services.  Calls to the web service will fail unless the Silverlight project is hosted in and launched from the same web project that contains the web services.  Do you want to debug anyway?"
 Click "Yes"

This application runs fine.  The warning dialog message was obviously inaccurate and misleading.

I wish I would have understood what this warning dialog was trying to say, but since the remote OData service was working, I had no real choice but to ignore the dialog, which would haunt me when trying to use the same application to call a local OData service.

I believe this dialog is trying to warn you that Silverlight has special constraints when calling web services, but it's still not clear to me what those are.  Proceed cautiously.

Here is one way to successfully call a local OData webservice from Silverlight 4 in Visual Studio 10:

File -> New -> Project
 Visual C# -> Silverlight -> Silverlight Application -> OK
  New Silverlight Application wizard
   OK (Accept the defaults: ASP.NET Web Application Project, Silverlight 4, etc.)

Right Click SilverlightApplication<number>.Web in Solution Explorer -> Add New Item...
 Visual C# -> Data -> ADO.NET Entity Data Model -> Add
  Entity Data Model Wizard
   Next (Generate from database - this assumes you have Northwind running locally)
   New Connection... -> Point to your local Northwind Database Server -> Next
   Select the Tables check box (to select all tables) -> Finish

Right Click SilverlightApplication<number>.Web in Solution Explorer -> Add New Item...
 Visual C# -> Web -> WCF Data Service -> Add

In WcfDataService1.cs:
 Replace " /* TODO: put your data source class name here */ " with "NorthwindEntities" (no quotes)
 Add the following line to the InitializeService method:

  config.SetEntitySetAccessRule("*", EntitySetRights.AllRead);

Debug -> Start Without Debugging

Right Click References under SilverlightApplication<number> in Solution Explorer -> Add Service Reference...
 Discover -> Services In Solution (note the port number of the discovered service, you will need it below)
 Namespace: LocalNorthwindServiceReference
 OK

Stop the browser that was started above

Follow the same basic steps as the first example above to modify MainPage except:
 Replace RemoteNorthwindServiceReference with LocalNorthwindServiceReference
 Replace "http://services.odata.org/Northwind/Northwind.svc/" with "http://localhost:<port number noted above>/WcfDataService1.svc/"

Debug -> Start Without Debugging

This application runs fine (without the warning dialog this time).

It can be ridiculously difficult to get a local OData service working with Silverlight 4 if you don't carefully dance around the project setup issues.
The warning dialog when you start debugging is nearly useless and the app will return no data with no apparent errors if you setup the project incorrectly.

Some references I found useful while building this sample:

 MSDN: How to: Create the Northwind Data Service (WCF Data Services/Silverlight)
 http://msdn.microsoft.com/en-us/library/cc838239(VS.95).aspx

 Audrey PETIT's blog: Use OData data with WCF Data Services and Silverlight 4
 http://msmvps.com/blogs/audrey/archive/2010/06/10/odata-use-odata-data-with-wcf-data-services-and-silverlight-4.aspx

 Darrel Miller's Bizcoder blog: World’s simplest OData service
 http://www.bizcoder.com/index.php/2010/03/26/worlds-simplest-odata-service/

Once you deploy your OData service to a real web hosting environment, you'll likely need to do some special setup to access your OData service from Silverlight 4 (clientaccesspolicy.xml / crossdomain.xml):

 Making a Service Available Across Domain Boundaries
 http://msdn.microsoft.com/en-us/library/cc197955(VS.95).aspx

Tremendous thanks go out to Scott Davis of Ignition Point Solutions for pointing me to the flawed project setup as the reason I couldn't get this working initially.

Saturday, August 07, 2010 12:49:17 PM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [1]  | 
# Wednesday, June 24, 2009

First, create the application setup described in my WCF - Establishing a sample app baseline blog entry.

We will then modify the client program so that it encounters some errors while communicating with the WCF Service.

Next, add a new method to ConsoleApp1:

        private static string GenerateStringOfCertainLength(int stringLength)
        {
            string returnValue = String.Empty;
            int numTens = stringLength / 10;
            int remainder = stringLength % 10;

            for (int counter = 0; counter < numTens; counter++)
            {
                returnValue += "0123456789";
            }
            for (int counter = 0; counter < remainder; counter++)
            {
                returnValue += counter.ToString();
            }

            return returnValue;
        }

Replace the contents of the try block in ConsoleApp1 with:

                ServiceReference1.Service1Client oneService1Client = new ServiceReference1.Service1Client();
                ServiceReference1.CompositeType oneCompositeType = new ServiceReference1.CompositeType();
                oneCompositeType.StringValue = GenerateStringOfCertainLength(8193);
                oneCompositeType = oneService1Client.GetDataUsingDataContract(oneCompositeType);

Ctrl-F5 (Debug -> Start Without Debugging)

This may crash Cassini (the VS Web Server) and will result in the following exception:

--
oneException=[System.ServiceModel.FaultException: The formatter threw an exception while trying to deserialize the message: There was an error while trying to deserialize parameter http://tempuri.org/:composite. The InnerException message was 'There was an error deserializing the object of type WcfApp1.CompositeType. The maximum string content length quota (8192) has been exceeded while reading XML data. This quota may be increased by changing the MaxStringContentLength property on the XmlDictionaryReaderQuotas object used when creating the XML reader. Line 1, position 8652.'.  Please see InnerException for more details.

Server stack trace:
   at System.ServiceModel.Channels.ServiceChannel.HandleReply(ProxyOperationRuntime operation, ProxyRpc& rpc)
   at System.ServiceModel.Channels.ServiceChannel.Call(String action, Boolean oneway, ProxyOperationRuntime operation, Object[] ins, Object[] outs, TimeSpan timeout)
   at System.ServiceModel.Channels.ServiceChannel.Call(String action, Boolean oneway, ProxyOperationRuntime operation, Object[] ins, Object[] outs)
   at System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation)
   at System.ServiceModel.Channels.ServiceChannelProxy.Invoke(IMessage message)

Exception rethrown at [0]:
   at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)
   at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
   at ConsoleApp1.ServiceReference1.IService1.GetDataUsingDataContract(CompositeType composite)
   at ConsoleApp1.ServiceReference1.Service1Client.GetDataUsingDataContract(CompositeType composite) in C:\dev\Prototyping\WCF\WcfApp1\ConsoleApp1\Service References\ServiceReference1\Reference.cs:line 120
   at ConsoleApp1.Program.Main(String[] args) in C:\dev\Prototyping\WCF\WcfApp1\ConsoleApp1\Program.cs:line 17]
--


If we send 50000 instead of 8193, we get a different exception:

--
oneException=[System.ServiceModel.ProtocolException: The remote server returned an unexpected response: (400) Bad Request. ---> System.Net.WebException: The remote server returned an error: (400) Bad Request.
   at System.Net.HttpWebRequest.GetResponse()
   at System.ServiceModel.Channels.HttpChannelFactory.HttpRequestChannel.HttpChannelRequest.WaitForReply(TimeSpan timeout)
   --- End of inner exception stack trace ---

Server stack trace:
   at System.ServiceModel.Channels.HttpChannelUtilities.ValidateRequestReplyResponse(HttpWebRequest request, HttpWebResponse response, HttpChannelFactory factory, WebException responseException)
   at System.ServiceModel.Channels.HttpChannelFactory.HttpRequestChannel.HttpChannelRequest.WaitForReply(TimeSpan timeout)
   at System.ServiceModel.Channels.RequestChannel.Request(Message message, TimeSpan timeout)
   at System.ServiceModel.Channels.ClientReliableChannelBinder`1.RequestClientReliableChannelBinder`1.OnRequest(TRequestChannel channel, Message message, TimeSpan timeout, MaskingMode maskingMode)
   at System.ServiceModel.Channels.ClientReliableChannelBinder`1.Request(Message message, TimeSpan timeout, MaskingMode maskingMode)
   at System.ServiceModel.Channels.ClientReliableChannelBinder`1.Request(Message message, TimeSpan timeout)
   at System.ServiceModel.Security.SecuritySessionClientSettings`1.SecurityRequestSessionChannel.Request(Message message, TimeSpan timeout)
   at System.ServiceModel.Dispatcher.RequestChannelBinder.Request(Message message, TimeSpan timeout)
   at System.ServiceModel.Channels.ServiceChannel.Call(String action, Boolean oneway, ProxyOperationRuntime operation, Object[] ins, Object[] outs, TimeSpan timeout)
   at System.ServiceModel.Channels.ServiceChannel.Call(String action, Boolean oneway, ProxyOperationRuntime operation, Object[] ins, Object[] outs)
   at System.ServiceModel.Channels.ServiceChannelProxy.InvokeService(IMethodCallMessage methodCall, ProxyOperationRuntime operation)
   at System.ServiceModel.Channels.ServiceChannelProxy.Invoke(IMessage message)

Exception rethrown at [0]:
   at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)
   at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
   at ConsoleApp1.ServiceReference1.IService1.GetDataUsingDataContract(CompositeType composite)
   at ConsoleApp1.ServiceReference1.Service1Client.GetDataUsingDataContract(CompositeType composite) in C:\dev\Prototyping\WCF\WcfApp1\ConsoleApp1\Service References\ServiceReference1\Reference.cs:line 120
   at ConsoleApp1.Program.Main(String[] args) in C:\dev\Prototyping\WCF\WcfApp1\ConsoleApp1\Program.cs:line 17]
--


The out of the box wizard generated service has a couple of limitations in string lengths that it will support.

These are essentially client side errors and can't be usefully debugged on the server side or using tools like Fiddler.

Let's go back to the 8193 character string and solve that issue first.  We can do this by modifying the web.config for the WCF web service and the app.config for the console client application.

There is a tool in Visual Studio that gives us a property editor for WCF configuration.  You may or may not find it preferable to use the tool over modifying the xml files directly, but that is how I am going to describe the changes we need to make.

If you right click on the web.config in the Solution Explorer and don't see a menu option called "Edit WCF Configuration", do the following to get it added:

  In Visual Studio 2008, go to the tools menu, then select "WCF Service Configuration Editor"
    File -> Exit

Now, right click web.config in Solution Explorer -> Edit WCF Configuration

  Select Bindings -> New Binding Configuration...
    Please select a binding type: wsHttpBinding
  Select Bindings -> NewBinding0 (wsHttpBinding)
    Set Configuration -> Name to: CustomWsHttpBindingConfiguration
    Set ReaderQuotas Properties -> MaxStringContentLength to: 9000

  Select Services -> WcfApp1.Service1 -> Endpoints -> (Empty Name) [the first one - not mexHttpBinding]
    Select Endpoint Properties -> Binding Configuration: CustomWsHttpBindingConfiguration

  File -> Save
  File -> Exit

Any time we update the web.config file, we should update the service reference, even though in this case it is likely unnecessary:

  Solution Explorer
    Right Click ConsoleApp1 -> Service References -> ServiceReference1
      Update Service Reference
      OK

We need to make the same change on the client side as we made on the server side:

  Right Click app.config in Solution Explorer -> Edit WCF Configuration
    Select Bindings -> WSHttpBinding_IService1 (wsHttpBinding)
      Set ReaderQuotas Properties -> MaxStringContentLength to: 9000
    Save & Exit


If we run the application now using 8193, the exception is gone.  So, it is now clear how to send strings larger than 8192 characters to a WCF web service (the exception helped point us in the right direction).

Now, let's go back to the 50000 scenario.  The above changes are not enough to fix it as we are now exceeding a different character limit.

Right click web.config in Solution Explorer -> Edit WCF Configuration
  Select Bindings -> CustomWsHttpBindingConfiguration (wsHttpBinding)
    Set General -> MaxReceivedMessageSize to: 75000
    Set ReaderQuotas Properties -> MaxStringContentLength to: 50000
  Save & Exit

Right Click app.config in Solution Explorer -> Edit WCF Configuration
  Select Bindings -> WSHttpBinding_IService1 (wsHttpBinding)
    Set General -> MaxReceivedMessageSize to: 75000
    Set ReaderQuotas Properties -> MaxStringContentLength to: 50000
  Save & Exit

Again, just for best practice reasons, we update the service reference:

  Solution Explorer
    Right Click ConsoleApp1 -> Service References -> ServiceReference1
      Update Service Reference
      OK

How did I come up with those numbers?  Trial and error.  The value for MaxStringContentLength logically matches the length of the string we are trying to send.  MaxReceivedMessageSize seems to include extra characters, but it's not clear to me what exactly those extra characters are.

If you increase the values of these two config settings on the server, but not the client, the client starts giving useful error messages (like: The maximum message size quota for incoming messages (70000) has been exceeded.) instead of the mostly useless "(400) Bad Request." which basically means that the server rejected your request and is pretty much unwilling to tell you why.

Please note that increasing the size of these config settings makes your web service more vulnerable to denial of service attacks.

It's pretty easy to exceed the wizard generated values however if you are, for example, trying to send the contents of a text file as a string parameter to a WCF method.

So, now you know how to configure WCF to allow sending larger strings back and forth.

Wednesday, June 24, 2009 2:49:23 PM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [1]  | 
# Tuesday, June 09, 2009

First, create the application setup described in my WCF - Establishing a sample app baseline blog entry.
We will then modify that simple application so that it's traffic will show up in Fiddler2.

Second, install and startup Fiddler.  It is probably worth watching the QuickStart video if you haven't already.
After you install and run Fiddler, and then run ConsoleApp1, you'll notice that the WCF traffic doesn't show up in Fiddler.  There are multiple ways to enable that, but we are only going to cover one simple one right now.

Third, modify the client application so that Fiddler will display it's traffic.

After this line of code in ConsoleApp1 Program.cs Main:

ServiceReference1.Service1Client oneService1Client = new ServiceReference1.Service1Client();

Add:
                oneService1Client.Endpoint.Address = new System.ServiceModel.EndpointAddress(
                    new Uri(oneService1Client.Endpoint.Address.Uri.ToString().Replace("localhost", "127.0.0.1.")),
                    oneService1Client.Endpoint.Address.Identity,
                    oneService1Client.Endpoint.Address.Headers);

This essentially pushes all the network traffic into a place where Fiddler can see it (the period after the 1 is critical, it's not a typo!).

Ctrl-F5 (Debug -> Start Without Debugging)

You should now see traffic showing up in Fiddler.

If you shut down Fiddler, the application will still work fine (but if you used "ipv4.fiddler" instead of "127.0.0.1.", you would get an exception like: [EndpointNotFoundException: There was no endpoint listening at http://ipv4.fiddler:1100/Service1.svc that could accept the message.]).

At this point, for readability/testability purposes, I would encourage you to modify the web.config for the WCF service and replace the wsHttpBinding with the basicHttpBinding.  This will make it much more clear what is going on.

If you modify the WCF web.config, you have to update the reference in the client (which will generally regenerate the client's app.config to match):

Solution Explorer
 Right Click ConsoleApp1 -> Service References -> ServiceReference1 -> Update Service Reference

(This is basically needed anytime the WCF application changes it's public interface.)

Tuesday, June 09, 2009 7:53:45 PM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [0]  | 

We are going to run the WCF application wizard and then create a client program that establishes basic communication with that WCF Service.

We will be using Visual Studio 2008 with Service Pack 1

File -> New -> Project
 Visual C# -> Web -> WCF Service Application
  WcfApp1 -> OK
Solution Explorer
 Right Click Solution -> Add -> New Project
  Visual C# -> Windows -> Console Application
   ConsoleApp1 -> OK

Solution Explorer
 Right Click ConsoleApp1 -> References -> Add Service Reference...
  Discover
  OK

If adding the service reference fails, try to run WcfApp1 (Debug -> Start Without Debugging) and click on "Service1.svc" first.  For whatever reason, the WCF service doesn't always "auto-start" when it should.  I end up using this "workaround" quite often.

Add the following to the Main method in Program.cs in ConsoleApp1:

            try
            {
                ServiceReference1.Service1Client oneService1Client = new ServiceReference1.Service1Client();
                string serviceOutput = oneService1Client.GetData(-1);
                Console.WriteLine("serviceOutput=[" + serviceOutput + "]");
            }
            catch (Exception oneException)
            {
                Console.WriteLine("oneException=[" + oneException.ToString() + "]");
                Environment.Exit(-1);
            }


Solution Explorer
 Right Click ConsoleApp1 -> Set as StartUp Project

Ctrl-F5 (Debug -> Start Without Debugging)

Program output should be:

--
serviceOutput=[You entered: -1]
Press any key to continue . . .
--

We now have a basic WCF application with a basic client that calls it.

One very helpful tip once the basics are running is to get Fiddler up and running to be able to watch WCF traffic in case things go downhill after you make some changes.

Click here for instructions on how to do that.

Tuesday, June 09, 2009 7:46:17 PM (GMT Daylight Time, UTC+01:00)  #    Disclaimer  |  Comments [0]  | 
# Wednesday, November 09, 2005

One of my personal pet programming projects at one point in time was to create some kind of screen scraper of monster.com and other job websites so I could "download" all the current job postings and do things like:

1) Rank/Prioritize the job listings by personal appeal
2) Detect job listing duplicates between monster/dice/etc.
3) Keep track of which jobs I applied for and when
4) Automate running multiple queries with multiple terms in job site specific ways so I could detect new entries in my areas of interest (something an RSS feed would normally be used for)
5) Categorize jobs by factors that were important to me - Salary, Location, Consulting vs. Full-time, How qualified I thought I was, etc.

Everyone puts that much effort into their job search, right?  ;)

I did get something of an automated process for the worst bits with tons of manual intervention required such that I could do this and it worked wonderfully.  It was never anywhere close to automated enough to share with friends though.  However, screen scrapping different web sites is not fun, rewarding work and I eventually shelved the project (and I now find most of my contract work through recruiters who I've talked to in the past and never apply for jobs from web site job listings anymore).

I still think this is very cool though:

http://www.indeed.com/jsp/apiinfo.jsp

Indeed is yet another job search engine, except with one major difference (it may not be the only job search engine with this feature, but it's the only one I know of).  Indeed publishes a Web Services API which you can query directly that seems to return job listings from Monster, Dice, etc.  This would have made my pet project above a piece of cake to implement.  This is a very good sign of things to come in the web services world!

Now if someone would just publish a web service for stock price quotes (you know it's going to happen eventually).

Wednesday, November 09, 2005 2:13:21 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer  |  Comments [2]  |