WCF With GP Web Services

by Jacob 15. March 2009 12:29

I’m at Convergence this week in New Orleans. If you’re unfamiliar with the conference (and don’t want to follow the nifty link), all you really need to know is that it’s Microsoft’s convention for their business solutions products. For me, that means Dynamics Great Plains.

I bring this up because in the last session I attended yesterday, Louis Maresca mentioned a problem I remembered having with GP Web Services. GP WS has a serious problem when you first instantiate the proxy object: it can take seconds (over 30 on our older systems—I put a timer in just to verify) to instantiate the web service proxy. The reason for this is that .Net queries the service to pull down the available methods and objects on instantiation. Since there are very many of them available in GP Web Services, the query and xml serialization adds up to quite a lot.

Now, his solution was very clever, but involved creating a new web service to slim down the contract retrieval. My solution was to saddle up and use WCF. You see, WCF doesn’t do silly things like query for contracts it already knows full well about. I cracked how to use WCF with GP Web Services about a year ago and I haven’t looked back since.

In that session last night I realized that others might want to know what it took to get it working (and thus a blog post was born…) I’m not going to go though creating the WCF bit. It’s pretty straight forward and explained all over.

Crap, I find I can’t actually proceed without at least giving an overview.

  1. Right-click your project.
  2. Select “Add Service Reference”.
  3. Fill out the dialog:

GPWSServiceReference

Okay, now that I got that out of my system there are two things that prevent WCF and GP Web Services from playing nice together.

Security

Since GP WS uses your windows identity to validate things like roles and permissions, your client needs to send the correct identity or “bad things can happen”™. In VS 2005 web services, this was a simple matter of setting .UseDefaultCredentials to true. In WCF it’s a good bit more complicated. It’s a mirror of remotely printing Reporting Services using WCF, though, so techniques used there are applicable (though slightly different).

First, you have to let the binding know the correct security mode and transport. I did this in a basicHttpBinding in the <security> section:

<security mode="TransportCredentialOnly">
  <transport clientCredentialType="Ntlm" />
</security>

I came at this setting obliquely and after much trial and error. I’m not sure why clientCredentialType=“Windows” didn’t work against GP WS when it worked with Reporting Services. Probably something quirky in our environment.

This alone is not enough, however. The binding setting is just the contract. To actually use the correct credentials, your proxy has to be told what to do. Not difficult, but easy to overlook when you’re coming from a 2005 web services background. Here’s all it takes:

DynamicsGPSoapClient service = new DynamicsGPSoapClient();
service.ClientCredentials.Windows.AllowedImpersonationLevel 
    = TokenImpersonationLevel.Impersonation;

Once that’s all taken care of, you’re set to go. Those two lines of code are processed pretty much instantaneously on even our slowest clients, so problem solved. Almost.

Errors

Error handling hung me up for a while and was the final hurdle to being able to truly implement WCF with GP WS. I was so excited when I finally figured it out that I blogged it at the time. The key point is that GP WS wants to check a user’s authorization to view errors before giving up the details of what happened so you have to hit the web service again for details. Thus, while the status message is informative, you only get a GUID for detail in the initial error. This is not a bad thing, but it leads to difficulties when putting together your excuses to the user—particularly when WCF doesn’t make it easy to get at the details of an untyped FaultException.

Simple as That

From here, everything is pretty much the same. You have your objects in the domain you specified in the “Add Service Reference” dialog given above (GPService in my screenshot). Your proxy object has the methods you can use.

Tags: , , , , ,

Programming

Getting at the Details

by Jacob 28. January 2008 18:56

wcf This should be pretty short, but I could not find this information anywhere so here’s something that will save you hours of frustration if you ever run into the same situation.

WCF Myopia

Windows Communication Foundation is nice and all, but it suffers from a really large myopia: it tends to assume that you control both the service and the client. This is a stupid assumption, but try finding information for a situation where a service throws a FaultException and you want to get at the details of the exception and you’ll see what I mean.

The Setup

Dynamics Great Plains Web Services allow you to manipulate common business objects. I’ve recently been trying to use WCF in communicating with those services and it has been an adventure. A recent post detailed my solution for customized warehousing. What I left out last time is moving to WCF’s error handling seriously messed up my error condition reporting.

GP Web Services sends a fault for both warnings and errors with a relatively informative status message and a detail section that is just a GUID that identifies the fault in the server log. The theory is that if you want more details, you can query the service again to pull further information in.

The Problem

All the solutions I could find for getting fault details out of WCF involve creating contracts on the service that the WCF client can read to create strongly typed objects in the generic version of FaultException. Well, I can’t control the web service to do this. I could probably have hacked or wrapped the wsdl for the service but who wants that much low-level headache? And before you go there, the faults that the GP Web Service sends didn’t trigger on what was billed as the default exception with generic details:

catch (FaultException<ExceptionDetail> ex)
{
}

Unfortunately, the regular FaultException doesn’t expose a Detail property or methods that you can use to get at any details sent. This is a stupid oversight. I mean, yeah, anything can be in there, but whatever is there has to be XML serializable pretty much by definition. They couldn’t throw us a bone and expose it as a string or XML fragment?

The Solution

So what do you do with WCF if you cannot change the service but need to get at a fault detail without a specific .Net-friendly contract?

The solution I came up with is actually extremely easy once I realized I had to go through a couple of classes to get there (don’t ask how long this took to ferret out).

  • Snag the message itself using MessageFault.
  • Get an XmlDictionaryReader that can get at the contents of the details sent.
  • Read the detail content.

Since I know that the detail content is always a GUID, this is really simple:

catch (FaultException soapEx)
{
    MessageFault mf = soapEx.CreateMessageFault();
    if (mf.HasDetail)
    {
        XmlDictionaryReader reader = mf.GetReaderAtDetailContents();
        Guid g = reader.ReadContentAsGuid();
    }
}

Amazingly, this works.

Tags: , , , ,

Programming

Custom Dynamics Warehousing

by Jacob 4. January 2008 03:25

Warehouse Prior developers and others who should have known better at my company decided many years ago that our warehouse people simply could not do their jobs unless we unleashed the ability for them to use multiple "bin" locations for each item. Now, strictly speaking, this was not really the case, because our needs simply aren’t that complex. We manufacture reading glasses so we don’t need inventory aging and all the warehouse people really needed were different sites to separate receiving from QA from shipping. In other words, we have product staging, but each stage is physically as well as conceptually separate and each item in each stage exists really only in a single location.

As in so many real-life situations, however, there is enough cruft built up around the current process that it is simply not worth expending the political capital to change. Which means that I’m stuck trying to automate order fulfillment for multiple bins with multiple locations.

The Tools

Now, Dynamics has a number of interesting tools and my company long ago purchased all the neat bits that I need to use them all. The typical default tool that people use to customize Dynamics is a hoary old beast called "Dexterity". I’m sure that the name once fit, but in an honest world the name would have long ago been changed to "Decrepit". I’ll use Dexterity if I absolutely have to, but I’ll avoid it more assiduously than, say, rabid ninja porcupines.

Fortunately, Microsoft, feeling their oats with cool technologies like the .Net framework and web services and all, has released developer tools that includes a .Net toolset to create addins and a set of web services that can be used to perform most business tasks. I also have the Database schema if I have to resort to direct table access, but I tend to avoid that if I can (though not at the rabid ninja porcupine level).

There are some important things to know about these tools.

.Net Integration

The .Net integration can hook into Dynamics forms and events and play with field values. You can also create your own forms and hook them into a special "Extras" menu when a given Dynamics form is active (or have them pop up during an existing event handler I suppose).

They didn’t skimp on the event handles, either. Each field has before and after change and click events, buttons have before and after click events, and forms and windows have activate, open, close, and print (all with before and after handles).

Web Services

The web services are more complicated to set up and are best off installed on your database server. They use some of the older E-Connect com objects in the background which makes it a pain to set up, but they’re pretty useful once you do.

