Introduction

Enzo Server
  Installation
  Quick Start
  Core Features
    Async Calls
    ConnectionStrings
    Edge Cache
    HTTP Access
    Scheduling
    Views
  Advanced Capabilities
    Change Data Capture
  Administration
    Configuration Settings
    Logins & ACL
    Auditing
    Current Executions
    Linked Server
    SSL

  User Guides
     Sharding
     Sharding Overview

     SharePoint

   Adapters
    All Adapters


  SDK
    Overview
    Installation
    DevHost
    Create A Simple Adapter
    Best Practices
    Developer Guide
      Handler Columns
      Handler Options
      Handler Decorators
      Dynamic Columns
      Table & Table Enumerators
      Virtual Tables

/sdk/firstadapter




Create A Simple Adapter


This section describes the steps needed to create a simple adapter, add a few handlers, and debug the adapter using the DevHost. You will build an adapter that returns information from the local machine where Enzo is running.

You must start Visual Studio in Administrative mode in order to debug your adapter.


New Project in Visual Studio

Before you can write your first adapter you must have followed the installation procedures.

  • Start Visual Studio in Administrative mode

  • Create a new Project
    • Select File -> New Project
    • Choose Enzo Unified DataAdapter from the list of available project types
    • Enter a name for the project (ex: ServerInfo) and the path where the project will be stored; the path should be under a folder that is the name of your namespace (a short name that is unique to you). The Blue Syntax Consulting namespace is BSC, so the path could be set to:
      C:\Program Files\Enzo Unified SDK\Adapters\BSC\
    • Uncheck the "Create a Subdirectory for Solution" option
    • The .NET Framework should be set to 4.6.1 or higher


  • Enter the project settings for your adapter; select the Supports Configuration option and click Okay.


  • Make sure your project compiles, and that you can run the adapter by pressing F5; check the troubleshooting section of the installation document if you experience any issues


Add Configuration Settings

The first step is to define configuration settings for this adapter. Configuration Settings are used by the adapter to retrieve secrets (such as API Keys) or default settings (such as RetryCount) that the adapter will observe. Adding configuration settings for an adapter is optional; however it is highly recommended to use these settings instead of hard-coding adapter settings in your code.

The RegisterConfig() method defines the settings that can be defined for the adapter and should be called inside the RegisterAdapter() method which is automatically added to your adapter code.

Since we selected the Supports Configuration option earlier when we created the adapter project, you will see the RegisterConfig() method in your adapter already added with a few properties. Our sample adapter will be using different properties, so replace the RegisterConfig() line with this:

RegisterConfig( "defaultPath|The default path to use when none is specified" );

The above call defines a defaultPath property as a configuration setting. Since no data type was provided for defaultPath, the string data type is assumed. This property has no default value, and is not required. You can add as many configuration settings as you need.

You can specify a default value for a config setting and make it a required setting. The general declaration syntax is as follows:
type varName|description|defaultVal|r

If you want to set the defaultPath property to C:\ and make it a required property, it would look like this:
RegisterConfig( "defaultPath|The default path to use when none is specified|c:\|r" );

or with the data type:
RegisterConfig( "string defaultPath|The default path to use when none is specified|c:\|r" );


Add a Handler

Let's define a handler for this adapter called GetFiles. This handler will return the list of files available under the path provided (or the defaultPath), along with basic file properties. Handlers are also defined inside the RegisterAdapter() method.

There are many advanced options available when declaring a handler. This section provides a high level overview, and a simple registration example.

The general format for declaring a handler is as follows:

RegisterHandler("procName,tableName|groupName",
  HandlerOptions,
  "description",
  new[] { "SQLexample1", "SQLexample2" },
  methodName,
  new[] { "field1", "field2" },
  new[] { "col1", "col2" }
);

Handler Name

The name of the handler defines how it will be called by end users. The procName refers to a Stored Procedure name or a REST endpoint name. The tableName is optional, and represents the name of the handler as a Table. In other words the handler can be called using EXEC procName, or SELECT .. FROM tableName, UPDATE tableName..., DELETE FROM tableName and INSERT INTO tableName.

The tableName and groupName arguments are both optional.

  • tableName: used when a name other than the Stored Procedure name shoul be used when SELECT, UPDATE, DELETE and INSERT operations are allowed on the handler.

  • groupName: used to group handlers by functional area so that they are displayed as a group when using the built-in EXEC adapterName.help method. Has no functional purpose.

Examples of valid handler names:
Handler name Proc Name Table Name Handler Group
GetHiddenFiles GetHiddenFiles
GetFiles,Files GetFiles Files
UpdateDocument,documents UpdateDocument documents
DeleteDocument,documents|Document DeleteDocument documents Document

NOTE: You can create as many handlers as you want; however handler names must be unique by operation. In other words you can only have one handler name for an EXEC command; but you can have multiple identical tableName defined (one per table operation as defined by the HandlerOptions).

Handler Options

Handler Options define how a handler can be called. By default all handlers can be called as an EXEC operation.

See the Handler Options section for a list of supported values.

Add the GetFiles Handler

The following code creates the GetFiles handler, using Files as the table name for the handler, and making it available as a SELECT operation.

