Developing SOAP server object extensions



About developing SOAP SOEs

Simple Object Access Protocol (SOAP) services are built on the standard SOAP message framework that uses a Web Service Description Language (WSDL) document to define strongly typed value objects and service capabilities for clients. ArcGIS for Server provides a set of prepackaged service types, such as map and geocode, and a common SOAP service handler by which they can be accessed. Custom server object extensions (SOEs) can also be accessed using the ArcGIS for Server SOAP service handler to expose custom methods, and utilize ArcGIS and custom SOAP types. This topic discusses the implementation and use of a custom SOAP SOE. The following are a few key items to remember in the context of this topic:
  • Implementation of the IRequestHandler2 interface is required to enable SOAP access to an SOE.
  • A WSDL must be created to define the capabilities of a custom SOAP SOE. It is packaged into your SOE when you build the project and is automatically copied to <ArcGIS for Server Install Location>\XmlSchema when you deploy the SOE. A WSDL template containing SOAP serializable ArcObjects types is included with the SOAP SOE samples.
  • The ESRI.ArcGIS.SOESupport library makes SOAP implementation easier.
Opening the SOAP SOE template in Visual Studio
When you install the SDK, you get a template project that can help you get started building a SOAP SOE. Follow these instructions to open the template:
  1. Start Microsoft Visual Studio.
  2. Click File, New, then Project.
  3. In the Installed Templates tree, click the node to expand your language (Visual C# or Visual Basic), click to expand the ArcGIS node, then click Server Object Extensions.
  4. At the top of the New Project dialog box, choose .NET Framework 3.5 from the drop-down menu. This step is essential because the default shows .NET Framework 4.0 templates.
  5. Click the SOAP Template and add a name and location for your SOE, then click OK.
  6. In the Solution Explorer, click the .cs or .vb file (depending on your chosen language in Step 3) and modify the template code as desired.

SOAP SOE implementation

ArcGIS for Server .NET SOEs leverage the SOAP message processing framework packaged with ArcObjects and used by core ArcGIS for Server services. This framework supports serialization and deserialization of both primitive types (for example, integer, string, and date) and ArcObjects application types. SOAP services are accessible via Hypertext Transfer Protocol (HTTP) requests and responses that carry SOAP messages. SOAP messages are physically encoded using Extensible Markup Language (XML). In general, a SOAP message has a name and a set of parameters. Parameters are written as XML elements. Parameters can be primitive types or application types. Normally, application types are classes that are serialized to XML when the message is written, or deserialized from XML when the message is read. Such parameter classes, must implement the IXMLSerialize ArcObjects interface. You can create your own domain-specific parameter classes.
SOAP uses XML schema to define the layout of XML documents. A parameter class has an associated type defined in the XML schema. The XML schema type for PointN, for instance, states that a Point ArcObjects is serialized as a sequence (order is important) of XML elements, the first of which is called X and is a double, followed by another double called Y, and so on. The following code example shows the PointN XML schema type:
[XML]
<xs:complexType name="PointN">
  <xs:complexContent>
    <xs:extension base="Point">
      <xs:sequence>
        <xs:element name="X" type="xs:double"/>
        <xs:element name="Y" type="xs:double"/>
        <xs:element minOccurs="0" name="M" type="xs:double"/>
        <xs:element minOccurs="0" name="Z" type="xs:double"/>
        <xs:element minOccurs="0" name="ID" type="xs:int"/>
        <xs:element minOccurs="0" name="SpatialReference" type="SpatialReference"/>
      </xs:sequence>
    </xs:extension>
  </xs:complexContent>
</xs:complexType>
In the preceding code example, the element for M is optional (that is, minOccurs="0"). This means an instance document containing a serialized Point might not have this element. Conversely, the elements for X and Y are mandatory. XML schema types are defined to belong to an XML namespace. PointN belongs to the ESRI namespace. This namespace has types for all other ArcObjects parameter classes.
Deserializing XML documents use the ArcObjects XML Class Registry. This registry defines the class that should be instantiated when an instance of a parameter class is found in an XML document. The registry is a set of XML documents, each with entries similar to the following code example:
[XML]
<Type>
  <Name>PointN</Name>
  <Namespace>http://www.esri.com/schemas/ArcGIS/10.1</Namespace>
  <CLSID>{00A5CB41-52DA-11d0-A8F2-00608C85EDE5}</CLSID>
</Type>
A Point in an XML document looks like the following code example:
[XML]
<Location
  xmlns:q1="http://www.esri.com/schemas/ArcGIS/10.1"
  xsi:type="q1:PointN"
  xmlns="">
  <X>
    551658.56245521992
  </X>
  <Y>4905022.8291804288</Y>
</Location>
In the preceding code example, the xsi:type attribute describes the XML schema type as PointN. When a PointN (in the ESRI namespace) is found in a document, the XML Class Registry is used to determine the class identifier (CLSID) of the Component Object Model (COM) class to instantiate.
In summary, SOAP messages are XML documents that can include application types. Each application type needs the following three components:
  1. A COM class that implements IXMLSerialize.
  2. An XML schema type that describes the layout of the class when serialized to XML. The type is included in the service's WSDL document.
  3. An entry in an XML Class Registry file.

The FindNearFeaturesSoapSOE has a parameter class called CustomLayerInfo. See the following code example:
[C#]
[ComVisible(true)][Guid("D8F87A81-8173-4ef7-911C-1A3422AB9215")][ClassInterface
    (ClassInterfaceType.None)]
public class CustomLayerInfo: IXMLSerialize
{
    public string Name
    {
        get;
        set;
    }
    public int ID
    {
        get;
        set;
    }
    public IEnvelope Extent
    {
        get;
        set;
    }
[VB.NET]
<ComVisible(True), Guid("D8F87A81-8173-4ef7-911C-1A3422AB9215"), ClassInterface(ClassInterfaceType.None)> _
            Public Class CustomLayerInfo
    Inherits IXMLSerialize
    Private privateName As String

    Public Property Name() As String
        Get
        Return privateName
        End Get
        Set(ByVal Value As String)
        privateName = Value
        End Set
    End Property

    Private privateID As Integer

    Public Property ID() As Integer
        Get
        Return privateID
        End Get
        Set(ByVal Value As Integer)
        privateID = Value
        End Set
    End Property

    Private privateExtent As IEnvelope

    Public Property Extent() As IEnvelope
        Get
        Return privateExtent
        End Get
        Set(ByVal Value As IEnvelope)
        privateExtent = Value
        End Set
    End Property
The implementation of Serialize defines the name and namespace of the XML schema type, then adds each of the fields to the IXMLSerializeData instance. See the following code example:
[C#]
public void Serialize(IXMLSerializeData data)
{
    data.TypeName = this.GetType().Name;
    data.TypeNamespaceURI = FindNearFeaturesSoapSOE.c_ns_soe;
    data.AddString("Name", Name);
    data.AddInteger("ID", ID);
    data.AddObject("Extent", Extent);
}
[VB.NET]
Public Sub Serialize(ByVal data As IXMLSerializeData)
    data.TypeName = Me.GetType().Name
    data.TypeNamespaceURI = FindNearFeaturesSoapSOE.c_ns_soe
    data.AddString("Name", Name)
    data.AddInteger("ID", ID)
    data.AddObject("Extent", Extent)
End Sub
The Deserialize method works in the opposite way. It uses IXMLSerializeData.Find to find a field by name, then uses a typed method (such as GetInteger) to retrieve the value of the field. IXMLSerializeData.GetObject requires passing the namespace and XML schema type. In the following code example, all fields are mandatory. To deserialize an optional field, you would attempt to find and avoid getting the value if not found.
[C#]
public void Deserialize(IXMLSerializeData data)
{
    int idx = FindMandatoryParam("Name", data);
    this.Name = data.GetString(idx);

    idx = FindMandatoryParam("ID", data);
    this.ID = data.GetInteger(idx);

    idx = FindMandatoryParam("Extent", data);
    this.Extent = (IEnvelope)data.GetObject(idx, FindNearFeaturesSoapSOE.c_ns_esri, 
        "Envelope");
}

private int FindMandatoryParam(string fieldName, IXMLSerializeData data)
{
    int idx = data.Find(fieldName);
    if (idx ==  - 1)
        throw new MissingMandatoryFieldException(fieldName);
    return idx;
}
[VB.NET]
Public Sub Deserialize(ByVal data As IXMLSerializeData)
    Dim idx As Integer = FindMandatoryParam("Name", data)
    Me.Name = data.GetString(idx)
    idx = FindMandatoryParam("ID", data)
    Me.ID = data.GetInteger(idx)
    idx = FindMandatoryParam("Extent", data)
    Me.Extent = CType(data.GetObject(idx, FindNearFeaturesSoapSOE.c_ns_esri, "Envelope"), IEnvelope)
End Sub


Private Function FindMandatoryParam(ByVal fieldName As String, ByVal data As IXMLSerializeData) As Integer
    Dim idx As Integer = data.Find(fieldName)
    If idx = -1 Then
        Throw New MissingMandatoryFieldException(fieldName)
    End If
    Return idx
End Function
The following code example is the XML schema type for the CustomLayerInfo class:
[XML]
<xs:complexType name="CustomLayerInfo">
  <xs:sequence>
    <xs:element name="Name" type="xs:string"/>
    <xs:element name="ID" type="xs:int"/>
    <xs:element name="Extent" type="Envelope"/>
  </xs:sequence>
</xs:complexType>
Finally, the following code example is the entry in the XML Class Registry file. The type, namespace, and CLSID match the values used in the code.
[XML]
<Type>
  <Name>CustomLayerInfo</Name>
  <Namespace>http://examples.esri.com/schemas/FindNearFeaturesSoapSOE/1.0</Namespace>
  <CLSID>{D8F87A81-8173-4ef7-911C-1A3422AB9215}</CLSID>
</Type>
ArcGIS provides a SOAP SOE template that is integrated with Visual Studio. For more information, see Overview of developing a server object extension.

Service API description

The FindNearFeatures service extends any map service by allowing clients to find features in a layer that are within a given distance. The following code example shows the methods exposed by the service:
[C#]
CustomLayerInfo[] GetLayerInfos();
RecordSet FindNearFeatures(int layerID, Point location, double distance);
[VB.NET]
CustomLayerInfo() GetLayerInfos()
RecordSet FindNearFeatures(Integer layerID, Point location, Double distance)
The following describes the elements in the preceding code example:
  • GetLayerInfos—Return an array of CustomLayerInfo instances, one for each layer in the map service.
  • FindNearFeatures—Return the set of features in a layer that are within a given location and distance. The features are returned as records in the RecordSet.
  • CustomLayerInfo—A developer defined parameter class. RecordSet is an ArcObjects parameter class.
  • A client can call GetLayerInfos to discover the layers and their extents, then call FindNearFeatures to get a layer's features within a certain distance of a location.

Using SOESupport

SOESupport includes helper classes that make implementing a SOAP SOE easier. The general idea is to create an SOE class that contains SOESupport.SoeSoapImpl. This class helps with deserialization of the SOAP request, checking for required capabilities, creating the SOAP response, basic logging, and error handling. It ensures all exceptions are mapped into SOAP faults, so clients can process them according to the standard. The following code example shows the declaration of the SOE class:
[C#]
[ComVisible(true)][Guid("C41E8674-F186-4a0c-8FC9-AAB7885EFD00")][ClassInterface
    (ClassInterfaceType.None)]
public class FindNearFeaturesSoapSOE: ServicedComponent, IRequestHandler2,
    IServerObjectExtension, IObjectConstruct
{
    private const string c_soe_name = "FindNearFeaturesSoapSOE";
    internal const string c_ns_soe = 
        "http://examples.esri.com/schemas/FindNearFeaturesSoapSOE/1.0";
    internal const string c_ns_esri = "http://www.esri.com/schemas/ArcGIS/10.1";

    private IServerObjectHelper serverObjectHelper;
    private ServerLogger logger;
    private IPropertySet configProps;

    IRequestHandler2 reqHandler;

    public FindNearFeaturesSoapSOE()
    {
        SoapCapabilities soapCaps = new SoapCapabilities();
        soapCaps.AddMethod("GetLayerInfos", "getInfo");
        soapCaps.AddMethod("FindNearFeatures", "findFeatures");

        logger = new ServerLogger();

        SoeSoapImpl soapImpl = new SoeSoapImpl(c_soe_name, soapCaps,
            HandleSoapMessage);
        reqHandler = (IRequestHandler2)soapImpl;
    }
[VB.NET]
<ComVisible(True), Guid("C41E8674-F186-4a0c-8FC9-AAB7885EFD00"), ClassInterface(ClassInterfaceType.None)> _
            Public Class FindNearFeaturesSoapSOE
    Inherits ServicedComponent
    Implements IRequestHandler2, IServerObjectExtension, IObjectConstruct
    Private Const c_soe_name As String = "FindNearFeaturesSoapSOE"
    Friend Const c_ns_soe As String = "http://examples.esri.com/schemas/FindNearFeaturesSoapSOE/1.0"
    Friend Const c_ns_esri As String = "http://www.esri.com/schemas/ArcGIS/10.1"
    Private serverObjectHelper As IServerObjectHelper
    Private logger As ServerLogger
    Private configProps As IPropertySet
    Private reqHandler As IRequestHandler2

    Public Sub New()
        Dim soapCaps As New SoapCapabilities()
        soapCaps.AddMethod("GetLayerInfos", "getInfo")
        soapCaps.AddMethod("FindNearFeatures", "findFeatures")
        logger = New ServerLogger()
        Dim soapImpl As New SoeSoapImpl(c_soe_name, soapCaps, HandleSoapMessage)
        reqHandler = CType(soapImpl, IRequestHandler2)
    End Sub
  • The class implements the interfaces required by the server.
  • The constructor does the following:
    • Uses the SOESupport.SoapCapabilities helper class to define the capabilities to execute each method.
    • Creates an instance of the SOESupport.ServerLogger class and keeps a reference.
    • Creates an instance of SoeSoapImpl and keeps a reference to its IRequestHandler2 interface.
  • The implementation of IRequestHandler.HandleStringRequest delegates the call to SoeSoapImpl.
See the following code example:
[C#]
public string HandleStringRequest(string Capabilities, string request)
{
    return reqHandler.HandleStringRequest(Capabilities, request);
}
[VB.NET]
Public Function HandleStringRequest(ByVal Capabilities As String, ByVal request As String) As String
    Return reqHandler.HandleStringRequest(Capabilities, request)
End Function
SoeSoapImpl.HandleStringRequest uses the ArcObjects XML framework to deserialize the string into a higher-level IMessage instance. This includes deserializing any parameter classes in the message (ArcObjects or developer defined). It also verifies the configuration has the required capability to execute the method, and lastly, it calls HandleSoapMessage on the SOE class.
HandleSoapMessage is a delegate, an implementation of which is passed to SoeSoapImpl in its constructor. The following code example shows how the delegate is defined:
[C#]
public delegate void HandleSoapMessage(IMessage reqMsg, IMessage respMsg);
[VB.NET]
Public Delegate Sub HandleSoapMessage(ByVal reqMsg As IMessage, ByVal respMsg As IMessage)
The HandleSoapMessage implementation in the SOE class determines what function should be called based on the request's message. The following code example shows the implementation:
[C#]
public void HandleSoapMessage(IMessage reqMsg, IMessage respMsg)
{
    string methodName = reqMsg.Name;

    if (string.Compare(methodName, "GetLayerInfos", true) == 0)
        GetLayerInfos(reqMsg, respMsg);

    else if (string.Compare(methodName, "FindNearFeatures", true) == 0)
        FindNearFeatures(reqMsg, respMsg);

    else
        throw new ArgumentException("Method not supported: " + QualifiedMethodName
            (c_soe_name, methodName));
}
[VB.NET]
Public Sub HandleSoapMessage(ByVal reqMsg As IMessage, ByVal respMsg As IMessage)
    Dim methodName As String = reqMsg.Name
    If String.Compare(methodName, "GetLayerInfos", True) = 0 Then
        GetLayerInfos(reqMsg, respMsg)
    ElseIf String.Compare(methodName, "FindNearFeatures", True) = 0 Then
        FindNearFeatures(reqMsg, respMsg)
    Else
        Throw New ArgumentException("Method not supported: " & QualifiedMethodName(c_soe_name, methodName))
    End If
End Sub
The implementation of the FindNearFeatures method is shown in the following code example. This is a wrapper method that deals with getting parameters from the request, executing the business logic, and setting parameters on the response.
[C#]
private void FindNearFeatures(IMessage reqMsg, IMessage respMsg)
{
    IXMLSerializeData reqParams = reqMsg.Parameters;

    int layerID = reqParams.GetInteger(FindParam("LayerID", reqParams));

    IPoint location = (IPoint)reqParams.GetObject(FindParam("Location", reqParams),
        c_ns_esri, "PointN");

    double distance = reqParams.GetDouble(FindParam("Distance", reqParams));

    IRecordSet recordSet = FindNearFeatures(layerID, location, distance);

    respMsg.Name = "FindNearFeaturesResponse";
    respMsg.NamespaceURI = c_ns_soe;
    respMsg.Parameters.AddObject("Result", recordSet);
}
[VB.NET]
Private Sub FindNearFeatures(ByVal reqMsg As IMessage, ByVal respMsg As IMessage)
    Dim reqParams As IXMLSerializeData = reqMsg.Parameters
    Dim layerID As Integer = reqParams.GetInteger(FindParam("LayerID", reqParams))
    Dim location As IPoint = CType(reqParams.GetObject(FindParam("Location", reqParams), c_ns_esri, "PointN"), IPoint)
    Dim distance As Double = reqParams.GetDouble(FindParam("Distance", reqParams))
    Dim recordSet As IRecordSet = FindNearFeatures(layerID, location, distance)
    respMsg.Name = "FindNearFeaturesResponse"
    respMsg.NamespaceURI = c_ns_soe
    respMsg.Parameters.AddObject("Result", recordSet)
End Sub
FindNearFeatures uses the ArcObjects IMessage interface to retrieve the request parameters, then it uses IXMLSerialize (also ArcObjects) to find and retrieve the method's parameters. The FindParam utility function finds a parameter given its name (or fails). See the following code example:
[C#]
private int FindParam(string parameterName, IXMLSerializeData msgParams)
{
    int idx = msgParams.Find(parameterName);
    if (idx ==  - 1)
        throw new ArgumentNullException(parameterName);
    return idx;
}
[VB.NET]
Private Function FindParam(ByVal parameterName As String, ByVal msgParams As IXMLSerializeData) As Integer
    Dim idx As Integer = msgParams.Find(parameterName)
    If idx = -1 Then
        Throw New ArgumentNullException(parameterName)
    End If
    Return idx
End Function
IXMLSerializeData has methods to coerce a parameter to a specific type. For example, in the case of the LayerID parameter, the code uses GetInteger to coerce the value in the message to an integer.
After deserialization of the request parameters, the code calls a second FindNearFeatures method that has the business logic and finally, fills up the response message by setting its name (FindNearFeaturesResponse), namespace (c_ns_soe), and parameters (Result). The code also shows how to retrieve a request parameter that is a value object of a known type, in this case, PointN. The GetObject call requires the type's namespace (c_ns_esri) and the type's name ("PointN"). It returns an object; therefore, a cast to IPoint is required. See the following code example:
[C#]
IPoint location = (IPoint)reqParams.GetObject(FindParam("Location", reqParams),
    c_ns_esri, "PointN");
[VB.NET]
Dim location As IPoint = CType(reqParams.GetObject(FindParam("Location", reqParams), c_ns_esri, "PointN"), IPoint)
The response message is marked as belonging to the c_ns_soe namespace. This namespace is not the ESRI namespace and should be uniquely defined for each SOE. The following code example shows how it is defined:
[C#]
internal const string c_ns_soe = 
    "http://examples.esri.com/schemas/FindNearFeaturesSoapSOE/1.0";
[VB.NET]
Friend Const c_ns_soe As String = "http://examples.esri.com/schemas/FindNearFeaturesSoapSOE/1.0"
Careful attention should be taken with the strings used in the implementation of wrapper methods. Message and parameter names are case-sensitive, and mismatches between the code and the WSDL are the number one cause of development problems for SOAP SOEs.
The following code example shows the implementation of the business logic for FindNearFeatures. The code buffers the input point, creates a filter, and queries the data. Lastly, it returns the record set. RecordSet is a class that supports XML serialization. This implementation uses a method available in the coarse map service application programming interface (API). In real-world SOEs, developers can choose to use the fine-grained ArcObjects API instead.
[C#]
private IRecordSet FindNearFeatures(int layerID, IPoint location, double distance)
{
    IMapServer3 mapServer = m_soh.ServerObject as IMapServer3;
    if (mapServer == null)
        throw new Exception("Unable to access the map server.");

    IGeometry queryGeometry = ((ITopologicalOperator)location).Buffer(distance);

    ISpatialFilter filter = new SpatialFilterClass();
    filter.Geometry = queryGeometry;
    filter.SpatialRel = esriSpatialRelEnum.esriSpatialRelIntersects;

    IQueryResultOptions resultOptions = new QueryResultOptionsClass();
    resultOptions.Format = esriQueryResultFormat.esriQueryResultRecordSetAsObject;

    IMapTableDescription tableDesc = GetTableDesc(mapServer, layerID);

    IQueryResult result = mapServer.QueryData(mapServer.DefaultMapName, tableDesc,
        filter, resultOptions);

    return (RecordSet)result.Object;
}
[VB.NET]
Private Function FindNearFeatures(ByVal layerID As Integer, ByVal location As IPoint, ByVal distance As Double) As IRecordSet
    Dim mapServer As IMapServer3 = TryCast(m_soh.ServerObject, IMapServer3)
    If mapServer Is Nothing Then
        Throw New Exception("Unable to access the map server.")
    End If
    Dim queryGeometry As IGeometry = (CType(location, ITopologicalOperator)).Buffer(distance)
    Dim Filter As ISpatialFilter = New SpatialFilterClass()
    Filter.Geometry = queryGeometry
    Filter.SpatialRel = esriSpatialRelEnum.esriSpatialRelIntersects
    Dim resultOptions As IQueryResultOptions = New QueryResultOptionsClass()
    resultOptions.Format = esriQueryResultFormat.esriQueryResultRecordSetAsObject
    Dim tableDesc As IMapTableDescription = GetTableDesc(mapServer, layerID)
    Dim result As IQueryResult = mapServer.QueryData(mapServer.DefaultMapName, tableDesc, Filter, resultOptions)
    Return CType(result.Object, RecordSet)
End Function

Arrays of parameter classes

The SOAP specification defines the XML representation for arrays of parameter classes. It dictates the names of the XML elements and their layout. To help create these arrays, SOESupport includes a base class—SerializableList—that implements IXMLSerialize. In FindNearFeaturesSoapSOE, CustomLayerInfos derives from SerializableList and creates a concrete, COM-visible class of CustomLayerInfo instances. See the following code example:
[C#]
[ComVisible(true)][Guid("845EE02E-C349-44af-B973-7CCD4DF0BCB8")][ClassInterface
    (ClassInterfaceType.None)]
public class CustomLayerInfos: SerializableList < CustomLayerInfo > 
{
    public CustomLayerInfos(): base(FindNearFeaturesSoapSOE.c_ns_soe){}
}
[VB.NET]
<ComVisible(True), Guid("845EE02E-C349-44af-B973-7CCD4DF0BCB8"), ClassInterface(ClassInterfaceType.None)> _
            Public Class CustomLayerInfos
    Inherits SerializableList(Of CustomLayerInfo)

    Public Sub New()
        MyBase.New(FindNearFeaturesSoapSOE.c_ns_soe)
    End Sub

End Class
SerializableList derives from List<T>; therefore, it implements any methods of that class. For example, you can use the Add method to add instances to the list (see the implementation of GetLayerInfos).
The following code example shows an XML schema type that corresponds to CustomLayerInfos:
[XML]
<xs:complexType name="ArrayOfCustomLayerInfo">
  <xs:sequence>
    <xs:element
      minOccurs="0"
      maxOccurs="unbounded"
      name="CustomLayerInfo"
      type="tns:CustomLayerInfo"/>
  </xs:sequence>
</xs:complexType>
It is defined as a sequence of zero, one, or more XML elements whose name and type is CustomLayerInfo. An XML containing this type of array is shown in the following code example (taken from a GetLayerInfos response):
[XML]
<Result xsi:type="tns:ArrayOfCustomLayerInfo">
  <CustomLayerInfo xsi:type="tns:CustomLayerInfo">
    <Name>Buildings</Name>
    <ID>0</ID>
    <Extent xsi:type="esri:EnvelopeN">[...]</Extent>
  </CustomLayerInfo>
  <CustomLayerInfo xsi:type="tns:CustomLayerInfo">
    <Name>Parcels</Name>
    <ID>1</ID>
    <Extent xsi:type="esri:EnvelopeN">[...]</Extent>
    </Extent>
  </CustomLayerInfo>
</Result>

XML registry

As previously mentioned, each parameter class must have an entry in an XML Class Registry file. ArcGIS for Server finds these files under folders following a naming convention. The following syntax is for the folders:
  • <common files>\ArcGIS\<product>+<version>\XmlClassRegistry\<organization>\XmlSupport*.dat
The following explains each element in the preceding syntax:
  • <common files>—The Windows common files directory, commonly C:\Program Files\Common Files.
  • <product>—The Esri product for which you are installing serializable classes, for example, Server.
  • <version>—The version of the Esri product you are targeting, for example, 10.1.
  • <organization>—The name of the organization that owns the classes.
  • XmlSupport*.dat—The file with the entries for serializable classes. Esri suggests to call it using the organization's name as part of the file, as in XmlSupportMyOrg.dat. An alternative naming convention could include the SOE's name.
The setup installing the XML class registry files should create the folder if not present, then put the XmlSupport*.dat file in the folder, or if you are manually configuring an SOE, you must place the file in this location. The following is an example path:
  • C:\Program Files\Common Files\ArcGIS\Server10.1\XmlClassRegistry\Acme\ XmlSupportFindNearFeatures.dat
The following code example shows the contents for the FindNearFeaturesSoapSOE file. It is an XML file whose root element must be called TypeMapping. It contains the entries for the two parameter classes in the service, CustomLayerInfo and ArrayOfLayerInfo.
[XML]
<TypeMapping>
  <Type>
    <Name>CustomLayerInfo</Name>
    <Namespace>http://examples.esri.com/schemas/FindNearFeaturesSoapSOE/1.0</Namespace>
    <CLSID>{D8F87A81-8173-4ef7-911C-1A3422AB9215}</CLSID>
  </Type>
  <Type>
    <Name>ArrayOfCustomLayerInfo</Name>
    <Namespace>http://examples.esri.com/schemas/FindNearFeaturesSoapSOE/1.0</Namespace>
    <CLSID>{845EE02E-C349-44af-B973-7CCD4DF0BCB8}</CLSID>
  </Type>
</TypeMapping>

WSDL description

The WSDL document describes the web service. Any WSDL included with your SOE is packaged into the .SOE file when you build. At the time the SOE is deployed, ArcGIS Server places the WSDL under the folder ArcGIS\XmlSchema. The WSDL describes the following:
  • Namespace for the service
  • Methods exposed by the service
  • Parameters of each method and their representation in XML
  • How to transport the messages
  • Name and address of the service

The WSDL can be seen as having a set of sections roughly corresponding to the preceding bullet items. The first section defines the namespaces to use. See the following code example:
[XML]
<definitions
  xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
  xmlns:xs="http://www.w3.org/2001/XMLSchema"
  xmlns="http://schemas.xmlsoap.org/wsdl/"
  xmlns:e="http://examples.esri.com/schemas/FindNearFeaturesSoapSOE/1.0"
  targetNamespace="http://examples.esri.com/schemas/FindNearFeaturesSoapSOE/1.0">
  ...
</definitions>
The definitions element defines the namespace of the service via the targetNamespace attribute. This namespace must match the one used in code (c_ns_soe). It also declares namespaces that will be used within the document (SOAP and XML schema).
The types section defines the types that will be used as parameters (for example, Point and double) and the methods (for example, GetLayerInfos). It does so by defining two XML schemas, represented by the schema elements. The first schema defines the ESRI types that can be used as parameters of the service's methods. For instance, one of the inputs of FindNearFeatures is a Point and its output is a RecordSet. Both types belong to the ESRI namespace and are defined in this first schema. This schema is surrounded by the comments "start of esri types" and "end of esri types." See the following code example:
[XML]
<types>
  <!-- start of esri types -->
  <xs:schema
    xmlns="http://www.esri.com/schemas/ArcGIS/10.1"
    targetNamespace="http://www.esri.com/schemas/ArcGIS/10.1"
    xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:complexType name="PropertySetProperty">. . .</xs:complexType>
  </xs:schema>
  <!-- end of esri types -->
  . . .
</types>
The second schema defines the elements and types for the service. They will belong to the service's target namespace, which again must match the one used in code (c_ns_soe). The types from the ESRI namespace are imported into this second schema using the import element. See the following code example:
[XML]
<!-- start of service schema -->
<xs:schema
    targetNamespace="http://examples.esri.com/schemas/FindNearFeaturesSoapSOE/1.0"
    xmlns:tns="http://examples.esri.com/schemas/FindNearFeaturesSoapSOE/1.0"
    xmlns="http://www.esri.com/schemas/ArcGIS/10.1"> 

    <xs:import namespace="http://www.esri.com/schemas/ArcGIS/10.1"/>
A service method is defined in the WSDL using two elements. The first defines the request name and its parameters, and the second defines the response name and its parameters. A request or response can have zero or more parameters.
The definition for the FindNearFeatures method is shown in the following code example. Its request message takes as inputs an integer for the LayerID, a Point for the location, and a double for the distance. Its response message returns a RecordSet in an element called Result. The WSDL defines elements for requests and responses for the method GetLayerInfos as well (with no input parameters). Once again, the element names here must exactly match those used in the code.
[XML]
<xs:element name="FindNearFeatures">
  <xs:complexType>
    <xs:sequence>
      <xs:element name="LayerID" type="xs:int"/>
      <xs:element name="Location" type="Point"/>
      <xs:element name="Distance" type="xs:double"/>
    </xs:sequence>
  </xs:complexType>
</xs:element>
<xs:element name="GetNearFeaturesResponse">
  <xs:complexType>
    <xs:sequence>
      <xs:element name="Result" type="RecordSet"/>
    </xs:sequence>
  </xs:complexType>
</xs:element>
The preceding code example section is very important in this process. The remaining code example sections can be considered boilerplate code and can be created easily by following a template.

The following code example section defines the messages exchanged between the client and server, and is for the FindNearFeatures method. The names in the message elements add the suffixes In and Out, as in FindNearFeaturesIn and FindNearFeaturesOut.
[XML]
<message name="FindNearFeaturesIn">
  <part name="parameters" element="e:FindNearFeatures"/>
</message>
<message name="FindNearFeaturesOut">
  <part name="parameters" element="e:FindNearFeaturesResponse"/>
</message>
The next code example section defines the portType or the web service's interface. It defines the name of the portType and its operations. The definition of the operation FindNearFeatures is copied in the following code example (notice that it uses the messages previously defined).
[XML]
<portType name="FindNearFeaturesSoapSoePortType">
  <operation name="FindNearFeatures">
    <input message="e:FindNearFeaturesIn"/>
    <output message="e:FindNearFeaturesOut"/>
  </operation>
  . . .
</portType>
The binding section states the operations of the portType will be serialized using SOAP and be transported using HTTP.
[XML]
<binding name="FindNearFeaturesSoapSoeBinding" type="e:FindNearFeaturesSoapSoePortType">
  
  <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http" />

  <operation name="FindNearFeatures">
    <soap:operation soapAction="" style="document" />
    <input>
      <soap:body use="literal" />
    </input>
    <output>
      <soap:body use="literal" />
    </output>
  </operation>
The last section defines the service's name and address. The name and address will be derived from an actual service instance. For example, if the SOE is enabled on a map service named UKCounties, the SOE service's name will be UKCounties and its uniform resource locator (URL) will be http://webservername/arcgis/services/UKCounties/MapServer/FindNearFeaturesSoap.
Because a single WSDL document is used for all service instances, it has placeholders for the name and address. At runtime, the server replaces these placeholders with the appropriate values. See the following code example:
[XML]
<service name="#NAME#">
  <port
    name="FindNearFeaturesSoapSoePortType"
    binding="e:FindNearFeaturesSoapSoeBinding">
    <soap:address location="#URL#"/>
  </port>
</service>

Creating a SOAP client

Once you've written your SOAP SOE, deployed it to ArcGIS Server and enabled it on a service, you'll need a client application that can consume it. Most development platforms maintain one or more SOAP toolkits that provide a convenient engine for creating native client proxies and classes to support working with a SOAP service. One significant benefit of a SOAP service is the ability to access and work with it via a standard contract (WSDL) so proprietary components (for example, ArcObjects) are not needed on the client. 
Microsoft's .NET SDK includes a wsdl.exe command line utility that is also integrated with Visual Studio (all versions and editions). The utility generates a set of native .NET classes to work with a SOAP service. Visual Studio actually manages the creation and use of these classes quite effectively. The following are the steps to generate the necessary client code to build a SOAP client in Visual Studio:
  1. Right-click References in the Solution Explorer.
  2. Click Add Service Reference.
  3. Click Advanced at the bottom of the dialog box.
  4. Click Add Web Reference at the bottom of the dialog box.
  5. Add the URL to the SOE service, adding ?wsdl at the end, then click Go. For example, if the USA map service has FindNearFeaturesSoapSoe enabled, the SOE endpoint is as follows: http://webservername/arcgis/services/Yellowstone/MapServer/FindNearFeaturesSoapSoe?wsdl.

    See the following screen shot:



    On the preceding screen shot, the name of the reference is win2008r2rx (the Web server's name). The generated proxy classes will be placed in a C# namespace that includes that name. You can change the Web reference name.
  6. Click Add Reference. This adds a new entry under Web References in the Solution Explorer using the Web reference named you defined on the dialog box. See the following screen shot:



    You can right-click the Web reference and click View on the Object Browser to see the proxy classes created for you. ESRI defined value classes, such as Envelope and PointN are available, as is the proxy for the service, for example, USA_FindNearFeaturesSoapSoe (again, assuming the map service name is USA and the SOE's name is FindNearFeaturesSoapSoe). On the Object Browser, note the namespace. See the following screen shot:



    Open the code and add a using directive (or Imports when using VB .NET) to import the types defined in that namespace.
    • using FindNearFeaturesSOAPClient.localhost;

    Once the namespaces is referenced, the following code example can be added to execute the methods in the SOE (for example, as the body of the click event on a button). The code first creates an instance of the service's proxy, then calls GetLayerInfos and FindNearFeatures on the first layer. The FindNearFeatures method requires the FindFeatures capability to be enabled on the SOE associated with the map service. 
[C#]
private void button1_Click(object sender, EventArgs e)
{
    try
    {
        //Create an instance of the proxy.
        USA_FindNearFeaturesSoapSOE nearFeatsService = new
            USA_FindNearFeaturesSoapSOE();
        nearFeatsService.Url = 
            "http://localhost/ArcGIS/services/USA/MapServer/FindNearFeaturesSoapSOE";

        //getLayerInfos.
        CustomLayerInfo[] layerInfos = nearFeatsService.GetLayerInfos();
        foreach (CustomLayerInfo layerInfo in layerInfos)
        {
            EnvelopeN extent = (EnvelopeN)layerInfo.Extent;
            debug(string.Format("Layer {0} has ID: {1} and extent: {2},{3},{4},{5}",
                layerInfo.Name, layerInfo.ID, extent.XMin, extent.YMin, extent.XMax,
                extent.YMax));
        }

        //findNearFeatures.
        CustomLayerInfo aLayerInfo = layerInfos[0];
        PointN location;
        double distance;
        GetCenterPointAndDistance((EnvelopeN)aLayerInfo.Extent, out location, out
            distance);
        RecordSet feats = nearFeatsService.FindNearFeatures(aLayerInfo.ID, location,
            distance);
        foreach (Record record in feats.Records)
        {
            foreach (object o in record.Values)
                if (o != null)
                    debug(o.ToString() + ", ");
            debug("\n");
        }
    }
    catch (Exception ex)
    {
        MessageBox.Show(ex.Message);
    }
}
[VB.NET]
Private Sub button1_Click(ByVal sender As Object, ByVal e As EventArgs)
    Try
    'Create an instance of the proxy.
    Dim nearFeatsService As New USA_FindNearFeaturesSoapSOE()
    nearFeatsService.Url = "http://localhost/ArcGIS/services/USA/MapServer/FindNearFeaturesSoapSOE"
    'getLayerInfos.
    Dim layerInfos() As CustomLayerInfo = nearFeatsService.GetLayerInfos()
    For Each layerInfo As CustomLayerInfo In layerInfos
        Dim extent As EnvelopeN = CType(layerInfo.Extent, EnvelopeN)
        debug(String.Format("Layer {0} has ID: {1} and extent: {2},{3},{4},{5}", layerInfo.Name, layerInfo.ID, extent.XMin, extent.YMin, extent.XMax, extent.YMax))
    Next layerInfo
    'findNearFeatures.
    Dim aLayerInfo As CustomLayerInfo = layerInfos(0)
    Dim location As PointN
    Dim distance As Double
    GetCenterPointAndDistance(CType(aLayerInfo.Extent, EnvelopeN), location, distance)
    Dim feats As RecordSet = nearFeatsService.FindNearFeatures(aLayerInfo.ID, location, distance)
    For Each record As Record In feats.Records
        For Each o As Object In record.Values
            If o IsNot Nothing Then
                debug(o.ToString() & ", ")
            End If
        Next o
        debug(Constants.vbLf)
    Next record
    Catch ex As Exception
    MessageBox.Show(ex.Message)
    End Try
End Sub
The following are the two helper functions:
  • GetProperty finds a property in a property set given its name. 
  • GetCenterPointAndDistance calculates the center point of the extent of a layer (envelope) and returns a distance that is 1/10th of the envelope's width. These values are used in the call to FindNearFeatures.
See the following code example:
[C#]
private object GetProperty(PropertySet props, string key)
{
    foreach (PropertySetProperty prop in props.PropertyArray)
    {
        if (string.Compare(prop.Key, key, true) == 0)
            return prop.Value;
    }
    return null;
}

private void GetCenterPointAndDistance(EnvelopeN extent, out PointN center, out
    double distance)
{
    center = new PointN();
    center.SpatialReference = extent.SpatialReference;
    center.X = extent.XMin + (Math.Abs(extent.XMax - extent.XMin) / 2);
    center.Y = extent.YMin + (Math.Abs(extent.YMax - extent.YMin) / 2);
    distance = Math.Abs(extent.XMax - extent.XMin) / 10;
}
[VB.NET]
Private Function GetProperty(ByVal props As PropertySet, ByVal Key As String) As Object
    For Each prop As PropertySetProperty In props.PropertyArray
        If String.Compare(prop.Key, Key, True) = 0 Then
            Return prop.Value
        End If
    Next prop
    Return Nothing
End Function


Private Sub GetCenterPointAndDistance(ByVal extent As EnvelopeN, <System.Runtime.InteropServices.Out()> ByRef center As PointN, <System.Runtime.InteropServices.Out()> ByRef distance As Double)
    center = New PointN()
    center.SpatialReference = extent.SpatialReference
    center.X = extent.XMin + (Math.Abs(extent.XMax - extent.XMin) / 2)
    center.Y = extent.YMin + (Math.Abs(extent.YMax - extent.YMin) / 2)
    distance = Math.Abs(extent.XMax - extent.XMin) / 10
End Sub
At runtime, the extent for each layer is returned and features in the first layer that intersect the envelope returned by GetCenterPointAndDistance will be returned with their attributes displayed in a text window. See the following screen shot: