How to develop the SOE


Summary This topic discusses the development of the server object extension (SOE), which runs within the geographic information system (GIS) server and exposes the methods that extend the MapServer server object.

In this topic


Creating the SOE solution and interfaces project

In this section of the implementation, do the following steps to create a project that contains interfaces implemented by SOE classes and utilized by clients:
  1. Start Visual Studio.
  2. Click the File menu, click New, then click Project. The New Project dialog box appears.
  3. Under the Project types pane, click to expand the Other Project Types node, then click Visual Studio solutions. Click Blank solution under the Visual Studio installed templates.
  4. Type ArcGIS_Spatial_Query_SOE_CSharp2008 in the Name text box.
  5. Type the path in the Location text box or click Browse for the location.
  6. Click OK.
  7. In the Solution Explorer, right-click the solution and select Add, New Solution Folder, then name the folder Implementation (where you will create the projects storing the SOE's implementation). See the following screen shot:


  8. Right-click the Implementation folder and select Add, then New Project. The Add New Project dialog box appears.
  9. Under the Project types pane, click to expand the Visual C# project type. Under the Visual Studio installed templates, select Class Library.
  10. Type the path or click Browse for the location.
  11. Type SpatialQuerySOE.Interfaces_CSharp in the Name text box for the project name.
  12. Click OK. In the Solution Explorer, right-click Class1.cs and select Delete.
  13. Right-click the project in the Solution Explorer and select Properties.
  14. On the Application tab, specify an assembly name and default namespace of SpatialQuerySOE.Interfaces.

Creating the interfaces class

You will create a C# (Interfaces) class that contains two interfaces to implement within the SOE classes in another project. Interfaces for the SOE are stored in a separate assembly because client applications only need the interfaces registered as Component Object Model (COM) types to interact with the SOE. The business logic for the SOE only needs to reside on the GIS server and does not need to be provided to the client. As a result, the assembly provided to a client application only contains interfaces (no business logic). Similar to working with ArcObjects remotely via ArcGIS Server, a client uses a custom SOE interface to work with SOE classes (objects) remotely via ArcGIS Server.
Do the following steps to add the new class to the SpatialQuerySOE.Interfaces_CSharp project:
  1. In the Solution Explorer, right-click the SpatialQuerySOE.Interfaces_CSharp project, click Add, then click New Item. The Add New Item dialog box appears.
  2. Under the Templates pane, click Class.
  3. Type Interfaces in the Name text box.
  4. Click Add. A new class is added to the project and code for the class appears with automatically generated code.

Implementing the SOE interfaces class

You will need to add references to assemblies that contain the required ArcObjects for the interface definitions in this class file. Because the class contains interfaces that will be exposed as COM types, it needs to reference additional assemblies to manage interoperability.
Do the following steps to implement the SOE interfaces class:
  1. Add references to the following assemblies:
    • ESRI.ArcGIS.Carto
    • ESRI.ArcGIS.Display
    • ESRI.ArcGIS.Geodatabase
    • ESRI.ArcGIS.Geometry
    • ESRI.ArcGIS.Server
    • ESRI.ArcGIS.System
    • System.EnterpriseServices
  2. Add the SpatialQuerySOE.Interfaces namespace. Within the namespace definition, add the IExtension and IResults interfaces. 

    The IExtension interface defines the basic framework to implement by the SpatialQuerySOE.Extension class, namely a method to accept a user provided ArcObjects object of type IPoint and a double value to define the distance around the point to generate a buffer. An ArcGIS Server client can work with a reference to the SpatialQuerySOE.Extension object running within the server object container (SOC) via the QueryPoint method on the IExtension interface. This pattern follows standard programming techniques for working with ArcObjects (COM objects) remotely via ArcGIS Server. The QueryPoint method creates a SpatialQuerySOE.Results object in the server container process, updates its properties, and returns to the client a reference to the remote SpatialQuerySOE.Results COM object via the IResults interface.

    The IResults interface defines the basic framework to implement by the SpatialQuerySOE.Results class, namely two properties to store a reference to an array of graphic elements and a set of records.

    Since both interfaces are registered (added to the registry) with COM, they should define a globally unique identifier (GUID) using the COM attribute, GuidAttribute. The GUID is used to uniquely identify an application, component, class, interface, and so on within the Windows registry. There are many options to generate a unique GUID. You can create a unique GUID using the .NET System.Guid struct. The call to System.Guid.NewGuid().ToString() returns a usable GUID string. Additionally, various Web sites and services provide GUID generating capabilities. The following code example shows code for the interface:
[C#]
namespace SpatialQuerySOE.Interfaces
{
    [GuidAttribute("9D47D51C-F2B8-4381-8FA7-A6E1E1E9792F")]
    public interface IExtension
    {
        IResults QueryPoint(ESRI.ArcGIS.Geometry.IPoint point, double distance);
    }
    [GuidAttribute("19A98B11-1EDE-4470-88FD-007F4B7BD05F")]
    public interface IResults
    {
        ESRI.ArcGIS.Carto.IGraphicElements ResultsGraphics
        {
            get;
            set;
        }
        ESRI.ArcGIS.Geodatabase.IRecordSet SummaryStatistics
        {
            get;
            set;
        }
    }
}
[VB.NET]
Namespace SpatialQuerySOE.Interfaces_VBNet
<Guid("20433F77-8AB9-45b0-B858-C3144EC50347")> _
      Public Interface IExtension

    Function QueryPoint(ByVal point As ESRI.ArcGIS.Geometry.IPoint, ByVal distance As Double) As IResults
        End Interface
        <Guid("D4086DEF-564B-4442-94EE-C4DB0C90402A")> _
              Public Interface IResults

            Property ResultsGraphics() As ESRI.ArcGIS.Carto.IGraphicElements

                Property SummaryStatistics() As ESRI.ArcGIS.Geodatabase.IRecordSet
                    End Interface
                    End Namespace

Setting assembly properties for SpatialQuerySOE.Interfaces_CSharp project

Do the following steps to set the assembly properties:
  1. In the SpatialQuerySOE.Interfaces_CSharp project, open the AssemblyInfo.cs file and change it to reflect the settings in the following code example. 
  2. Set the ComVisibleAttribute to true and specify a unique GUID for the GuidAttribute. Since the assembly is used to generate a type library, these attributes enable the interfaces to be registered with COM and defines a unique identifier.
[C#]
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("SpatialQuerySOE.Interfaces")][assembly:
    AssemblyDescription("")][assembly: AssemblyConfiguration("")][assembly:
    AssemblyCompany("ESRI")][assembly: AssemblyProduct("SpatialQuerySOE.Interfaces")
    ][assembly: AssemblyCopyright("Copyright © ESRI 2010")][assembly:
    AssemblyTrademark("")][assembly: AssemblyCulture("")][assembly: AssemblyVersion(
    "1.0.0.0")][assembly: AssemblyFileVersion("1.0.0.0")][assembly:
    ComVisibleAttribute(true)][assembly: GuidAttribute(
    "8B929DF3-7D8A-4119-BEC8-FA8986CA10E3")]
[VB.NET]
Imports Microsoft.VisualBasic
Imports System.Reflection
Imports System.Runtime.CompilerServices
Imports System.Runtime.InteropServices
<Assembly
AssemblyTitle("SpatialQuerySOE.Interfaces_VBNet")>
<Assembly
AssemblyDescription("")>
<Assembly
AssemblyConfiguration("")>
<Assembly
AssemblyCompany("ESRI")>
<Assembly
AssemblyProduct("SpatialQuerySOE.Interfaces_VBNet")>
<Assembly
AssemblyCopyright("Copyright © ESRI 2010")>
<Assembly
AssemblyTrademark("")>
<Assembly
AssemblyCulture("")>
<Assembly
AssemblyVersion("1.0.0.0")>
<Assembly
AssemblyFileVersion("1.0.0.0")>
<Assembly
ComVisibleAttribute(True)>
<Assembly
Guid("07A3ECE1-FE0D-4aa3-A84F-72DE58F7CAEB")>

Creating the SOE project

Do the following steps to create the SOE project:
  1. In the same Visual Studio session used to create the SpatialQuerySOE.Interfaces_CSharp project, right-click the Implementation folder, click Add, then click New Project. The Add New Project dialog box appears.
  2. Under the Project types pane, click the Visual C# project type. Under the Templates pane, click Class Library.
  3. In the Location text box, type the path or click Browse for the location.
  4. Type SpatialQuerySOE_CSharp in the Name text box.
  5. Click OK. In the Solution Explorer, right-click Class1.cs and select Delete.
  6. Right-click the project in the Solution Explorer and select Properties.
  7. On the Application tab of the Properties page, specify an assembly name and a default namespace of SpatialQuerySOE.

Creating the Results class

You will create a C# class—named Extension—that represents the SOE, which provides a method to perform the spatial query, and returns an object of type Results that contains an array of graphic elements and a recordset. Before creating the SOE, create the Results (spatial query results) class. This class is used as a complex type to store the results of the SOE in a single object for use by the consuming client (for example, Web application).
Do the following steps to add the new class to the SpatialQuerySOE_CSharp project:
  1. In the Solution Explorer, right-click the SpatialQuerySOE_CSharp project, click Add, then click New Item. The Add New Item dialog box appears.
  2. Under the Templates pane, click Class.
  3. Type Results.cs in the Name text box.
  4. Click Add. A new class is added to your project and the code appears for the class with automatically generated code. Delete the automatically generated code.

Implementing the Results class

You need to add references to assemblies that contain the required ArcObjects for this class. Because this is a COM object to use in the GIS server, add additional references to support calling a managed .NET component from a COM client. The .NET component is the custom SOE assembly and the COM client is the container process, ArcSOC.exe. The COM client uses a COM callable wrapper to work with the .NET component. COM interop is the technology that permits .NET components and COM clients to work together.
Do the following steps to implement the Results class:
  1. Add references to the following assemblies:
    • ESRI.ArcGIS.Carto
    • ESRI.ArcGIS.Display 
    • ESRI.ArcGIS.Geodatabase
    • ESRI.ArcGIS.Geometry
    • ESRI.ArcGIS.Server
    • ESRI.ArcGIS.System
    • System.EnterpriseServices
  2. Add a reference to the SpatialQuerySOE.Interfaces_CSharp project in the same solution. If the SpatialQuerySOE.Interfaces_CSharp project is not in the same solution, add a reference to the SpatialQuerySOE.Interfaces assembly.
  3. Define the public COM class to contain implementation of the Results object. Since this class is exposed to COM, some COM attributes need to be added to the class definition (specifically, AutomationProxy, ClassInterface, and GuidAttribute). AutomationProxy determines if an object should be marshaled using the automation marshaller (true) or custom marshaled (false). In this case, use the automation marshaller. 

    ClassInterface defines how interfaces to the COM class are generated in the type library and exposed to a COM client. In this case, you want to support COM versioning and explicitly define the default interface by which a COM client accesses the class. Setting the ClassInterface attribute to ClassInterfaceType.None requires that you define an explicit interface (IResults) to work with your class (Results). Reference the GuidAttribute with a unique GUID to identify your class. 

    Derive your class from the ServicedComponent class to be hosted by COM clients. Implement the IResults interface as a default interface to interact with your COM object. Define two properties to get and set the graphic elements and recordset—ResultsGraphics and SummaryStatistics, respectively. See the following code example:   
[C#]
namespace SpatialQuerySOE
{
    [AutomationProxy(true), ClassInterface(ClassInterfaceType.None), GuidAttribute(
        "C9FB0536-D1D6-455d-897D-8AD26849C79A")]
    public class Results: ServicedComponent, SpatialQuerySOE.Interfaces.IResults
    {
        private ESRI.ArcGIS.Carto.IGraphicElements m_resultsGraphics;
        private ESRI.ArcGIS.Geodatabase.IRecordSet m_summaryStats;
        public ESRI.ArcGIS.Carto.IGraphicElements ResultsGraphics
        {
            get
            {
                return m_resultsGraphics;
            }
            set
            {
                m_resultsGraphics = (ESRI.ArcGIS.Carto.IGraphicElements)value;
            }
        }
        public ESRI.ArcGIS.Geodatabase.IRecordSet SummaryStatistics
        {
            get
            {
                return m_summaryStats;
            }
            set
            {
                m_summaryStats = (ESRI.ArcGIS.Geodatabase.IRecordSet)value;
            }
        }
    }
}
[VB.NET]
Namespace SpatialQuerySOE_VBNet
<AutomationProxy(True), ClassInterface(ClassInterfaceType.None), Guid("EC1526DE-97E9-4228-8972-DE50C4A71696")> _
                 Public Class Results
    Inherits ServicedComponent
    Implements SpatialQuerySOE.Interfaces_VBNet.IResults
    Private m_resultsGraphics As ESRI.ArcGIS.Carto.IGraphicElements
    Private m_summaryStats As ESRI.ArcGIS.Geodatabase.IRecordSet

    Public Property ResultsGraphics() As ESRI.ArcGIS.Carto.IGraphicElements Implements SpatialQuerySOE.Interfaces_VBNet.IResults.ResultsGraphics
        Get
        Return m_resultsGraphics
        End Get
        Set(ByVal Value As ESRI.ArcGIS.Carto.IGraphicElements)
        m_resultsGraphics = CType(Value, ESRI.ArcGIS.Carto.IGraphicElements)
        End Set
    End Property


    Public Property SummaryStatistics() As ESRI.ArcGIS.Geodatabase.IRecordSet Implements SpatialQuerySOE.Interfaces_VBNet.IResults.SummaryStatistics
        Get
        Return m_summaryStats
        End Get
        Set(ByVal Value As ESRI.ArcGIS.Geodatabase.IRecordSet)
        m_summaryStats = CType(Value, ESRI.ArcGIS.Geodatabase.IRecordSet)
        End Set
    End Property

End Class

End Namespace

Creating the SOE class

Do the following steps to add the new class to the project:
  1. In the Solution Explorer, right-click the SpatialQuerySOE_CSharp project, click Add, then click New Item. The Add New Item dialog box appears.
  2. Under the Templates pane, click Class.
  3. Type Extension.cs in the Name text box. 
  4. Click Add. A new class is added to the project and the code for the class appears with automatically generated code. Delete the automatically generated code. 

Implementing the SOE class

Now that you have implemented the Results class, you can implement the SOE class. SOEs extend a server object with additional interfaces to provide more specialized functionality. In this case, the SOE implements the IExtension custom interface. 
  1. Define the public COM class to contain the implementation of the Extension object. Since this class is exposed to COM, COM attributes need to be added to the class definition (specifically, AutomationProxy, ClassInterface, and GuidAttribute).

    AutomationProxy determines if an object should be marshaled using the automation marshaller (true) or custom marshaled (false). You will use the automation marshaller. ClassInterface defines how interfaces to the COM class are generated in the type library and exposed to a COM client. In this case, you want to support COM versioning and explicitly define the default interface by which a COM client accesses the class. Setting the ClassInterface attribute to ClassInterfaceType.None requires that you define an explicit interface (IExtension) to work with your class (Extension). Reference the GuidAttribute—a unique GUID—to identify your class. 

    Create a set of member variables to store a reference to an IServerObjectHelper (IServerObjectExtension implementation), an ILayer, two strings to account for layer name and field name (IExtension implementation), and an ILog (ILogSupport implementation). Populating these variables will be discussed in the next section. See the following code example:
[C#]
namespace SpatialQuerySOE
{
    [AutomationProxy(true), ClassInterface(ClassInterfaceType.None), GuidAttribute(
        "1932805D-7266-41a2-9428-0421A5617436")]
    public class Extension: ServicedComponent, SpatialQuerySOE.Interfaces.IExtension,
        ESRI.ArcGIS.Server.IServerObjectExtension,
        ESRI.ArcGIS.esriSystem.IObjectConstruct, ESRI.ArcGIS.esriSystem.ILogSupport,
        ESRI.ArcGIS.esriSystem.IObjectActivate
    {
        private ESRI.ArcGIS.Server.IServerObjectHelper m_ServerObjectHelper;
        private ESRI.ArcGIS.Carto.IFeatureLayer m_featureLayer;
        private string m_layerName;
        private string m_fieldName;
        private ESRI.ArcGIS.esriSystem.ILog m_log;
[VB.NET]
Namespace SpatialQuerySOE_VBNet
<AutomationProxy(True), ClassInterface(ClassInterfaceType.None), Guid("3896A0F5-1028-489c-81CD-F7757D117C9E")> _
                 Public Class Extension
    Inherits ServicedComponent
    Implements SpatialQuerySOE.Interfaces_VBNet.IExtension, ESRI.ArcGIS.Server.IServerObjectExtension, ESRI.ArcGIS.esriSystem.IObjectConstruct, ESRI.ArcGIS.esriSystem.ILogSupport, ESRI.ArcGIS.esriSystem.IObjectActivate
    Private m_ServerObjectHelper As ESRI.ArcGIS.Server.IServerObjectHelper
    Private m_featureLayer As ESRI.ArcGIS.Carto.IFeatureLayer
    Private m_layerName As String
    Private m_fieldName As String
    Private m_log As ESRI.ArcGIS.esriSystem.ILog
  1. Implement the following class and interfaces:
    • ServicedComponent (class)
    • ILogSupport
    • IServerObjectExtension
    • IObjectConstruct
    • IObjectActivate
    • IExtension
    The following illustration shows the order in which the methods of interfaces implemented by an SOE are called during initialization and used:

     
    See the following discussion and appropriate implementation code:
    • ServicedComponent—Derive the Extension class from the ServicedComponent class hosted by COM clients (for example, ArcSOC.exe). Additional code is not required.
    • ILogSupport—If you want your SOE to log messages to the GIS server's log file, your SOE should implement ILogSupport. ILogSupport is an optional interface for SOEs that has a single InitLogging method. InitLogging is called when the SOE is created and returns a reference to the GIS server's log object via the log argument. Once you have a reference to the server log, you will often call a single method—AddMessage()—to add information to the log. The AddMessage() method has the following parameters:
      • Level—Message's level of detail in relation to other messages. Levels are classified from 1 to 5 as follows:
        • 1 = Error
        • 2 = Warning
        • 3 = Normal
        • 4 = Detailed
        • 5 = Debug
        ArcGIS Server log file settings determine the messages that are included in the server log.
      • Code—Result code associated with the message. The code is an arbitrary integer value to uniquely define the source of the message. Codes 0 through 5999 are utilized by the service object manager (SOM). Codes 6000 and above can be generated by any serviced component (for example, MapServer, GeocodeServer, custom component, and so on).
      • Message—Custom string inserted into the GIS server log file. See the following code example:
[C#]
public void InitLogging(ESRI.ArcGIS.esriSystem.ILog log)
{
    m_log = log;
}
[VB.NET]
Public Sub InitLogging(ByVal Log As ESRI.ArcGIS.esriSystem.ILog) Implements ESRI.ArcGIS.esriSystem.ILogSupport.InitLogging
    m_log = Log
End Sub
    • IServerObjectExtension—A mandatory interface that must be supported by all SOEs and includes the Init and Shutdown methods. This interface is used by the server object to manage the SOE's life span. The server object cocreates the SOE and calls the Init method, handing it a reference to the server object via the server object helper argument. The server object helper implements a weak reference on the server object. The extension can keep a strong reference on the server object helper (for example, in a member variable) but should not keep a strong reference on the server object.

      Extensions should get the server object from the server object helper to make any method calls on the server object and release the reference after making the method calls. Init is called once, when the instance of the SOE is created. The Shutdown method is called once and informs the SOE that the server object's context is shutting down. In response, the SOE releases its reference on the server object helper. The log entries are informative and optional. See the following code example:
[C#]
public void Init(ESRI.ArcGIS.Server.IServerObjectHelper pSOH)
{
    m_ServerObjectHelper = pSOH;
    m_log.AddMessage(3, 8000, "SpatialQuerySOE custom message. Init called");
}

public void Shutdown()
{
    m_log.AddMessage(3, 8000, "SpatialQuerySOE custom message. Shutdown called");
    m_ServerObjectHelper = null;
    m_featureLayer = null;
    m_log = null;
}
[VB.NET]
Public Sub Init(ByVal pSOH As ESRI.ArcGIS.Server.IServerObjectHelper) Implements ESRI.ArcGIS.Server.IServerObjectExtension.Init
    m_ServerObjectHelper = pSOH
    m_log.AddMessage(3, 8000, "SpatialQuerySOE custom message. Init called")
End Sub


Public Sub Shutdown() Implements ESRI.ArcGIS.Server.IServerObjectExtension.Shutdown
    m_log.AddMessage(3, 8000, "SpatialQuerySOE custom message. Shutdown called")
    m_ServerObjectHelper = Nothing
    m_featureLayer = Nothing
    m_log = Nothing
End Sub
    • IObjectConstruct—If your SOE includes configuration properties or requires additional initialization logic, implement IObjectConstruct. IObjectConstruct is an optional interface for SOEs. The interface includes a single method called Construct. Construct is called only once—when the SOE is created—after IServerObjectExtension.Init is called. Include any expensive initialization logic within your implementation of Construct.

      Construct returns the configuration properties for the SOE as a property set. The configuration properties are stored in the server object configuration file. Configuration files are named <service name>.<server object type>.cfg and stored on the SOM machine in the <ArcGIS Install>\server\user\cfg directory. For example, a map service named Yellowstone has a configuration file named Yellowstone.MapServer.cfg. SOE properties configured to use with a server object are also stored in the .cfg file. In this case, a set of properties are read from the service's .cfg file. The property values, a feature layer name and field name, are validated to confirm they exist in the default map frame associated with the map server object. If an error occurs, the appropriate log entries are added. See the following code example:
[C#]
public void Construct(ESRI.ArcGIS.esriSystem.IPropertySet props)
{
    try
    {
        m_layerName = props.GetProperty("LayerName")as string;
        m_fieldName = props.GetProperty("FieldName")as string;
    }
    catch (Exception ex)
    {
        m_log.AddMessage(1, 8000, 
            "SpatialQuerySOE custom error. Error reading properties: " + ex.Message 
            + " " + props.Count.ToString());
        return ;
    }
    try
    {
        // Get the map underlying the map service and the IGeoFeatureLayers contained 
        //  in the map.
        ESRI.ArcGIS.Carto.IMapServer mapServer = (ESRI.ArcGIS.Carto.IMapServer)
            m_ServerObjectHelper.ServerObject;
        ESRI.ArcGIS.Carto.IMapServerObjects mapServerObjects = 
            (ESRI.ArcGIS.Carto.IMapServerObjects)mapServer;
        ESRI.ArcGIS.Carto.IMap map = mapServerObjects.get_Map
            (mapServer.DefaultMapName);
        ESRI.ArcGIS.esriSystem.UID layerTypeID = new ESRI.ArcGIS.esriSystem.UIDClass
            ();
        layerTypeID.Value = "{E156D7E5-22AF-11D3-9F99-00C04F6BC78E}";
        ESRI.ArcGIS.Carto.IEnumLayer enumLayer = map.get_Layers(layerTypeID, true);
        enumLayer.Reset();
        // Get the layer specified as the SOE layer.
        while ((m_featureLayer = enumLayer.Next()as ESRI.ArcGIS.Carto.IFeatureLayer)
            != null)
        {
            if (m_featureLayer.Name == m_layerName)
                break;
        }
        if (m_featureLayer == null)
        {
            m_log.AddMessage(1, 8000, "SpatialQuerySOE custom error: Layer " +
                m_layerName + " not found.");
            return ;
        }
        // Make sure the layer contains the field specified by the SOE's configuration.
        if (m_featureLayer.FeatureClass.FindField(m_fieldName) ==  - 1)
            m_log.AddMessage(1, 8000, "SpatialQuerySOE custom error: Field " +
                m_fieldName + " not found in layer " + m_layerName);
        else
            m_log.AddMessage(3, 8000, "SpatialQuerySOE successfully initialized.");
    }
    catch (Exception ex)
    {
        m_log.AddMessage(1, 8000, 
            "SpatialQuerySOE custom error: Failed to initialize extension: " +
            ex.Message + "::" + ex.StackTrace.Length.ToString());
    }
}
[VB.NET]
Public Overloads Sub Construct(ByVal props As ESRI.ArcGIS.esriSystem.IPropertySet) Implements ESRI.ArcGIS.esriSystem.IObjectConstruct.Construct
Try
m_layerName = TryCast(props.GetProperty("LayerName"), String)
m_fieldName = TryCast(props.GetProperty("FieldName"), String)
Catch ex As Exception
m_log.AddMessage(1, 8000, "SpatialQuerySOE custom error. Error reading properties: " & ex.Message & " " & props.Count.ToString())
Return
End Try
Try
' Get the map underlying the map service and the IGeoFeatureLayers contained in the map.
Dim mapServer As ESRI.ArcGIS.Carto.IMapServer = CType(m_ServerObjectHelper.ServerObject, ESRI.ArcGIS.Carto.IMapServer)
Dim mapServerObjects As ESRI.ArcGIS.Carto.IMapServerObjects = CType(mapServer, ESRI.ArcGIS.Carto.IMapServerObjects)
Dim map As ESRI.ArcGIS.Carto.IMap = mapServerObjects.Map(mapServer.DefaultMapName)
Dim layerTypeID As ESRI.ArcGIS.esriSystem.UID = New ESRI.ArcGIS.esriSystem.UIDClass()
layerTypeID.Value = "{E156D7E5-22AF-11D3-9F99-00C04F6BC78E}"
Dim enumLayer As ESRI.ArcGIS.Carto.IEnumLayer = map.Layers(layerTypeID, True)
enumLayer.Reset()
' Get the layer specified as the SOE layer.
m_featureLayer = TryCast(enumLayer.Next(), ESRI.ArcGIS.Carto.IFeatureLayer)
Do While m_featureLayer IsNot Nothing
    If m_featureLayer.Name = m_layerName Then
        Exit Do
    End If
    m_featureLayer = TryCast(enumLayer.Next(), ESRI.ArcGIS.Carto.IFeatureLayer)
Loop
If m_featureLayer Is Nothing Then
    m_log.AddMessage(1, 8000, "SpatialQuerySOE custom error: Layer " & m_layerName & " not found.")
    Return
End If
' Make sure the layer contains the field specified by the SOE's configuration.
If m_featureLayer.FeatureClass.FindField(m_fieldName) = -1 Then
    m_log.AddMessage(1, 8000, "SpatialQuerySOE custom error: Field " & m_fieldName & " not found in layer " & m_layerName)
Else
    m_log.AddMessage(3, 8000, "SpatialQuerySOE successfully initialized.")
End If
Catch ex As Exception
m_log.AddMessage(1, 8000, "SpatialQuerySOE custom error: Failed to initialize extension: " & ex.Message & "::" & ex.StackTrace.Length.ToString())
End Try
End Sub
    • IObjectActivate—While IServerObjectExtension.Init and IObjectConstruct.Construct are called once when the instance of the SOE is created, if your SOE requires logic to run each time its server context is acquired and released (each time a client calls CreateServerContext and ReleaseContext), implement IObjectActivate. IObjectActivate is an optional interface for SOEs that includes the following methods: 
      • Activate—Called each time a client calls CreateServerContext on the SOE's server object's context.
      • Deactivate—Called each time a client releases the context (via ReleaseContext).
      Because Activate and Deactivate are called each time a client gets and releases the server object's context, any logic implemented in these methods should not be expensive. In this case, add instructive information to the log file to indicate the method was called. See the following code example:
[C#]
void ESRI.ArcGIS.esriSystem.IObjectActivate.Activate()
{
    m_log.AddMessage(3, 8000, "SpatialQuerySOE custom message. Activate called");
}

void ESRI.ArcGIS.esriSystem.IObjectActivate.Deactivate()
{
    m_log.AddMessage(3, 8000, "SpatialQuerySOE custom message. Deactivate called");
}
[VB.NET]
Private Sub IObjectActivate_Activate() Implements ESRI.ArcGIS.esriSystem.IObjectActivate.Activate
    m_log.AddMessage(3, 8000, "SpatialQuerySOE custom message. Activate called")
End Sub


Private Sub Deactivate() Implements ESRI.ArcGIS.esriSystem.IObjectActivate.Deactivate
    m_log.AddMessage(3, 8000, "SpatialQuerySOE custom message. Deactivate called")
End Sub
    • IExtension—A custom interface implemented by the custom SOE to expose a method called by a client application. Implementing the QueryPoint method defined by the IExtension interface is the last step to complete the SOE. The QueryPoint method takes as arguments a point and a distance. In the method, add code that buffers the point to the specified distance, then query the layer's feature class for all polygons that intersect that buffer. For each polygon, the method clips it to the buffer, creates a graphic of the clipped polygon, and adds its area to a Dictionary object based on the value of the specified field. See the following code example:
[C#]
public SpatialQuerySOE.Interfaces.IResults QueryPoint(ESRI.ArcGIS.Geometry.IPoint
    point, double distance)
{
    if (m_featureLayer == null)
    {
        m_log.AddMessage(1, 8000, "SpatialQuerySOE custom error: layer not found");
        return null;
    }
    ESRI.ArcGIS.Geodatabase.IFeatureClass featureClass = m_featureLayer.FeatureClass;
    // Buffer the point.
    ESRI.ArcGIS.Geometry.ITopologicalOperator topologicalOperator = 
        (ESRI.ArcGIS.Geometry.ITopologicalOperator)point;
    ESRI.ArcGIS.Geometry.IGeometry queryGeometry = topologicalOperator.Buffer
        (distance);
    // Query the feature class.
    ESRI.ArcGIS.Geodatabase.ISpatialFilter spatialFilter = new
        ESRI.ArcGIS.Geodatabase.SpatialFilter();
    spatialFilter.Geometry = queryGeometry;
    spatialFilter.SpatialRel =
        ESRI.ArcGIS.Geodatabase.esriSpatialRelEnum.esriSpatialRelIntersects;
    spatialFilter.GeometryField = featureClass.ShapeFieldName;
    ESRI.ArcGIS.Geodatabase.IFeatureCursor resultsFeatureCursor =
        featureClass.Search(spatialFilter, true);
[VB.NET]
Public Function QueryPoint(ByVal point As ESRI.ArcGIS.Geometry.IPoint, ByVal distance As Double) As SpatialQuerySOE.Interfaces_VBNet.IResults Implements SpatialQuerySOE.Interfaces_VBNet.IExtension.QueryPoint
    If m_featureLayer Is Nothing Then
        m_log.AddMessage(1, 8000, "SpatialQuerySOE custom error: layer not found")
        Return Nothing
    End If
    Dim featureClass As ESRI.ArcGIS.Geodatabase.IFeatureClass = m_featureLayer.FeatureClass
    ' Buffer the point.
    Dim topologicalOperator As ESRI.ArcGIS.Geometry.ITopologicalOperator = CType(point, ESRI.ArcGIS.Geometry.ITopologicalOperator)
    Dim queryGeometry As ESRI.ArcGIS.Geometry.IGeometry = topologicalOperator.Buffer(distance)
    ' Query the feature class.
    Dim spatialFilter As ESRI.ArcGIS.Geodatabase.ISpatialFilter = New ESRI.ArcGIS.Geodatabase.SpatialFilter()
    spatialFilter.Geometry = queryGeometry
    spatialFilter.SpatialRel = ESRI.ArcGIS.Geodatabase.esriSpatialRelEnum.esriSpatialRelIntersects
    spatialFilter.GeometryField = featureClass.ShapeFieldName
    Dim resultsFeatureCursor As ESRI.ArcGIS.Geodatabase.IFeatureCursor = featureClass.Search(spatialFilter, True)
Before looping through the features, create a GraphicElements collection to hold the graphics, a simple fill symbol to apply to each graphic element, a dictionary object that you use to categorize the different feature types, and other necessary variables. The fill symbol is created using a helper method called createFillSymbol, which will be defined later. See the following code example:
[C#]
topologicalOperator = (ESRI.ArcGIS.Geometry.ITopologicalOperator)queryGeometry;
int classFieldIndex = featureClass.FindField(m_fieldName);
System.Collections.Specialized.ListDictionary summaryStatsDictionary = new
    System.Collections.Specialized.ListDictionary();
// Create the symbol and graphic elements collection for the graphics.
ESRI.ArcGIS.Display.ISimpleFillSymbol simpleFillSymbol = createFillSymbol();
ESRI.ArcGIS.Carto.IGraphicElements resultsGraphics = new
    ESRI.ArcGIS.Carto.GraphicElements();
[VB.NET]
topologicalOperator = CType(queryGeometry, ESRI.ArcGIS.Geometry.ITopologicalOperator)
Dim classFieldIndex As Integer = featureClass.FindField(m_fieldName)
Dim summaryStatsDictionary As New System.Collections.Specialized.ListDictionary()
' Create the symbol and graphic elements collection for the graphics.
Dim simpleFillSymbol As ESRI.ArcGIS.Display.ISimpleFillSymbol = createFillSymbol()
Dim resultsGraphics As ESRI.ArcGIS.Carto.IGraphicElements = New ESRI.ArcGIS.Carto.GraphicElements()
Loop through the features in the feature class that intersect the buffer geometry and clip each feature's polygon to the buffer. The resulting clipped geometry is then used to create a graphic that is added to the graphic collection. The area of the clipped geometry is added to the total area of the feature's type (as defined by the field specified in the SOE properties) in the Dictionary object. See the following code example:
[C#]
ESRI.ArcGIS.Geodatabase.IFeature resultsFeature;
while ((resultsFeature = resultsFeatureCursor.NextFeature()) != null)
{
    // Create the graphic.
    ESRI.ArcGIS.Carto.IFillShapeElement fillShapeElement = new
        ESRI.ArcGIS.Carto.PolygonElement()as ESRI.ArcGIS.Carto.IFillShapeElement;
    ESRI.ArcGIS.Carto.IElement element = fillShapeElement as
        ESRI.ArcGIS.Carto.IElement;
    // Clip the geometry.
    ESRI.ArcGIS.Geometry.IGeometry clippedResultsGeometry =
        topologicalOperator.Intersect(resultsFeature.Shape,
        ESRI.ArcGIS.Geometry.esriGeometryDimension.esriGeometry2Dimension);
    element.Geometry = clippedResultsGeometry;
    fillShapeElement.Symbol = simpleFillSymbol;
    ESRI.ArcGIS.Carto.IGraphicElement resultsGraphicElement = fillShapeElement as
        ESRI.ArcGIS.Carto.IGraphicElement;
    resultsGraphics.Add(resultsGraphicElement);
    // Get statistics and add to dictionary.
    ESRI.ArcGIS.Geometry.IArea area = clippedResultsGeometry as
        ESRI.ArcGIS.Geometry.IArea;
    string resultsClass = resultsFeature.get_Value(classFieldIndex)as string;
    // If the class is already in the dictionary, add the current feature's area to the 
    //  existing entry.
    if (summaryStatsDictionary.Contains(resultsClass))
        summaryStatsDictionary[resultsClass] = (double)
            summaryStatsDictionary[resultsClass] + area.Area;
    else
        summaryStatsDictionary[resultsClass] = area.Area;
}
[VB.NET]
Dim resultsFeature As ESRI.ArcGIS.Geodatabase.IFeature
resultsFeature = resultsFeatureCursor.NextFeature()
Do While resultsFeature IsNot Nothing
    ' Create the graphic.
    Dim fillShapeElement As ESRI.ArcGIS.Carto.IFillShapeElement = TryCast(New ESRI.ArcGIS.Carto.PolygonElement, ESRI.ArcGIS.Carto.IFillShapeElement)
    Dim element As ESRI.ArcGIS.Carto.IElement = TryCast(fillShapeElement, ESRI.ArcGIS.Carto.IElement)
    ' Clip the geometry.
    Dim clippedResultsGeometry As ESRI.ArcGIS.Geometry.IGeometry = topologicalOperator.Intersect(resultsFeature.Shape, ESRI.ArcGIS.Geometry.esriGeometryDimension.esriGeometry2Dimension)
    element.Geometry = clippedResultsGeometry
    fillShapeElement.Symbol = simpleFillSymbol
    Dim resultsGraphicElement As ESRI.ArcGIS.Carto.IGraphicElement = TryCast(fillShapeElement, ESRI.ArcGIS.Carto.IGraphicElement)
    resultsGraphics.Add(resultsGraphicElement)
    ' Get statistics and add to dictionary.
    Dim area As ESRI.ArcGIS.Geometry.IArea = TryCast(clippedResultsGeometry, ESRI.ArcGIS.Geometry.IArea)
    Dim resultsClass As String = TryCast(resultsFeature.Value(classFieldIndex), String)
    ' If the class is already in the dictionary, add the current feature's area to the existing entry.
    If summaryStatsDictionary.Contains(resultsClass) Then
        summaryStatsDictionary(resultsClass) = CDbl(summaryStatsDictionary(resultsClass)) + area.Area
    Else
        summaryStatsDictionary(resultsClass) = area.Area
    End If
    resultsFeature = resultsFeatureCursor.NextFeature()
Loop
The summary statistics Dictionary object will have a key for each unique value of the specified field and a value that is the total area within the buffer of features that share the unique value. Create a recordset object and copy the keys and values from the dictionary into rows and fields in the recordset. This is accomplished using the createSummaryRecordSet helper method, which will be implemented later. See the following code example:
[C#]
ESRI.ArcGIS.Geodatabase.IRecordSet summaryStatsRecordSet = createSummaryRecordSet
    (summaryStatsDictionary);
[VB.NET]
Dim summaryStatsRecordSet As ESRI.ArcGIS.Geodatabase.IRecordSet = createSummaryRecordSet(summaryStatsDictionary)
Finally, since the QueryPoint function returns a SpatialQuerySOE.Results object, the last part of the function creates a Results object, sets the graphic collection and summary recordset in the object, and returns the object to the caller. The new Results object is referenced using the SpatialQuerySOE.Interfaces.IResults interface. The client application works with the interface to process the results. See the following code example:
[C#]
SpatialQuerySOE.Interfaces.IResults results = new Results();
results.ResultsGraphics = resultsGraphics;
results.SummaryStatistics = summaryStatsRecordSet;
return results;
[VB.NET]
Dim results As SpatialQuerySOE.Interfaces_VBNet.IResults = New Results()
results.ResultsGraphics = resultsGraphics
results.SummaryStatistics = summaryStatsRecordSet
Return results
  1. As previously described, the QueryPoint method uses two helper methods to create a fill symbol (createFillSymbol) and to copy the contents of a Dictionary object to a recordset (createSummaryRecordSet). Implement these helper methods in your SOE class. The createSummaryRecordSet method takes a Dictionary object as an argument and returns a recordset. The function creates a recordset with a field for the type of feature (key) and a field for the total area (value). It then loops through the keys and values in the dictionary and creates a row in the recordset for each key and value pair. See the following code example:
[C#]
private ESRI.ArcGIS.Geodatabase.IRecordSet createSummaryRecordSet
    (System.Collections.Specialized.ListDictionary summaryStatsDictionary)
{
    // Initialize the summary statistics recordset.
    ESRI.ArcGIS.Geodatabase.IRecordSet summaryStatsRecordSet = new
        ESRI.ArcGIS.Geodatabase.RecordSet();
    ESRI.ArcGIS.Geodatabase.IRecordSetInit recordSetInit = summaryStatsRecordSet as
        ESRI.ArcGIS.Geodatabase.IRecordSetInit;
    ESRI.ArcGIS.Geodatabase.IFields summaryFields = new
        ESRI.ArcGIS.Geodatabase.Fields();
    ESRI.ArcGIS.Geodatabase.IFieldsEdit summaryFieldsEdit = summaryFields as
        ESRI.ArcGIS.Geodatabase.IFieldsEdit;
    summaryFieldsEdit.FieldCount_2 = 2;
    ESRI.ArcGIS.Geodatabase.IField field = new ESRI.ArcGIS.Geodatabase.Field();
    ESRI.ArcGIS.Geodatabase.IFieldEdit fieldEdit = field as
        ESRI.ArcGIS.Geodatabase.IFieldEdit;
    fieldEdit.Name_2 = "Type";
    fieldEdit.Type_2 = ESRI.ArcGIS.Geodatabase.esriFieldType.esriFieldTypeString;
    fieldEdit.Length_2 = 50;
    summaryFieldsEdit.set_Field(0, field);
    field = new ESRI.ArcGIS.Geodatabase.Field();
    fieldEdit = field as ESRI.ArcGIS.Geodatabase.IFieldEdit;
    fieldEdit.Name_2 = "Area";
    fieldEdit.Type_2 = ESRI.ArcGIS.Geodatabase.esriFieldType.esriFieldTypeDouble;
    summaryFieldsEdit.set_Field(1, field);
    recordSetInit.CreateTable(summaryFields);
    ESRI.ArcGIS.Geodatabase.ICursor cursor = recordSetInit.Insert();
    ESRI.ArcGIS.Geodatabase.IRowBuffer rowBuffer = recordSetInit.CreateRowBuffer();
    // Copy the summary stats to the recordset.
    System.Collections.IDictionaryEnumerator summaryStatsEnumerator =
        summaryStatsDictionary.GetEnumerator();
    while (summaryStatsEnumerator.MoveNext())
    {
        rowBuffer.set_Value(0, summaryStatsEnumerator.Key);
        rowBuffer.set_Value(1, summaryStatsEnumerator.Value);
        cursor.InsertRow(rowBuffer);
    }
    return summaryStatsRecordSet;
}
[VB.NET]
Private Function createSummaryRecordSet(ByVal summaryStatsDictionary As System.Collections.Specialized.ListDictionary) As ESRI.ArcGIS.Geodatabase.IRecordSet
    ' Initialize the summary statistics recordset.
    Dim summaryStatsRecordSet As ESRI.ArcGIS.Geodatabase.IRecordSet = New ESRI.ArcGIS.Geodatabase.RecordSet()
    Dim recordSetInit As ESRI.ArcGIS.Geodatabase.IRecordSetInit = TryCast(summaryStatsRecordSet, ESRI.ArcGIS.Geodatabase.IRecordSetInit)
    Dim summaryFields As ESRI.ArcGIS.Geodatabase.IFields = New ESRI.ArcGIS.Geodatabase.Fields()
    Dim summaryFieldsEdit As ESRI.ArcGIS.Geodatabase.IFieldsEdit = TryCast(summaryFields, ESRI.ArcGIS.Geodatabase.IFieldsEdit)
    summaryFieldsEdit.FieldCount_2 = 2
    Dim field As ESRI.ArcGIS.Geodatabase.IField = New ESRI.ArcGIS.Geodatabase.Field()
    Dim fieldEdit As ESRI.ArcGIS.Geodatabase.IFieldEdit = TryCast(field, ESRI.ArcGIS.Geodatabase.IFieldEdit)
    fieldEdit.Name_2 = "Type"
    fieldEdit.Type_2 = ESRI.ArcGIS.Geodatabase.esriFieldType.esriFieldTypeString
    fieldEdit.Length_2 = 50
    summaryFieldsEdit.Field_2(0) = field
    field = New ESRI.ArcGIS.Geodatabase.Field()
    fieldEdit = TryCast(field, ESRI.ArcGIS.Geodatabase.IFieldEdit)
    fieldEdit.Name_2 = "Area"
    fieldEdit.Type_2 = ESRI.ArcGIS.Geodatabase.esriFieldType.esriFieldTypeDouble
    summaryFieldsEdit.Field_2(1) = field
    recordSetInit.CreateTable(summaryFields)
    Dim cursor As ESRI.ArcGIS.Geodatabase.ICursor = recordSetInit.Insert()
    Dim rowBuffer As ESRI.ArcGIS.Geodatabase.IRowBuffer = recordSetInit.CreateRowBuffer()
    ' Copy the summary stats to the recordset.
    Dim summaryStatsEnumerator As System.Collections.IDictionaryEnumerator = summaryStatsDictionary.GetEnumerator()
    Do While summaryStatsEnumerator.MoveNext()
        rowBuffer.Value(0) = summaryStatsEnumerator.Key
        rowBuffer.Value(1) = summaryStatsEnumerator.Value
        cursor.InsertRow(rowBuffer)
    Loop
    Return summaryStatsRecordSet
End Function
The createFillSymbol method creates and returns a SimpleFillSymbol object, which is a hollow fill symbol with a green outline. The client application can choose to use this rendering scheme when working with SOE results. See the following code example:
[C#]
private ESRI.ArcGIS.Display.ISimpleFillSymbol createFillSymbol()
{
    ESRI.ArcGIS.Display.ISimpleLineSymbol simpleLineSymbol = new
        ESRI.ArcGIS.Display.SimpleLineSymbol();
    ESRI.ArcGIS.Display.IRgbColor rgbColor = new ESRI.ArcGIS.Display.RgbColor();
    rgbColor.Red = 0;
    rgbColor.Green = 255;
    rgbColor.Blue = 0;
    simpleLineSymbol.Color = rgbColor;
    simpleLineSymbol.Style = ESRI.ArcGIS.Display.esriSimpleLineStyle.esriSLSSolid;
    simpleLineSymbol.Width = 2;
    ESRI.ArcGIS.Display.ISimpleFillSymbol simpleFillSymbol = new
        ESRI.ArcGIS.Display.SimpleFillSymbol();
    simpleFillSymbol.Outline = simpleLineSymbol;
    simpleFillSymbol.Style = ESRI.ArcGIS.Display.esriSimpleFillStyle.esriSFSHollow;
    return simpleFillSymbol;
}
[VB.NET]
Private Function createFillSymbol() As ESRI.ArcGIS.Display.ISimpleFillSymbol
    Dim simpleLineSymbol As ESRI.ArcGIS.Display.ISimpleLineSymbol = New ESRI.ArcGIS.Display.SimpleLineSymbol()
    Dim rgbColor As ESRI.ArcGIS.Display.IRgbColor = New ESRI.ArcGIS.Display.RgbColor()
    rgbColor.Red = 0
    rgbColor.Green = 255
    rgbColor.Blue = 0
    simpleLineSymbol.Color = rgbColor
    simpleLineSymbol.Style = ESRI.ArcGIS.Display.esriSimpleLineStyle.esriSLSSolid
    simpleLineSymbol.Width = 2
    Dim simpleFillSymbol As ESRI.ArcGIS.Display.ISimpleFillSymbol = New ESRI.ArcGIS.Display.SimpleFillSymbol()
    simpleFillSymbol.Outline = simpleLineSymbol
    simpleFillSymbol.Style = ESRI.ArcGIS.Display.esriSimpleFillStyle.esriSFSHollow
    Return simpleFillSymbol
End Function

Setting assembly properties for the SpatialQuerySOE_CSharp project

Do the following steps to set assembly properties:
  1. In the SpatialQuerySOE_CSharp project, open the AssemblyInfo.cs file.
  2. Modify the AssemblyInfo.cs file to reflect the settings in the following code example. Set ComVisibleAttribute to true (or remove the value since the default is true). Setting this on the assembly makes all public types visible to COM clients. The ComVisibleAttribute can also be applied to individual types in the assembly.
[C#]
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("SpatialQuerySOE")][assembly: AssemblyDescription("")
    ][assembly: AssemblyConfiguration("")][assembly: AssemblyCompany("ESRI")
    ][assembly: AssemblyProduct("SpatialQuerySOE")][assembly: AssemblyCopyright(
    "Copyright © ESRI 2010")][assembly: AssemblyTrademark("")][assembly:
    AssemblyCulture("")][assembly: AssemblyVersion("1.0.0.0")][assembly:
    AssemblyFileVersion("1.0.0.0")][assembly: ComVisibleAttribute(true)][assembly:
    GuidAttribute("C1E4CA92-0380-4ed1-ABF4-9F45C9914D6D")]
[VB.NET]
Imports Microsoft.VisualBasic
Imports System.Reflection
Imports System.Runtime.CompilerServices
Imports System.Runtime.InteropServices
<Assembly
AssemblyTitle("SpatialQuerySOE_VBNet")>
<Assembly
AssemblyDescription("")>
<Assembly
AssemblyConfiguration("")>
<Assembly
AssemblyCompany("ESRI")>
<Assembly
AssemblyProduct("SpatialQuerySOE_VBNet")>
<Assembly
AssemblyCopyright("Copyright © ESRI 2010")>
<Assembly
AssemblyTrademark("")>
<Assembly
AssemblyCulture("")>
<Assembly
AssemblyVersion("1.0.0.0")>
<Assembly
AssemblyFileVersion("1.0.0.0")>
<Assembly
ComVisibleAttribute(True)>
<Assembly
Guid("AE0C359B-55AE-4bb4-AD9F-A78EF0D0660B")>

Building the SOE projects

Build the SpatialQuerySOE.Interfaces_CSharp and SpatialQuerySOE_CSharp projects. Each project creates an assembly, SpatialQuerySOE.Interfaces.dll and SpatialQuerySOE.dll.


See Also:

Walkthrough: Creating a server object extension
How to create SOE property pages
How to register the SOE
How to create an SOE client
Sample: Server spatial query server object extension