RegisterHandler(
    "GetFiles,Files",
    HandlerOptions.Select | HandlerOptions.HttpGET ,
    "Gets files from a local directory or from a UNC path",
    new[] { "EXEC ServerInfo.GetFiles 'c:\'", "SELECT * FROM ServerInfo.Files WHERE path='c:\'" },
    GetFiles,
    new[] {
        "string path|The path to inspect"
    },
    new[]
    {
        "path|The path of the file",
        "fullName|The full path to the file including the file name",
        "long size|File size in bytes",
        "bool directory|True for a directory"
    }
);


Once the above hander has been defined, all you need is to implement the GetFiles method in the adapter class. An explanation of the code is provided as part of comments in the code below.

private EventResult GetFiles(object sender, ExecEventArgs e)
{
    // Create a EventResult object which is what we will return from this method
    EventResult res = new EventResult(e);

    // Get the path as provided as an argument; if null, look for the configuration setting property called defaultPath
    // If all fails, assume c:\ as the default path to look into
    string path = e.GetArg("path") ?? e.GetSetting("defaultPath", @"c:\");
    
    // If argument validation is all we need, exit now. Don't do the work.
    if (e.IsArgValidation)
        return res;
    
    // Get the list of files as requested including folders 
    // We could expand the handler to also accept the search pattern, and if the search should be recursive.
    string[] files = System.IO.Directory.GetFileSystemEntries(path, "*.*", System.IO.SearchOption.TopDirectoryOnly);
    
    foreach(string file in files)
    {
        // Add a record for each returned item; break if we have reached the TOP directive

        // Is this a file?
        if (!System.IO.Directory.Exists(file))
        {
            // Get file information for each file
            var info = new System.IO.FileInfo(file);
            
            // Add the file now using the AddResultRow method
            // This method returns false if we have reached the number of maximum records requested
            // or if the caller cancelled the query
            if (!res.AddResultRow(path, file, info.Length, false))
                break;
        }
        else
        {
            // Add the directory to the result
            // Specify a NULL value using DBNull.Value 
            if (!res.AddResultRow(path, file, DBNull.Value, true))
                break;
        }
    }

    // Return the result to the base adapter
    return res;
}


Test the Adapter

Before running the adapter, the source code should look like this:

using System;
using BSC.Enzo.Unified;
using BSC.Enzo.Unified.Logging;

namespace BSC
{
    public partial class ServerInfo : DataAdapter
    {
        #region DataAdapter overrides
        public override void RegisterAdapter()
        {
            #region Descriptive details
            Title = "ServerInfo";
            Description = "Sample adapter returning information from the local server";
            Author = "Blue Syntax Consulting";
            AuthorCopyright = "Copyright (c) 2020 Blue Syntax Consulting. All Rights Reserved.";
            AuthorUrl = "www.enzounified.com";
            AuthorEmail = "info@enzounified.com";
            LogSeverityThreshold = Severity.Debug;
            #endregion


            #region Register any configuration variables that can be referenced by your DataAdapter (optional)
            RegisterConfig(
                    "defaultPath|The default path to use when none is specified"
                );
            #endregion

            RegisterHandler(
                "GetFiles,Files",
                HandlerOptions.Select | HandlerOptions.HttpGET ,
                "Gets files from a local directory or from a UNC path",
                new[] { "" },
                GetFiles,
                new[] {
                    "string path|The path to inspect"
                },
                new[] 
                {
                    "path|The path of the file",
                    "fullName|The full path to the file including the file name",
                    "long size|File size in bytes",
                    "bool directory|True for a directory"
                }
                );

        }
        #endregion

        private EventResult GetFiles(object sender, ExecEventArgs e)
        {
            EventResult res = new EventResult(e);

            // Get the path
            string path = e.GetArg("path") ?? e.GetSetting("defaultPath", @"c:\");
            
            if (e.IsArgValidation)
                return res;

            string[] files = System.IO.Directory.GetFileSystemEntries(path, "*.*", System.IO.SearchOption.TopDirectoryOnly);

            foreach(string file in files)
            {
                // Add a record for each returned item; break if we have reached the TOP directive

                if (!System.IO.Directory.Exists(file))
                {
                    // Get file information for each file
                    var info = new System.IO.FileInfo(file);

                    if (!res.AddResultRow(path, file, info.Length, false))
                        break;
                }
                else
                {
                    // Add directory
                    if (!res.AddResultRow(path, file, DBNull.Value, true))
                        break;
                }
            }

            return res;
        }

    }
}



You can now run the adapter by pressing F5. The DevHost may take a minute or two to start. Once it has started, type the following SQL commands to test your handler:

EXEC ServerInfo.GetFiles 'c:\Windows'
SELECT * FROM ServerInfo.Files WHERE path = 'c:\windows'
SELECT TOP 5 * FROM ServerInfo.files WHERE path = 'c:\windows' AND size > 0 AND fullName like '%.exe'

As you can see above, the SQL command is more flexible because you can specify the TOP operator, and provide a WHERE clause the provides the path argument, but also provides a filter on the data returned. The AddResultRow automatically performs the TOP and filtering capability for you.