The web services expose a large number of create, update, get, and delete methods for all the stuff you’d expect—customers, invoices, sales orders, purchase orders, that kind of thing. They can be a bit temperamental when it comes to setting up the policies for the operations, but once you’ve got them working, they work well.

The Problem

When we receive orders from Nordstrom, we have two factors that complicate our work flow. The main difficulty is that printing our shipping and invoice labels is a fiddly piece of work that can result in fines from Nordstrom if we get it wrong. Some long forgotten "genius" managed to get them formatted correctly using our EDI software. This is the software that is still using FoxPro as their interface and data storage. It’s a hunk of junk, but it works. More or less.

Unfortunately, for those labels to print correctly, the EDI software has to do the Dynamics import itself (instead of using my cool new EDI processing service). If the EDI software doesn’t do the import, it doesn’t know all the right invoice ids and associated stuff to print on those labels.

Also unfortunately, the EDI program knows nothing about our multiple bin fulfillment requirements. Adding to the pain is the fact that Dynamics cannot handle automated fulfillment for multiple bins on its own—particularly when certain sites are to be allowed, others avoided, and one or two bins excluded manually (yeah, we did all that to ourselves).

Since we get orders for hundreds of items at a time, this largely manual process for Nordstrom is taking a single very kind and patient person (who may or may not ever read this) about four hours. The bulk of this time is manually assigning bins to all the items on each order in the batch.

My Solution

We’ve been working on making all of our customizations accessible within Dynamics, so it’s best if an automation solution kicks off from there. The .Net tools make this part relatively easy.

public class OrderFulfillmentAddIn : IDexterityAddIn
{
    // IDexterityAddIn interface
 
    public void Initialize()
    {
        Dynamics.Forms.SopBatchEntry.AddMenuHandler(AddOrderFulfillmentMenu, "Allocate Bins");
    }
 
 
    static FulfillmentForm form = new FulfillmentForm();
    static void AddOrderFulfillmentMenu(object sender, EventArgs e)
    {
        if (form == null || form.Created == false)
        {
            form = new FulfillmentForm();
        }
 
        form.BatchId = Dynamics.Forms.SopBatchEntry.SopBatchEntry.BatchNumber.Value;
        form.Show();
        form.Activate();
    }
}

This inserts an "Allocate Bins" menu item on the extras menu when the Batch Entry form is open. The menu item opens my own form (which isn’t any great shakes). However, because the .Net integration tools are only able to handle user-initiated events and already open form fields actual bin allocation has to happen some other way.

Yeah, I chose to use the web services.

Complicating this further, the web services don’t expose anything that will give me all the bins that contain a specific item so I’ll need to pull that information from the tables directly. Fortunately, Dynamics has a handy Resource Descriptions tool that helps here.

Dynamics Resources 

It can take getting used to, but it tells me that I need table IV00112.

ItemSiteBinMaster Table

With that information, this becomes a simple Linq query.

You Want Tests With That?

Now, this is where things get tricky because I like to test my code once or twice before deployment. Oh yeah, and sometimes I’ve even been known to create unit tests and such. Which means that I have to be able to run this in both our test company as well as the live one.

Since Dynamics keeps each company in a separate database, this means that I need to know both the Company Id to feed the web service and the database name for the Linq query. This actually held me up for quite a while. My first impulse (that actually worked out relatively well) was to put both into the app config.

Linq already has it’s connection string there so I’d just need to add a CompanyId setting.

<connectionStrings>
  <add name="Integration.GPWebServices.Properties.Settings.myConnectionString"
      connectionString="Data Source=MyServer;Initial Catalog=MyDB;Integrated Security=True"
      providerName="System.Data.SqlClient" />
</connectionStrings>
<applicationSettings>
  <FulfillOrders.Properties.Settings>
    <setting name="CompanyId" serializeAs="String">
      <value>2</value>
    </setting>
  </FulfillOrders.Properties.Settings>

This worked well on the surface, but it was kind of clunky when it came to developer testing where I went into the test company within Dynamics and created batches and orders with the Visual Studio debugger attached.

