Zen and the Art of ServerObjectExtensions
Posted by Dave Bouwman | Posted in .NET, ASP.NET MVC, ArcGIS Server, ESRI, Visual Studio 2008 | Posted on 14-12-2009
4
I was going to cook up a full SOE example, but that was going to take way more time that I don’t have, so here’s the abridged version – just the major points…
Projects in the Solution
SOE’s have a lot of moving parts, so let’s think about all the parts in terms of servers & assemblies.
First – the SOE project. We’ll create the actual SOE’s in here. We can have references to all the ArcObjects / ADF goop we want in here, since this will of course be running on the SOC machine.
In the “client” application (ASP.NET MVC in my case) we’ll use an interface to access the SOE. Since we’ll want the interface definition on both the SOC machine(s) and the “client” machine, we put this in it’s own assembly. Remember that anything that moves between the SOE and the “client” is really doing DCOM under the covers, so all types need to be serializable.
Next up is the Utility assembly. We have a mess of static classes in here that are pure ArcObjects. This assembly will be used on the ArcGIS SOC box, by the SOEs. An upside of keeping this code “pure” ArcObjects (by that I mean there is no ServerObject cruft in here) we can use these same utility classes in ArcEngine or Desktop development.
Finally, we’ll want that “client” application I’ve be talking about. I’m a big fan of ASP.NET MVC, so I’ve been using that to host services. It’s worth noting that you can put standard ASMX SOAP web services into an MVC project. This is nice because you can expose your SOE methods as SOAP or JSON from the same project. Remember, this project will be using the ESRI ADF library to create the connection into the SOC and get access to the SOE, so it will need to run on a box that has these libraries accessible. Note that at 9.4 the ADF components are re-distributable with out incurring additional licensing.
Building
The build process can be a little hairy. First off, make sure you have ArcGIS Server on your development box – this way your build/test cycle will be manageable. If you need to copy dll’s to another server as part of your debugging process you will lose your mind.
So – we need to register all the COM assemblies – that would be the SOE, the SOEInterface, and the COMUtility assemblies at minimum. While you *CAN* tell visual studio to register these assemblies for COM interop, DON’T! That will register the assembles in \bin\debug.
A better plan is to copy these dll’s to a separate folder, and install them there. We call ours _install_ComUtilities. We have a post-build event that copies the SOE and it’s dependencies into that folder, and does all the dirty work. Jeff Germain just threw up a good post on a Post-Build Event that does all this, but here’s the basic process:
- Stop the ArcGIS SOM Process (SC STOP ArcServerObjectManager)
- Un-register the dll’s (regasm /unregister…)
- delete the type libraries (*.tlb)
- delete the dlls
- copy the new dll’s from the build output folder into _install_ComUtilities
- run regasm on them to create the type libraries
- re-start the ArcGIS SOM process (SC START ArcServerObjectManager)
Now, of course you don’t want to do this every time your solution builds, as it takes ~15 seconds, so use the visual studio configuration manager to not build the SOE project automatically.
Registering the SOE with the Map Service
Get Vish’s SOExplorer app, and use that. You can do this manually or write some code to do it, but just use Vish’s tool. It rocks. Moving on…
Logging
Since you’ll likely have a bug or two in your code, having some logging is really important. We use Log4Net on the client side, but have never gotten this to work smoothly in COM land. Again, Jeff has saved the day with a ComLogger utility that he whipped up for this. I’ve asked him to post about this, but he has not gotten to it yet. If you want this, go to his blog and hassle him.
The upside is that when debugging the app, I’ve got two logs to watch – the log on the client (MVC) and from the SOC (via ComLogger). The ComLogger simply creates a \log folder next to the SOE dll – so you have to watch for permissions on this – make sure that the ArcSOC process has write permission to the folder where you install your dll’s!
Here’s an example of what I get in my SOE Log:
12/14/2009 3:11:37 PM INFO LrsSOE::GetRoute LrsSOE::GetRoute called.
12/14/2009 3:11:37 PM INFO LrsSOE::GetRoute params: routeId: 002B
12/14/2009 3:11:37 PM INFO LrsSOE::GetRoute whereclause: Route = ‘002B’
What shows up in here is up to you, but make sure you stuff all exceptions into the log because that’s where this will save you a ton of time. If you don’t you’ll just get an InteropException on the client side, and that’s completely useless. Also worth mentioning, I use NotePad++ and just keep these two files open in it. When they change, and you change focus to Notepad++, it will ask if you want to refresh the files. Vish is a big fan of BareTail, but from what we’ve see, it’s got wacky behavior on multi-monitor setups. Moving on…
SOAP and JSON in MVC
As noted above I needed to provide services to different clients – one is a Flex app where JSON is convenient, and the other is another MVC app that can speak SOAP fluently. To do this, I just created a standard MVC project, and created a /SOAP folder, inside of which I created a standard asmx web service. By default MVC maps all urls to routes, and so /SOAP would normally route to a class called SOAPController. However, we don’t want that behavior, so we tell MVC to ignore anything under /SOAP (this is in global.asax.cs)
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.IgnoreRoute("SOAP/{*pathInfo}"); routes.MapRoute( "Default", "{controller}/{action}/{id}", new { controller = "Home", action = "Index", id = "" } ); }
For the Json stuff, I setup a JsonController and thus we have routes like http://server/json/getroute?routeId=25b, which of course maps to a the GetRoute method on the JsonController, passing in a routeId of 25b. The logic in the JsonController is pretty simple – we just pass the arguments over to the SOE, and return the response.
Getting the SOE
Getting the SOE itself is a two step process. First you need to get a server context – ideally one that has your SOE registered with it, and then get the SOE from that. We’ve written a lot of these things, so we have lots of handy utilities to handle configuration sections, Identity etc etc. But it boils down to two methods:
public static IServerContext GetServerContext(string arcGISServerName, string serviceName, string serviceType, ESRI.ArcGIS.ADF.Identity identity) { //Connect to ags ESRI.ArcGIS.ADF.Connection.AGS.AGSServerConnection agsConnection = new ESRI.ArcGIS.ADF.Connection.AGS.AGSServerConnection(arcGISServerName, identity); agsConnection.Connect(); if (!agsConnection.IsConnected) { agsConnection.Dispose(); return null; } //Get the som IServerObjectManager som = agsConnection.ServerObjectManager; //Get the server context and the mapserver IServerContext serverContext = som.CreateServerContext(serviceName, serviceType); return serverContext; }
private ILrsSOEInterface GetSOE(IServerContext serverContext, string soeName) { //Get the server object extension IServerObjectExtensionManager serverObjExtMgr = serverContext.ServerObject as IServerObjectExtensionManager; if (serverObjExtMgr == null) { throw new ApplicationException("Failed to get the ServerObjectExtensionManager from the server context."); } IServerObjectExtension serverObjExt = serverObjExtMgr.FindExtensionByTypeName(soeName); if (serverObjExt == null) { throw new ApplicationException("Failed to get the ServerObjectExtension by extension type name" ); } ILrsSOEInterface lrsSOE = (ILrsSOEInterface)serverObjExt; if (lrsSOE == null) { throw new ApplicationException("Failed to create a reference to the {0} server object extension.", soeName)); } return lrsSOE; }
Notice that the second method returns the SOE typed as the interface we defined in the interfaces assembly.
Serialization
The last step is to get the results from our SOE back to the client. This is easy for simple types, but as usual Geometry is a pain. The Geometry we get back from our SOE is the SOAP Value type geometry. And as far as we know there is no open API for converting this into a Json geometry. Of course, using Reflector you can *see* that ESRI has all the handy methods you may want to use for this in the REST assembly, but alas they are all internal. What’s a dev to do? I’ve asked ESRI to provide public methods for this, and Morten mentioned being able to do this using the WPF components, but again, the needed methods are internal (and they don’t handle the SOAP value types). If you are working with GeoJSON, the Vish (did I mention Vish rocks?) whipped up GeoJson.NET, which you can get from SVN at Assembla (http://svn2.assembla.com/svn/GeoJSON/) . It cleanly handles the ESRI –> GeoJson formatting. All hail Vish.
So – that’s a pretty shot-gun style round up of SOE related stuff. If you have questions or ideas, just drop me a line and I’ll see what I can do.


Just a quick note that I’ll be giving two talks next week at the ESRI South West Users Group (#SWUG09) meeting in Pueblo, Colorado. It will be nice to attend another conference that I don’t have to fly to!