That’s when the apple fell out of the sky and I realized that, duh, I have the Dynamics environment right here already. It took me a while to figure out that the database name is held in the "IntercompanyId" global property, but once I’d tracked that down, we were golden.

Orders.FulfillByBatch(BatchId
    , (int)Dynamics.Globals.CompanyId.Value
    , Dynamics.Globals.IntercompanyId.Value);

The unit test can still pull it from the config file, but my form can simply use the company currently open.

Doing the Work

I’ll give you my worker class just for kicks here. Note that allocating bins for hundreds of items takes long enough that we definitely need UI feedback (we love thee oh progress bar). That means that much of the class real estate is taken up with events that help keep my users from freaking out.

using System;
using System.Collections.Generic;
using System.Text;
using Integration.GPWebServices;
 
namespace FulfillOrders
{
    public static class Orders
    {
        public static event EventHandler OrderProcessed;
        public static event EventHandler<BeginFulfillingEventArgs> BeginFulfillment;
        public static void FulfillByBatch(string OriginalBatchId, string NewBatchId, DateTime CancelDate, int CompanyId, string CompanyDb)
        {
            Utility.DataBase = CompanyDb;
            Utility.CompanyId = CompanyId;
            SalesDocumentSummary.BeginLoadingSummary += new EventHandler<BeginSummaryLoadEventArgs>(SalesDocumentSummary_BeginLoadingSummary);
            SalesDocumentSummary.DocumentLoaded += new EventHandler(SalesDocumentSummary_DocumentLoaded);
            List<SalesDocument> orders = SalesDocumentSummary.GetSalesDocumentsByBatch(OriginalBatchId);
 
            if (orders.Count > 0)
            {
                OnBeginFulfillment(orders.Count);
                foreach (SalesDocument order in orders)
                {
                    order.BatchId = NewBatchId;
                    order.Date = order.UserDate1 = order.UserDate2 = order.ReqShipDate = CancelDate.Date;
                    order.AllocateBins();
                    order.CommitDocument();
                    OnOrderProcessed();
                }
            }
        }
 
        static void SalesDocumentSummary_DocumentLoaded(object sender, EventArgs e)
        {
            OnOrderProcessed();
        }
 
        static void SalesDocumentSummary_BeginLoadingSummary(object sender, BeginSummaryLoadEventArgs e)
        {
            OnBeginFulfillment(e.TotalOrders);
        }
 
        private static void OnBeginFulfillment(int total)
        {
            EventHandler<BeginFulfillingEventArgs> begin = BeginFulfillment;
            if (begin != null)
            {
                begin(null, new BeginFulfillingEventArgs(total));
            }
        }
 
        private static void OnOrderProcessed()
        {
            EventHandler processed = OrderProcessed;
            if (processed != null)
            {
                processed(null, new EventArgs());
            }
        }
    }
    public class BeginFulfillingEventArgs : EventArgs
    {
        internal BeginFulfillingEventArgs(int total)
        {
            Total = total;
        }
        public int Total { get; set; }
    }
}

Wrapping it Up

Integration.GPWebServices is my own library and it uses shell classes to simplify working with the GP web service objects (including the static Utility class used to store the Company Id and Database).

Wrapper Classes

Enlarge to see it in full, including the nifty methods used to put the web service objects together.

Isolating the web services also means that the WCF objects are fully encapsulated and I don’t have to worry about referencing those libraries or be concerned when they cross app boundaries. It does mean that I have to be careful with the app.config files that contain my WCF endpoint connectivity, however.

Integration.GPWebServices probably deserves it’s own post. It’s certainly large enough and it is getting relatively well polished at this point. Also, it has become great personal example of the benefits of ruthless refactoring in a case where the initial design was solid to begin with.

Tags: , , , , ,

Programming

scruffylookingcatherder.com

Information

    Recent Posts

    Calendar

    <<  September 2010  >>
    MoTuWeThFrSaSu
    303112345
    6789101112
    13141516171819
    20212223242526
    27282930123
    45678910

    View posts in large calendar
    Disclaimer
    The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

    © Copyright 2010 Scruffy-looking Cat Herder