-----------------------------------

Acquista i software ArcGIS tramite Studio A&T srl, rivenditore autorizzato dei prodotti Esri.

I migliori software GIS, il miglior supporto tecnico!

I migliori software GIS, il miglior supporto tecnico!
Azienda operante nel settore GIS dal 2001, specializzata nell’utilizzo della tecnologia ArcGIS e aderente ai programmi Esri Italia Business Network ed Esri Partner Network

-----------------------------------



sabato 10 ottobre 2015

ArcGIS Pro: Nun te regghe Queued(Task)

ArcGIS Pro si differenzia nettamente dalle applicazioni ArcGIS desktop esistenti, poichè è sviluppato secondo un'architettura multithread per sfruttare le moderne CPU/GPU con più core di esecuzione. Per lo sviluppatore di add-in che vuole estendere le funzionalità di ArcGIS Pro, questo significa modificare il modello di programmazione e familiarizzare, in un primo momento, con alcuni nuovi concetti che possono apparire bizzarri. Ovviamente, come con qualsiasi novità, lavorare con questi modelli diventerà gradualmente sempre più facile, ed i benefici del multithreading saranno sempre più evidenti.
Sfide che affronterai nella programmazione multithreading:
Le seguenti quattro differenze fondamentali distinguono le applicazioni multithread tra cui ArcGIS Pro, da una classica applicazione a singolo thread singolo:
  • Per garantire un’esperienza d’uso (UX) responsiva, il thread dell'interfaccia grafica utente (GUI) deve essere in grado di ricevere l'input da parte dell'utente e produrre output senza intoppi e senza interruzioni. Ciò significa che le esecuzioni delle azioni programmate devono essere eseguite in modo asincrono su thread di lavoro separati; il thread della GUI non dovrebbe mai svolgere un lavoro di qualsiasi tipo che blocchi nell’attesa. Ciò è in contrasto con le esistenti applicazioni ArcGIS desktop dove la maggior parte del lavoro viene eseguito direttamente sul singolo thread della GUI.
  • Mentre il lavoro è in esecuzione sui thread in background, agli utenti deve essere presentata un’interfaccia utente logicamente coerente e informativa. Comandi, strumenti, e altre parti dell'interfaccia utente devono essere attivate o disattivate opportunamente basandosi su quali operazioni sono in esecuzione, e dovrebbe essere fornito un adeguato feedback. Se un'operazione di lunga esecuzione è logicamente annullabile, dovrebbe essere fornita la possibilità di annullarla.
  • Operazioni che possono generare conflitto non dovrebbero essere eseguite simultaneamente e devono sempre essere eseguite in una sequenza logica appropriata. Ad esempio, le operazioni su una mappa non possono essere eseguite mentre il progetto che contiene la mappa è ancora in fase di caricamento ed un insieme di feature selezionate non può essere cancellato finché la selezione stessa non è stata completata. La maggior parte delle operazioni avviate tramite l'interazione dell'utente sono logicamente dipendenti dall’ordine e dovrebbero essere eseguite in serie.
  • Bisogna fare attenzione a garantire che l'accesso allo stato volatile, cioè l'accesso alle variabili all'interno del programma, sia correttamente sincronizzato quando tale stato è condiviso tra i thread. Ad esempio, se un oggetto collection è condiviso tra un thread di lavoro e il thread della GUI, entrambi i thread devono essere coordinati per l’accesso alla collection in modo che un thread non legga gli elementi della collection, mentre un altro thread simultaneamente sta aggiungendo o rimuovendo elementi. Questo tipo di protezione è comune a tutti i tipi di programmazione multithreaded e viene normalmente realizzato utilizzando un lock. In un'applicazione in cui più parti indipendenti possono estendere il comportamento dell'applicazione, le operazioni di coordinamento possono diventare complesse e fuori controllo senza un comune framework che orchestra i vari componenti a lavorare insieme.


Il modello di threading interno ad ArcGIS Pro

Gli ingegneri di Esri hanno posto la massima priorità per rendere ArcGIS Pro facile da programmare il più possibile nella nuova architettura multithread. A tal fine, ArcGIS Pro incorpora le più recenti funzionalità della programmazione asincrona fornita da Microsoft insieme a nuove infrastrutture threading specifiche dell'applicazione su misura per ridurre la complessità del codice.
Nella maggior parte dei casi, gli sviluppatori di add-in dovrebbero solo aver bisogno di trattare con due thread: il thread dell'interfaccia utente e un thread di lavoro specializzato forniti dall'applicazione.
Internamente, ArcGIS Pro utilizza un gran numero di thread per vari scopi, tra cui la rasterizzazione, il rendering dei grafici, il caricamento dei dati e la selezione degli algoritmi di geoprocessing che sfruttano il parallelismo per velocizzare il calcolo. Mantenere tutte queste attività senza blocchi e senza conflitti richiede una notevole attività di coordinamento e di complessità associata; per questo motivo, questi thread sono completamente interni e isolati agli sviluppatori nell'ambito delle implementazioni con l’SDK pubblico. Quando viene chiamato un metodo nella API pubblica, l'implementazione interna può, quando applicabile, dividere l’operazione e delegare parti ad uno o più di questi thread interni specializzati, o accodare le operazioni che alla fine vengono eseguite all'interno di un processo esterno o un servizio web .



Attività (Tasks) e il modello asincrono basato su attività (TAP)

I metodi all'interno ArcGIS Pro SDK rientrano in tre categorie:
- Metodi asincroni che possono essere chiamati in qualsiasi thread. Metodi di questo tipo sono nominati utilizzando il suffisso Async e di solito restituiscono Tasks. In alcuni casi, può essere fornita sia la versione sincrona che asincrona del metodo.
- Metodi sincroni che dovrebbero essere chiamati solo sul thread di lavoro. Metodi di questo tipo sono segnalati nella guida in linea delle API e un tip sul codice appariranno quando siamo col cursore sul metodo.
- Metodi sincroni che dovrebbero essere chiamati solo sul thread della GUI. Questi tipi di metodi sono di solito associati con WPF.
Se un metodo su un particolare oggetto viene chiamato sul thread sbagliato, la chiamata genera un'eccezione di tipo ArcGIS.Core.CalledOnWrongThreadException. Se non sei sicuro su un particolare caso, è possibile consultare la guida del componente SDK o l’help fornito da Microsoft per determinare se un particolare metodo o una proprietà ha delle restrizioni.
All'interno dell’SDK - in particolare all'interno del namespace ArcGIS.Core – il thread di lavoro associa metodi e proprietà che tendono ad essere a grana fine. Per ridurre il tempo associato con la schedulazione e la commutazione di contesto, questi metodi sono sincroni e devono essere utilizzati nel codice usando task.
Il .NET Task Parallel Library di Microsoft (TPL) e il modello di programmazione associata noto come il modello asincrono basato su attività (TAP) semplificano la creazione di codice asincrono all'interno di un'applicazione multithread. La classe Task viene utilizzata per rappresentare un'operazione eseguita in modo asincrono.
Nel seguente esempio, il metodo PrintReportAsync viene richiamato e restituisce immediatamente un oggetto Task al chiamante. Nel frattempo, la funzione di stampa continua l’esecuzione in background su un altro thread.

    private void Button_Click(object sender, RoutedEventArgs e)
    {
      Task t = PrintReportAsync ("HP1");
      // Attendo che il task abbia finito.
      t.Wait ();
      MessageBox.Show ("Il Report è pronto!");
    }

Questo esempio vuole mostrare un messaggio quando la stampa è completata e utilizza il metodo Wait sull'oggetto Task restituito per sospendere il thread chiamante fino a quando il task ha completato il suo compito.
Questo approccio presenta due grandi inconvenienti: in primo luogo il thread chiamante non può fare altro mentre è in attesa; in realtà è meno efficiente che semplicemente chiamare una versione sincrona della funzione di stampa. In secondo luogo, poiché il thread chiamante è un thread della GUI, l'interfaccia utente risulta ‘congelata’. Un thread sospeso, ovviamente, non è in grado di elaborare l'input dell'utente, aggiornare elementi grafici o fare qualsiasi altra cosa. Per queste ragioni, non si dovrebbe mai usare il metodo Wait su un thread della GUI.
Fortunatamente, .NET introduce le funzionalità al linguaggio async e await. Il modificatore async segna il metodo in modo che il compilatore sappia che il metodo è asincrono e usi l'operatore await. L'operatore await è molto utile in quanto è utilizzato per chiamare i metodi in modo asincrono e dopo per forzare il thread chiamante a tornare automaticamente alla riga successiva e continuare l'esecuzione una volta che l'operazione asincrona è stata completata. Il thread chiamante - normalmente il thread della GUI - non è bloccato ed è libero di adottare altre azioni mentre il task sul thread di lavoro è ancora in esecuzione.
Si noti che possiamo modificare ora l'obiettivo originale con poche variazioni, ma in questo caso l'interfaccia utente non si blocca.

    private async void Button_Click(object sender, RoutedEventArgs e)
    {
      Task t = PrintReportAsync ("HP1");
      // Attendere (senza blocco) fino a quando il task è completato.
      await t;
      // riparte da qui quando il task è completato.
      MessageBox.Show ("Il report è pronto!");
    }

L’utilizzo di Run

Quando una funzione asincrona non è disponibile, è possibile scrivere facilmente le proprie funzioni wrapper che eseguono internamente uno o più metodi sincroni. L'esempio seguente utilizza il metodo statico Run per accodare l'esecuzione della funzione WorkFunc ad una thread casuale nel pool di thread dei Task. Si noti che il metodo click restituisce immediatamente il controllo al chiamante, mentre il WorkFunc continua a eseguire sul thread di lavoro.

    private void Button_Click(object sender, RoutedEventArgs e)
    {
      Task t = Task.Run ((Azione) WorkFunc);
    }
    private void WorkFunc ()
    {
      // my work
    }

Invece di utilizzare una funzione separata, può essere impiegata una funzione anonima chiamata anche lambda. Utilizzando le lambda, mantieniamo il codice della funzione di lavoro nella funzione stessa consentendoci di utilizzare gli argomenti e le variabili locali all'interno della lambda come se fossero parte della funzione stessa.

    private void Button_Click(object sender, RoutedEventArgs e)
    {
      int steps = GetSteps();
      Task t = Task.Run (() =>
      {
        // Posso utilizzare la variabile steps qui anche se mi trovo in una
        // diversa funzione in esecuzione su un diverso thread!
        // my work
      });
    }

I Task possono anche essere parametrizzati per restituire un tipo particolare, come risultato di un calcolo della lambda.

      Task<double> t = Task.Run<double>(() =>
      {
        double risultato;
        // Calcolo della variabile risultato …
        return risultato;
      });

L'operatore await può essere utilizzato anche in linea per ottenere il risultato della funzione asincrona e senza doverlo estrarre dal Task restituito.

    private async void Button_Click(object sender, RoutedEventArgs e)
    {
      double computedValue = await Task.Run <double>(() =>
      {
        double risultato;
        // Calcolo della variabile risultato ...
        return risultato;
      });
      // L’esecuzione riprende automaticamente qui quando il task sopra ha  completato!
      MessageBox.Show (String.Format ("Il risultato era {0}", computedValue.ToString ()));
    }

C'è un piccolo ‘carico’ associato ad await, quindi è sempre più efficiente chiamare più metodi sincroni all'interno della propria lambda che chiamare molte funzioni asincrone utilizzando await. Questo è particolarmente vero quando si scrive codice in un loop, dove il costo di utilizzo await attraverso centinaia o migliaia di iterazioni diventa sostanziale.


L’utilizzo di QueuedTask

Mentre i Task sono un appuntamento fisso all'interno di qualsiasi codice dell’add-in, i task devono essere forniti in ArcGIS Pro in modo diverso dal tradizionale TAP. Il framework fornisce una schedulazione personalizzata del Task che dovrebbe essere utilizzato quando fornisci Task che effettuano chiamate ai metodi sincroni di ArcGIS Pro SDK. Invece di chiamare Task.Run, gli sviluppatori di add-in devono chiamare QueuedTask.Run.

      Task t = QueuedTask.Run (() =>
      {
        // Chiama metodi SDK sincroni
      });

La classe QueuedTask viene utilizzata al posto della classe Task per i seguenti motivi:

Controllo della concorrenza e delle code

Quando i Task vengono inviati utilizzando Task.Run, il Task associato sarà in esecuzione su un thread a caso nel pool di thread gestito ogni volta che viene chiamato. Se una chiamata successiva a Task.Run viene fatta da qualsiasi altra parte dell'applicazione, il nuovo Task inizierà a funzionare immediatamente su un altro thread mentre il primo Task è ancora in esecuzione sul primo thread. Tornando alla lista delle sfide da affrontare nel codice multithread, dovrebbe essere ovvio che l'esecuzione delle operazioni non organizzate e concorrenti rischia di far andare in crash l’applicazione o di corrompere lo stato dell'applicazione. Il comportamento di accodamento QueuedTask.Run garantisce il corretto ordine delle chiamate e riduce il rischio di conflitti. Ricordate che il parallelismo all'interno di ArcGIS Pro si realizza internamente; questo semplifica il modello di programmazione pubblica e riduce notevolmente la probabilità di conflitti.

Affinità e stato

Per motivi di prestazioni, ArcGIS Pro mantiene un notevole stato su specifici thread e, in molti casi, utilizza oggetti che hanno affinità di thread. Affinità di thread significa che un oggetto è legato ad un particolare thread e non deve interagire con qualsiasi thread ma con il thread col quale ha affinità. Vincoli di affinità sono comuni in sistemi operativi e componenti, tra cui connessioni a database, windows, controlli, code di input, timer, WPF Bitmap e server COM. In WPF, per esempio, chiamate a metodi su un qualsiasi oggetto derivato dalla classe WPF DependencyObject si tradurrà in un'eccezione se la chiamata viene effettuata da un thread da dove l'oggetto non è stato creato.
Thread nel pool di thread gestiti sono anche incompatibili con la maggior parte dei componenti COM, per cui non si dovrebbe tentare di utilizzare Task.Run con codice che potrebbe eseguire direttamente o indirettamente, componenti COM.

Integrazione dell’applicazione

Quando i Task vengono eseguiti utilizzando QueuedTask.Run, essi vengono integrati automaticamente con varie funzionalità all'interno dell'applicazione come segue:
  • L’estensione della Progress/Cancellation del framework, in cui la progress include la finestra di dialogo con lo stato di avanzamento programmabile, viene visualizzata e nascosta automaticamente e lo stato cancellazione è correttamente comunicato tra le parti interessate dell’applicazione.
  • Lo stato occupato del sistema dell’applicazione dove gli elementi dell'interfaccia utente come pulsanti e strumenti sono attivati e disattivati automaticamente quando i Task sono in esecuzione. Esecuzione dei task possono essere coordinati anche con fasi critiche quali la creazione di viste e chiusura dell'applicazione.
  • La coda dei Task è scritta nelle strutture di diagnostica del framework, quando abilitate. Questo consente agli sviluppatori di monitorare la sequenza di esecuzione dei task, i task in esecuzione e la durata dell'esecuzione. Questo tipo di informazione è preziosa per il debugging e l'analisi delle prestazioni.

Casi in cui è accettabile l'utilizzo di Task.Run

Ci sono casi in cui l'uso di Task.Run è accettabile, ad esempio quando si eseguono operazioni in background indipendenti costituite interamente da codice in modo gestito a condizione che i particolari componenti gestiti in uso non abbiano affinità di thread. Lo sviluppatore si assume la piena responsabilità della gestione della cancellazione, della visualizzazione dello stato di avanzamento, dell'abilitazione/disabilitazione dell'interfaccia utente in modo appropriato e del coordinamento delle operazioni e della gestione della logica dei conflitti.

Progress e cancellation

I metodi asincroni possono talvolta accettare un argomento Progressor, un oggetto che viene utilizzato dal chiamante per configurare le impostazioni della finestra di dialogo per lo stato di avanzamento e annullamento e di coordinare la comunicazione tra il chiamante e il chiamato. I metodi asincroni che non sono annullabili prendono una classe Progressor, mentre i metodi annullabili prendono una classe CancelableProgressor.
Gli oggetti Progressor seguono il modello stabilito dal CancelationToken di Microsoft e non possono essere creati direttamente; invece, lo sviluppatore deve creare un ProgressorSource o CancelableProgressorSource.
Gli oggetti "source" consentono di configurare come il progressor gestirà il progress senza esporre queste impostazioni al codice esterno, che potrebbero accedere al Progressor. L'oggetto ProgressorSource espone i seguenti costruttori:

  public ProgressorSource (Action<Progressor> callback)
  public ProgressorSource (ProgressDialog progDlg)
  public ProgressorSource (string message, bool delayedShow = false)

Il primo override prende un delegato che sarà chiamato, a intervalli regolari, mentre il Task è in esecuzione. Questa opzione è appropriata quando si desidera fornire un feedback specializzato durante l'esecuzione del Task.
Il secondo override prende una finestra dialogo di avanzamento, oggetto costruito separatamente. Se non è già visualizzato, il progressor mostrerà automaticamente questa finestra di dialogo di avanzamento quando il Task inizia l’esecuzione e si nasconderà automaticamente al completamento del Task. Se la finestra di dialogo è già visibile, il progressor aggiornerà il contenuto della finestra di dialogo durante l'esecuzione e sarà compito dello sviluppatore nascondere la finestra di dialogo di avanzamento al momento opportuno. Questa opzione è appropriata quando si desidera controllare manualmente la visibilità della finestra dello stato di avanzamento, ad esempio quando è necessario mantenere la finestra dello stato di avanzamento tra diverse attività separate.
Il terzo override creerà automaticamente e mostrerà una finestra di dialogo di avanzamento quando il Task inizia l'esecuzione e lo nasconde al termine del completamento del Task. Il parametro delayedShow controlla se la finestra di dialogo di avanzamento deve essere mostrata immediatamente o ritardare la sua apparizione per consentire operazioni rapide da completare ed evitare di apparire, se non necessario. Se si prevede che il Task sia rapido nel completamento dell’esecuzione, impostare questo parametro a true. Se si prevede che il Task abbia bisogno di più di uno o due secondi per completare l’operazione, impostare delayedShow a false in modo che la finestra di dialogo di avanzamento appaia immediatamente per trasmettere responsività.
CancelableProgressors richiedono un ulteriore argomento che specifica cosa dovrebbe visualizzare il messaggio di annullamento. Verrà visualizzato il messaggio di annullamento, non appena l'utente fa clic sul pulsante Annulla nella finestra di dialogo.

  public CancelableProgressorSource(Action <CancelableProgressor> callback)
  public CancelableProgressorSource(ProgressDialog progDlg)
  public CancelableProgressorSource(string message, string cancelMessage, bool delayedShow = false)

Esempio di implementazione del metodo utilizzando cancellation

Lo specializzato CancelableProgressor espone una proprietà CancellationToken che può essere utilizzata per comunicare l’annullamento. All'interno dell'implementazione del metodo, il codice in esecuzione nel loop dovrebbe controllare la proprietà IsCancellationRequested e uscire dal metodo gettando l’eccezione OperationCanceledException (che riconosce la richiesta di cancellazione) come illustrato di seguito:

public Task<long> CalcFactorialAsync(int x, CancelableProgressor progressor)
{
  return QueuedTask.Run<long>(() =>
  {
    long result = 1;
    for (int i = 1; i < x; ++i)
    {
      if (progressor.CancellationToken.IsCancellationRequested)
        throw new OperationCanceledException();
      result *= i;
    }
    return result;
  });
}

Utilizzare con i metodi asincroni la finestra di dialogo integrata

Se il Progressor è stato configurato per mostrare lo stato di avanzamento, il Task in esecuzione può aggiornare le informazioni visualizzate nella finestra di avanzamento utilizzando il progressor (entrambe Progressor e CancelableProgressor supportano finestre di dialogo di avanzamento):

public Task<long> CalcFactorialAsync(int x, Progressor progressor)
{
  return QueuedTask.Run<long>(() =>
  {
    long result = 1;
    for (int i = 1; i < x; ++i)
    {
      progressor.Message = string.Format("Working on step:{0}", i);
      result *= i;
    }
    return result;
  }, progressor);
}

Complicazioni comuni

Ipotesi di stato costante

Si consideri il seguente esempio. Questa chiamata viene richiamata dal thread della GUI e l'intento è quello di eliminare lo specifico layer dalla mappa della vista attiva.

private Task DeleteSelectedLayerAsync(Layer layer)
{
  return QueuedTask.Run(() =>
  {
      MapView.Active.Map.RemoveLayer(layer);
  });
}

Anche se semplice in apparenza, questa funzione a volte può causare un'eccezione quando in uso all'interno dell'applicazione. L'errore qui è stato quello di pensare che lo stato del sistema rimanga statico attraverso i thread. Ci possono essere precedentemente delle operazioni in coda in esecuzione e queste devono essere completate prima che un'altra operazione possa iniziare l'esecuzione. Durante questo tempo, lo stato dell’applicazione può cambiare a causa dell’interazione con l'utente o il risultato di una operazioni ancora in esecuzione. In questo caso, l’oggetto attivo potrebbe diventare una tabella prima che la lambda effettivamente inizi l’esecuzione, nel qual caso la mappa sarà nulla determinando un'eccezione. L'approccio sicuro è quello di evitare "concatenamenti" di chiamate su variabili membro o variabili passate tra thread; utilizzare variabili locali come istantanee dello stato dell'applicazione quando il metodo è stato chiamato, dal momento che non cambieranno al loro interno.

private Task DeleteSelectedLayerAsync (Layer layer)
{
  // Prendere una "istantanea" della mappa sulla vista attiva.
  Map m = MapView.Active.Map;
  return QueuedTask.Run (() =>
  {
      m.RemoveLayer(layer);
  });
}

In un ambiente multithreading si dovrebbe gestire il codice allestendo una strategia difensiva. Si consideri un Task che modifica come un particolare layer debba essere simboleggiato. Se tale Task finisce in una coda dietro un altro Task che rimuove questo stesso layer dalla mappa, la seconda operazione è logicamente invalidata dalla prima. Per gestire questo caso correttamente, il secondo Task dovrebbe essere gestito per visualizzare un avviso o annullare l'operazione in modalità silente quando viene a sapere che il layer è stato eliminato.

Thread safe con associazione dati  in WPF

Per impostazione predefinita, i dati di collection associati a WPF devono essere modificati sul thread dove è stata creata l’associazione al controllo WPF. Questa limitazione diventa un problema quando si desidera riempire la collection da un thread di lavoro per produrre una buona esperienza per l’utente. Ad esempio, un elenco dei risultati di ricerca che viene riempito gradualmente man mano che vengono trovate corrispondenze, senza costringere l'utente ad attendere fino a quando l'intera ricerca è completata.
Per ovviare a questa limitazione, WPF fornisce una classe statica BindingOperations che permette di stabilire un'associazione tra un lock e una collection (ad esempio, ObservableCollection<T>). Tale associazione consente a collection associate di essere aggiornate dai thread esterni al thread principale della GUI, in modo coordinato senza generare la consueta eccezione.
BindingOperations.EnableCollectionSynchronization(Items, _lockObj);
Nell'esempio sopra, la variabile _lockObj - di tipo oggetto - è in genere istanziata quando viene creata la classe contenitore e servirà come lock per il coordinamento. Una volta che si chiama EnableCollectionSynchronization, WPF entrerà nello specificato lock quando c’è lettura o scrittura alla collection associata. Come proprietari della collection, si è obbligati a inserire il lock durante la lettura alla collection o scrittura della collection.
I wrapper ReadOnlyObservableCollection sono comunemente usati per forzare le sole letture semantiche sulle proprietà della collection osservabile. Per impostare correttamente la sincronizzazione con multithreading, è necessario chiamare EnableCollectionSynchronization sul wrapper, invece che sulla collection stessa, dal momento che è il wrapper al quale il WPF sarà associato effettivamente.

internal class HelloWorld
{
  private ObservableCollection<string> _items = new ObservableCollection<string>();
  private ReadOnlyObservableCollection<string> _itemsRO;
  private Object _lockObj = new Object();
  internal HelloWorld()
  {
    _itemsRO = new ReadOnlyObservableCollection<string>(_items);
    BindingOperations.EnableCollectionSynchronization(_itemsRO, _lockObj);
  }
//la proprietà pubblica utilizzata per il binding
public ReadOnlyObservableCollection<string> Items { get { return _itemsRO; } }
//all’interno della funzione di lavoro, il lock è inserito prima di modificare //la collection:
public void FillCollectionAsync()
{
  QueuedTask.Run(() =>
  {
    // letture e scritture dovrebbe essere fatte all’interno del lock
    lock (_lockObj)
    {
       _items.Add( GetData() );
    }
  });
}

Oggetti "Live" come proprietà

Si deve prestare attenzione quando si espongono oggetti – soprattutto collection - come proprietà pubbliche se la collezione è destinata a cambiare in un thread separato. Se qualcuno ottiene e trattiene tale proprietà e poi inizia l'enumerazione attraverso un thread A, un'eccezione può essere generata se il proprio codice modifica la collection sul thread B in quanto non vi è alcun lock di collaborazione tra essi. Distribuire in sola lettura istantanee della collection è più sicuro.

Eseguire codice sul thread della GUI

Ci sono casi in cui di tanto in tanto, mentre il codice viene eseguito su un thread di lavoro, si incontrano situazioni in cui è necessario chiedere input da parte dell'utente prima di procedere. Non si dovrebbe cercare di presentare una finestra direttamente dal thread di lavoro poiché le finestre hanno affinità di thread. Una finestra di dialogo creata sul thread di lavoro non si collega alla coda di input del thread della GUI e non rispetta lo z-order e la politica di focus stabilita dal thread GUI. In generale, è possibile eseguire codice sul thread della GUI da un thread di lavoro usando l'oggetto dispatcher dell'applicazione.
Questo può essere fatto in modo sincrono.

      FrameworkApplication.Current.Dispatcher.Invoke (() =>
      {
        // Fare qualcosa sul thread  della GUI
        System.Windows.MessageBox.Show ("Pronto!");
      });

O in modo asincrono:

      FrameworkApplication.Current.Dispatcher.BeginInvoke (() =>
      {
        // Fare qualcosa sul thread della GUI
        System.Windows.MessageBox.Show ("Pronto!");
      });

Si dovrebbe cercare di raccogliere le informazioni necessarie da parte dell'utente sul thread della GUI prima di eseguire il lavoro in modo da non dover utilizzare questa scorciatoia. Bloccando chiamate tra thread c’è rischio di deadlock e di trattenere operazioni in esecuzione sul thread di lavoro.

La gestione delle eccezioni in ambiente asincrono

Come le funzioni sincrone, le funzioni asincrone possono gettare eccezioni. Questo introduce un problema interessante da quando il chiamante fornisce il try/catch in un thread, e l'eccezione viene generata su un altro. Inoltre, il frame del chiamante non è solitamente ancora nello stack quando viene generata l'eccezione.
Tuttavia, .NET permette di utilizzare async/await con try/catch in modo che, se viene generata un'eccezione dal codice in esecuzione all'interno del Task, sarete in grado di catturare di nuovo da dove la funzione asincrona è stata chiamata. Si noti che la funzione asincrona deve restituire Task o Task<T> per eccezioni asincrone per essere adeguatamente rediretta (non void).

      try
      {
        var result = await PrintMapAsync ();
      }
      catch (Exception e)
      {
        // gestione eccezione.
      }

Se viene generata un'eccezione da parte del worker e non è stato fornito un try/catch intorno a dove si attende il worker, il runtime .NET si collegherà all'eccezione come eccezione inner, UnobservedException.
Eccezioni Unobserved solitamente compaiono solo quando l'oggetto eccezione viene raccolto dalla garbage collection di .NET, in nessun posto vicino a dove in realtà si è verificata l'eccezione. Se si ottiene uno di questi, esaminare l'eccezione interna per ottenere lo stack delle chiamate fallite. Nella finestra di controllo di VisualStudio, è possibile utilizzare la pseudo variabile $exception per esaminare il corrente oggetto eccezione.

Oggetti Freezable

WPF definisce un modello in cui determinati tipi di oggetti possono essere "congelati". Una volta che l'oggetto è congelato, modifiche non possono essere apportate all'oggetto senza che si generi un'eccezione. Oggetti freezable possono migliorare le prestazioni in alcune situazioni, e permettono anche di condividere l'oggetto tra i thread (vedi affinità di thread). Ad esempio, se un BitmapImage viene creato su un thread di lavoro, non è possibile utilizzarlo in seguito sul thread della GUI a meno di congelarlo prima.
Si consideri un caso comune in cui l'associazione venga utilizzata in combinazione con le immagini che sono state generate su un thread di lavoro. La classe di esempio VM qui sotto espone una proprietà chiamata Img:

public class VM: INotifyPropertyChanged
  {
    public BitmapImage Img {get {return _image; }}
    ...
  }

Questa proprietà restituisce un'istanza di BitmapImage (un oggetto Freezable), che è poi associata ad un pulsante in XAML:

<Button>
<Image Source = "{Binding Img}"> </ Immagine>
</ Button>

La bitmap sottostante viene periodicamente aggiornata sul thread di lavoro come segue; notare che verrà creata la bitmap sul thread di lavoro:

public Task Refresh ()
    {
      return QueuedTask.Run (() =>
      {
        var uri = GenerateThumbnail();
        Img = new BitmapImage (uri);
      });
    }

Nel processo di rendering dell'interfaccia utente, WPF tenterà di accedere alla proprietà bitmap dal thread della GUI ... ma questo si tradurrà in un'eccezione perché la bitmap è ancora "scongelata" e quindi ancorata al suo thread di lavoro principale. Questo problema può essere risolto semplicemente "congelando" la Bitmap dopo il suo l'aggiornamento.

var uri = GenerateThumbnail ();
Img = new BitmapImage (uri);
Img.Freeze ();

Nota: non tutte le classi che ereditano da System.Windows.Freezable possono essere congelate. Utilizzare la proprietà CanFreeze per verificare se l’oggetto può essere congelato.

lunedì 11 maggio 2015

Tutti pazzi per il SOI

Una delle novità più interessanti introdotte in ArcGIS Server 10.3.1 è il SOI ovvero Server Object Interceptor. Come si può un po' dedurre dal nome, un SOI intercetta le chiamate REST, SOAP e di servizi OGC su servizi di tipo Map o Image prima e dopo l'esecuzione di una operazione su una SOE (Server Object Extension) o sul SO (Server Object). In pratica i SOI estendono le capability dei servizi GIS (map e image) esistenti, modificando, bloccando o inoltrando le richieste REST/SOAP e OGC ma, a differenza di una SOE, le API rimangono inalterate mentre il comportamento può cambiare: non verranno quindi creati nuovi web service end point mentre, come avviene per le SOE, possono essere sviluppate in NET o Java e distribuite dagli administrator e publisher dal manager.
Giusto per fare un paragone, i SOI possiamo pensarli come le server filter.

Ma come mai sono stati introdotti i SOI? Precedentemente non era possibile modificare il comportamento delle chiamate standard dei servizi GIS e alcune funzionalità personalizzate erano piuttosto complicate e richiedevano l'utilizzo di proxy - comunque non erano soluzioni perfettamente integrate nei servizi GIS.
I potenziali use case per lo sviluppo di SOI sono:

Sicurezza
  • controllo di accesso a livello di layer  e di operation
  • extent/AOI basato su masking
  • risposte di query modificate/perfezionate sia negli attributi che nella geometria
  • watermark, classificazioni e personalizzazioni
  • auditing e logging
Integrazione sistemi e dati
  • integrazione con dati esterni e propri sistemi
  • validazione degli input e output (post processing)
  • inserimenti avanzati (in input e output), funzionalità personalizzate (reindirizzare specifiche operazioni ad entità esterne)

I servizi di tipo Map e Image, con incluse le capability (ad esempio i feature servizi) supportano tre diversi tipi di richieste:
  • richieste REST API
  • richieste SOAP API
  • richieste OGC

Come possiamo vedere dalle immagini il SOI si frappone tra i client e SO/SOE
I client fanno delle richieste che possono essere gestite o meno sia prima di inviarle all'handler responsabile che dopo l'elaborazione (ma prima di inviarle al client).

Qui la risposta post process può anche essere reindirizzata altrove oltre che al client

 

Qui la richiesta può essere sempre inviata altrove oltre che agli oggetti sottostanti al servizio



Affinché un SOI possa intercettare queste richieste, è necessario implementare le seguenti interfacce:
- IRESTRequestHandler - per la gestione delle richieste API REST
- IRequestHandler2 - per la gestione delle richieste SOAP API, tra le quali anche le richieste dai clienti desktop quali ArcMap
- IWebRequestHandler - per la gestione delle richieste OGC

Tutte queste interfacce si trovano nello spazio dei nomi Esri.ArcGIS.esriSystem.
 
Anche se una configurazione del servizio non supporta le richieste OGC (ad esempio WMS, WFS ecc.), è necessario gestire tutte le interfacce di cui sopra.
A seconda della logica di business che si desidera di implementare, si può scegliere tra due approcci generali. Se si implementa un SOI che gestirà funzioni di sicurezza, allora è consigliabile iniziare ad implementare tutte le interfacce e bloccare tutte le richieste. Una volta implementato il codice personalizzato, è possibile consentire l'accesso tramite le interfacce. Se non adottiamo questo metodo non bloccando tutte le richieste in arrivo e consentendo l'accesso successivamente, c'è maggiore rischio ad esporsi a 'buchi' nella sicurezza. Mentre, se non si sta sviluppando funzionalità di sicurezza, è possibile implementare le tre interfacce facendo passare tutte le richieste con l'implementazione standard di base, al fine di consentire le funzionalità già presenti e successivamente aggiungere la logica di business per una o più operazioni che si desidera modificare.
Quando un servizio GIS è stato configurato con un SOI, il server inoltra tutte le richieste del servizio al SOI. Ovviamente è responsabilità del SOI elaborare le richieste, delegare la richiesta ai correnti oggetti del servizio Map o Image, se applicabile, e poi eventuali ulteriori elaborazioni sulla risposta prima di restituirla al client.
 
Implementare l'interfaccia IRESTRequestHandler
La gestione delle API REST è il modo più semplice per iniziare con lo sviluppo di un SOI, dal momento che tutti i parametri e le risposte dell'operazione sono text /JSON e possono essere facilmente gestiti.
 
La principale funzione di questa interfaccia è:
 
public byte [] HandleRESTRequest (string capabilities, string resourceName, string
    operationName, string operationInput, string outputFormat, string
    requestProperties,  ref string responseProperties);
 
La funzione modella le REST API in termini di risorse e di operazioni e le sue corrispondenti proprietà della richiesta.
Il parametro operationInput contiene tipicamente un oggetto JSON che rappresenta la richiesta. Si può facilmente gestire questo oggetto utilizzando la libreria JSON disponibile nella piattaforma di vostra scelta (NET o Java). Al fine di ottenere la configurazione di default per il servizio, il REST Services Directory invoca questa operazione passando oggetti JSON vuoti per tutti gli argomenti. Delegando questa chiamata al servizio sottostante e poi manipolando l'output nel SOI, si può vedere quali risorse (layers) e operazioni (export, find, identity, ecc) vengono presentati al Services Directory e ai client.
Un'altra funzione che deve essere gestita è:
public string getSchema ()
Questa funzione informa il REST Services Directory su come rappresentare la configurazione del servizio. In genere, l'implementazione del SOI dovrebbe delegare questa chiamata all'oggetto sottostante del servizio di Map o Image e poi eventualmente, se lo desideri, manipolare l'output.

Implementare l'interfaccia IRequestHandler2
L'implementazione di questa interfaccia è più complessa in quanto gestisce le richieste binarie (da ArcGIS for Desktop ed altre applicazioni basate su ArcObjects) e le richieste SOAP (da client SOAP personalizzati). In entrambi i casi, è necessario utilizzare le API ArcObjects per decodificare la richiesta in entrata, eventualmente modificare i parametri della richiesta, e quindi ricodificare la richiesta da inviare agli oggetti sottostanti dei servizi Map o Image. Anche per il post-process delle risposte, è sempre necessario utilizzare le API ArcObjects per gestire le risposte prima di inviarle al client. L'SDK contiene le classi di helper per gestire le richieste/risposte.
Questa interfaccia contiene due metodi che occorre implementare:
 
public byte [] HandleBinaryRequest2 (string capabilities, byte [] request)
 
La funzione di cui sopra viene richiamata quando il server riceve richieste binarie (da ArcGIS for Desktop) per un servizio. Il parametro di richiesta contiene la richiesta binaria che deve essere gestita utilizzando le API ArcObjects.

public string HandleStringRequest (string capabilities, string request)
 
Questa funzione viene richiamata quando il server riceve una richiesta SOAP (XML). I client SOAP in genere fanno queste richieste. Il parametro di richiesta contiene la richiesta XML che deve essere gestita utilizzando le API ArcObjects.
 
Implementare l'interfaccia IWebRequestHandler
Richieste KML e richieste di servizi OGC (WMS e WFS) possono essere intercettate implementando l'interfaccia IWebRequestHandler nel SOI. Le funzioni in questa interfaccia rappresentano una tipica richiesta OGC che è una richiesta HTTP con query string di parametri. È necessario implementare la seguente funzione:
 
public byte [] HandleStringWebRequest (esriHttpMethod httpMethod, string requestURL,
    stringa queryString, string capabilities, string requestData, ref string responseContentType, ref int respDataType)
 
Il parametro requestData è generalmente un text/XML e contiene l'input per la richiesta. Tuttavia, i parametri vengono spesso inviati tramite il parametro queryString. Il parametro respDataType informa il tuo codice su come il valore byte[] restituito deve essere interpretato. Se il respDataType indica un file, il tuo codice deve inviare i contenuti del file al percorso restituito da byte []. Il responseContentType informa l'handler web il tipo di contenuto da impostare mentre risponde alla richiesta HTTP.
 
Una classe di helper chiamata SOISupport è parte dell' esempio 'Layer Access' presente nell'SDK ArcObjects . Questa classe di helper contiene metodi per interagire con il servizio per delegare le richieste in arrivo, metodi per la conversione tra oggetti ArcObjects di risposta e molto altro. Inoltre nell'esempio 'Layer Access' è possibile vedere come utilizzare questi metodi in particolare quando si tratta di richieste SOAP e binarie. Altre classi nell'assembly ESRI.ArcGIS.SOESupport forniscono funzionalità per manipolare i dati e gli oggetti JSON.

Ed ora passiamo alla pratica vedendo un po' di esempi.
Come abbiamo accennato precedentemente lo sviluppo di un SOI è analogo a quello delle SOE.
Una volta installato l'SDK ArcObjects è possibile utilizzare il template disponibile nel proprio ambiente di sviluppo.



Il template genererà il seguente codice:

namespace MyFirstSOI
{
    [ComVisible(true)]
    [Guid("29a910dc-1cb6-48f5-a8a5-7f723c63e553")]
    [ClassInterface(ClassInterfaceType.None)]
    [ServerObjectInterceptor("",//use "MapServer" if SOI extends a Map service and "ImageServer" if it extends an Image service.
        Description = "",
        DisplayName = "SOI1",
        Properties = "")]
    public class MyFirstSOI : IServerObjectExtensionIRESTRequestHandlerIWebRequestHandlerIRequestHandler2IRequestHandler
    {
        private string _soiName;
        private IServerObjectHelper _soHelper;
        private ServerLogger _serverLog;
        private Dictionary<StringIServerObjectExtension> _extensionCache = new Dictionary<StringIServerObjectExtension>();
        IServerEnvironment2 _serverEnvironment;
 
        public MyFirstSOI()
        {
            _soiName = this.GetType().Name;
        }
 
        public void Init(IServerObjectHelper pSOH)
        {
            _soHelper = pSOH;
            _serverLog = new ServerLogger();
 
            _serverLog.LogMessage(ServerLogger.msgType.infoStandard, _soiName + ".init()", 200, "Initialized " + _soiName + " SOI.");
        }
 
        public void Shutdown()
        {
            _serverLog.LogMessage(ServerLogger.msgType.infoStandard, _soiName + ".init()", 200, "Shutting down " + _soiName + " SOI.");
        }
 
        #region REST interceptors
 
        public string GetSchema()
        {
            IRESTRequestHandler restRequestHandler = FindRequestHandlerDelegate<IRESTRequestHandler>();
            if (restRequestHandler == null)
                return null;
 
            return restRequestHandler.GetSchema();
        }
 
        public byte[] HandleRESTRequest(string Capabilities, string resourceName, string operationName,
            string operationInput, string outputFormat, string requestProperties, out string responseProperties)
        {
            responseProperties = null;
            _serverLog.LogMessage(ServerLogger.msgType.infoStandard, _soiName + ".HandleRESTRequest()",
                200, "Request received in Sample Object Interceptor for handleRESTRequest");
 
            /*
            * Add code to manipulate REST requests here
            */
 
            // Find the correct delegate to forward the request too
            IRESTRequestHandler restRequestHandler = FindRequestHandlerDelegate<IRESTRequestHandler>();
            if (restRequestHandler == null)
                return null;
 
            return restRequestHandler.HandleRESTRequest(
                    Capabilities, resourceName, operationName, operationInput,
                    outputFormat, requestProperties, out responseProperties);
        }
 
        #endregion
 
        #region SOAP interceptors
 
        public byte[] HandleStringWebRequest(esriHttpMethod httpMethod, string requestURL,
            string queryString, string Capabilities, string requestData,
            out string responseContentType, out esriWebResponseDataType respDataType)
        {
            _serverLog.LogMessage(ServerLogger.msgType.infoStandard, _soiName + ".HandleStringWebRequest()",
                200, "Request received in Sample Object Interceptor for HandleStringWebRequest");
 
            /*
             * Add code to manipulate requests here
             */
 
            IWebRequestHandler webRequestHandler = FindRequestHandlerDelegate<IWebRequestHandler>();
            if (webRequestHandler != null)
            {
                return webRequestHandler.HandleStringWebRequest(
                        httpMethod, requestURL, queryString, Capabilities, requestData, out responseContentType, out respDataType);
            }
 
            responseContentType = null;
            respDataType = esriWebResponseDataType.esriWRDTPayload;
            //Insert error response here.
            return null;
        }
 
        public byte[] HandleBinaryRequest(ref byte[] request)
        {
            _serverLog.LogMessage(ServerLogger.msgType.infoStandard, _soiName + ".HandleBinaryRequest()",
                  200, "Request received in Sample Object Interceptor for HandleBinaryRequest");
 
            /*
             * Add code to manipulate requests here
             */
 
            IRequestHandler requestHandler = FindRequestHandlerDelegate<IRequestHandler>();
            if (requestHandler != null)
            {
                return requestHandler.HandleBinaryRequest(request);
            }
 
            //Insert error response here.
            return null;
        }
 
        public byte[] HandleBinaryRequest2(string Capabilities, ref byte[] request)
        {
            _serverLog.LogMessage(ServerLogger.msgType.infoStandard, _soiName + ".HandleBinaryRequest2()",
                  200, "Request received in Sample Object Interceptor for HandleBinaryRequest2");
 
            /*
             * Add code to manipulate requests here
             */
 
            IRequestHandler2 requestHandler = FindRequestHandlerDelegate<IRequestHandler2>();
            if (requestHandler != null)
            {
                return requestHandler.HandleBinaryRequest2(Capabilities, request);
            }
 
            //Insert error response here.
            return null;
        }
 
        public string HandleStringRequest(string Capabilities, string request)
        {
            _serverLog.LogMessage(ServerLogger.msgType.infoStandard, _soiName + ".HandleStringRequest()",
                   200, "Request received in Sample Object Interceptor for HandleStringRequest");
 
            /*
             * Add code to manipulate requests here
             */
 
            IRequestHandler requestHandler = FindRequestHandlerDelegate<IRequestHandler>();
            if (requestHandler != null)
            {
                return requestHandler.HandleStringRequest(Capabilities, request);
            }
 
            //Insert error response here.
            return null;
        }
 
        #endregion
 
        #region Utility code
 
        private IServerEnvironment2 ServerEnvironment
        {
            get
            {
                if (_serverEnvironment == null)
                {
                    UID uid = new UIDClass();
                    uid.Value = "{32D4C328-E473-4615-922C-63C108F55E60}";
 
                    // CoCreate an EnvironmentManager and retrieve the IServerEnvironment
                    IEnvironmentManager environmentManager = new EnvironmentManager() as IEnvironmentManager;
                    _serverEnvironment = environmentManager.GetEnvironment(uid) as IServerEnvironment2;
                }
 
                return _serverEnvironment;
            }
        }
 
        private THandlerInterface FindRequestHandlerDelegate<THandlerInterface>() where THandlerInterface : class
        {
            try
            {
                IPropertySet props = ServerEnvironment.Properties;
                String extensionName;
                try
                {
                    extensionName = (String)props.GetProperty("ExtensionName");
                }
                catch (Exception /*e*/)
                {
                    extensionName = null;
                }
 
                if (String.IsNullOrEmpty(extensionName))
                {
                    return (_soHelper.ServerObject as THandlerInterface);
                }
 
                // Get the extension reference from cache if available
                if (_extensionCache.ContainsKey(extensionName))
                {
                    return (_extensionCache[extensionName] as THandlerInterface);
                }
 
                // This request is to be made on a specific extension
                // so we find the extension from the extension manager
                IServerObjectExtensionManager extnMgr = _soHelper.ServerObject as IServerObjectExtensionManager;
                IServerObjectExtension soe = extnMgr.FindExtensionByTypeName(extensionName);
                return (soe as THandlerInterface);
            }
            catch (Exception e)
            {
                _serverLog.LogMessage(ServerLogger.msgType.error,
                                    _soiName + ".FindRequestHandlerDelegate()", 500, e.ToString());
                throw;
            }
        }
        #endregion
    }
}

Come possiamo subito notare la definizione della classe è simile a quella della SOE solo che implementerà anche le interfacce IWebRequestHandler, IRequestHandler2, IRequestHandler ed è decorata con l'attributo ServerObjectInterceptorAttribute. Init e Shutdown sono sempre presenti visto il SOI 'vive' con il servizio GIS nel quale è abilitato.
Nella region 'Utility code' sono già disponibili una funzione  FindRequestHandlerDelegate ed una proprietà ServerEnvironment . FindRequestHandlerDelegate determina il corretto delegato per reindirizzare la richiesta in arrivo mentre la proprietà ServerEnvironment è utilizzata dalla funzione stessa per verificare se la richiesta è indirizzata al SO o ad una specifica SOE e quindi di conseguenza determinare il delegato corrispondente.

Vediamo ora un esempio che modifica la richiesta dopo averla fatta elaborare all'oggetto sottostante al servizio. Watermark sulla mappa generata su una richiesta di Export


namespace Studioat.ArcGis.Soi.Rest
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.Drawing;
    using System.Runtime.InteropServices;
    using ESRI.ArcGIS.esriSystem;
    using ESRI.ArcGIS.Server;
    using ESRI.ArcGIS.SOESupport;
 
    [SuppressMessage("StyleCop.CSharp.NamingRules""SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "Reviewed.")]
    [SuppressMessage("StyleCop.CSharp.NamingRules""SA1306:FieldNamesMustBeginWithLowerCaseLetter", Justification = "Reviewed.")]
 
    [ComVisible(true)]
    [Guid("b7058a41-e897-4151-bced-82ab9a03f2f9")]
    [ClassInterface(ClassInterfaceType.None)]
    [ServerObjectInterceptor("MapServer", Description = "Watermark SOI", DisplayName = "Watermark SOI", Properties = "")]
    public class Watermark : IServerObjectExtensionIRESTRequestHandlerIWebRequestHandlerIRequestHandler2IRequestHandler
    {
        /// <summary>
        /// server object interceptor name
        /// </summary>
        private string soiName;
 
        /// <summary>
        /// server object Helper
        /// </summary>
        private IServerObjectHelper soHelper;
 
        /// <summary>
        /// server log
        /// </summary>
        private ServerLogger serverLog;
 
        /// <summary>
        /// extension cache of service
        /// </summary>
        private Dictionary<stringIServerObjectExtension> extensionCache = new Dictionary<stringIServerObjectExtension>();
 
        /// <summary>
        /// server Environment
        /// </summary>
        private IServerEnvironment2 serverEnvironment;
 
        /// <summary>
        /// the first output directory ArcGIS server
        /// </summary>
        private string outputDirectory = string.Empty;
 
        /// <summary>
        /// Initializes a new instance of the <see cref="Watermark"/> class
        /// </summary>
        public Watermark()
        {
            this.soiName = this.GetType().Name;
        }
 
        /// <summary>
        /// Gets Server Environment
        /// </summary>
        private IServerEnvironment2 ServerEnvironment
        {
            get
            {
                if (this.serverEnvironment == null)
                {
                    UID uid = new UIDClass();
                    uid.Value = "{32D4C328-E473-4615-922C-63C108F55E60}";
 
                    // CoCreate an EnvironmentManager and retrieve the IServerEnvironment
                    IEnvironmentManager environmentManager = new EnvironmentManager() as IEnvironmentManager;
                    this.serverEnvironment = environmentManager.GetEnvironment(uid) as IServerEnvironment2;
                }
 
                return this.serverEnvironment;
            }
        }
 
        /// <summary>
        /// initialize of server object interceptor
        /// </summary>
        /// <param name="pSOH">Server Object Helper</param>
        public void Init(IServerObjectHelper pSOH)
        {
            try
            {
                this.soHelper = pSOH;
                this.serverLog = new ServerLogger();
 
                try
                {
                    var se4 = this.ServerEnvironment as IServerEnvironmentEx;
                    var dirInfos = se4.GetServerDirectoryInfos();
                    dirInfos.Reset();
                    object dirInfo = dirInfos.Next();
                    while (dirInfo != null)
                    {
                        var dinfo2 = dirInfo as IServerDirectoryInfo2;
                        if (null != dinfo2 && dinfo2.Type == esriServerDirectoryType.esriSDTypeOutput)
                        {
                            this.outputDirectory = dinfo2.Path;
                            break;
                        }
 
                        dirInfo = dirInfos.Next();
                    }
                }
                catch
                {
                    this.outputDirectory = string.Empty;
                }
 
                this.outputDirectory = this.outputDirectory.Trim();
                if (string.IsNullOrEmpty(this.outputDirectory))
                {
                    this.serverLog.LogMessage(ServerLogger.msgType.error, this.soiName + ".init()", 500, "OutputDirectory is empty or missing. Reset to default.");
                    this.outputDirectory = "C:\\arcgisserver\\directories\\arcgisoutput";
                }
 
                this.serverLog.LogMessage(ServerLogger.msgType.infoDetailed, this.soiName + ".init()", 500, "OutputDirectory is " + this.outputDirectory);
                this.serverLog.LogMessage(ServerLogger.msgType.infoStandard, this.soiName + ".init()", 200, "Initialized " + this.soiName + " SOI.");
            }
            catch (Exception e)
            {
                this.serverLog.LogMessage(ServerLogger.msgType.error, this.soiName + ".HandleRESTRequest()", 500, "Exception " + e.GetType().Name + " " + e.Message + " " + e.StackTrace);
                throw;
            }
        }
 
        /// <summary>
        /// shutdown server object interceptor
        /// </summary>
        public void Shutdown()
        {
            this.serverLog.LogMessage(ServerLogger.msgType.infoStandard, this.soiName + ".init()", 200, "Shutting down " + this.soiName + " SOI.");
        }
 
        #region REST interceptors
 
        /// <summary>
        /// get schema
        /// </summary>
        /// <returns>return schema</returns>
        public string GetSchema()
        {
            IRESTRequestHandler restRequestHandler = FindRequestHandlerDelegate<IRESTRequestHandler>();
            if (restRequestHandler == null)
            {
                return null;
            }
 
            return restRequestHandler.GetSchema();
        }
 
        /// <summary>
        /// handling rest request
        /// </summary>
        /// <param name="Capabilities">list of capabilities</param>
        /// <param name="resourceName">name of resource</param>
        /// <param name="operationName">name of operation</param>
        /// <param name="operationInput">input operation</param>
        /// <param name="outputFormat">output format</param>
        /// <param name="requestProperties">request properties</param>
        /// <param name="responseProperties">response properties</param>
        /// <returns>response</returns>
        public byte[] HandleRESTRequest(string Capabilities, string resourceName, string operationName, string operationInput, string outputFormat, string requestProperties, out string responseProperties)
        {
            try
            {
                responseProperties = null;
                this.serverLog.LogMessage(ServerLogger.msgType.infoStandard, this.soiName + ".HandleRESTRequest()", 200, "Request received in Sample Object Interceptor for handleRESTRequest");
 
                // Add code to manipulate REST requests here
                // Find the correct delegate to forward the request too
                IRESTRequestHandler restRequestHandler = FindRequestHandlerDelegate<IRESTRequestHandler>();
                if (restRequestHandler == null)
                {
                    return null;
                }
 
                var response = restRequestHandler.HandleRESTRequest(Capabilities, resourceName, operationName, operationInput, outputFormat, requestProperties, out responseProperties);
 
                // Manipulate the response.
                // Add watermark
                if (operationName.Equals("export"StringComparison.CurrentCultureIgnoreCase))
                {
                    Image sourceImage = null;
                    if (outputFormat.Equals("image"StringComparison.CurrentCultureIgnoreCase))
                    {
                        sourceImage = Image.FromStream(new System.IO.MemoryStream(response));
 
                        var watermarker = new ApplyWatermark();
 
                        var watermarkedImage = watermarker.Mark(sourceImage, "(c) ESRI Inc.");
                        var newResponse = new System.IO.MemoryStream();
                        watermarkedImage.Save(newResponse, sourceImage.RawFormat);
 
                        return newResponse.GetBuffer();
                    }
                    else if (outputFormat.Equals("json"StringComparison.CurrentCultureIgnoreCase))
                    {
                        var responseString = System.Text.Encoding.UTF8.GetString(response);
                        var jo = new JsonObject(responseString);
                        string hrefString = null;
                        if (!jo.TryGetString("href"out hrefString))
                        {
                            throw new RESTErrorException("Export operation returned invalid response");
                        }
 
                        if (string.IsNullOrEmpty(hrefString))
                        {
                            throw new RESTErrorException("Export operation returned invalid response");
                        }
 
                        // Generate output file location
                        var outputImageFileLocation = this.GetOutputImageFileLocation(hrefString);
 
                        var watermarker = new ApplyWatermark();
                        Image watermarkedImage;
 
                        System.Drawing.Imaging.ImageFormat sourceImageFormat;
                        using (sourceImage = Image.FromFile(outputImageFileLocation))
                        {
                            sourceImageFormat = sourceImage.RawFormat;
                            watermarkedImage = watermarker.Mark(sourceImage, "(c) ESRI Inc.");
                        }
 
                        // make sure we dispose sourceImage handles before saving to it
                        watermarkedImage.Save(outputImageFileLocation, sourceImageFormat);
                        watermarkedImage.Dispose();
 
                        // return response as is because we have modified the file its pointing to
                        return response;
                    }
                    else if (outputFormat.Equals("kmz"StringComparison.CurrentCultureIgnoreCase))
                    {
                        // Note: Watermark can be added for the kmz format too. In this example we didn't
                        // implement it.
                        throw new RESTErrorException("Kmz format is not supported");
                    }
                    else
                    {
                        throw new RESTErrorException("Invalid operation parameters");
                    }
                }
 
                return response;
            }
            catch (RESTErrorException restException)
            {
                responseProperties = "{\"Content-Type\":\"text/plain;charset=utf-8\"}";
                return System.Text.Encoding.UTF8.GetBytes(restException.Message);
            }
            catch (Exception e)
            {
                this.serverLog.LogMessage(ServerLogger.msgType.error, this.soiName + ".HandleRESTRequest()", 500, "Exception " + e.GetType().Name + " " + e.Message + " " + e.StackTrace);
                throw;
            }
        }
 
        #endregion
 
        #region SOAP interceptors
 
        /// <summary>
        /// handling OGC request
        /// </summary>
        /// <param name="httpMethod">http Method</param>
        /// <param name="requestURL">request URL</param>
        /// <param name="queryString">query string</param>
        /// <param name="Capabilities">list of capabilities</param>
        /// <param name="requestData">request data</param>
        /// <param name="responseContentType">response content type</param>
        /// <param name="respDataType">response data type</param>
        /// <returns>response</returns>
        public byte[] HandleStringWebRequest(esriHttpMethod httpMethod, string requestURL, string queryString, string Capabilities, string requestData, out string responseContentType, out esriWebResponseDataType respDataType)
        {
            this.serverLog.LogMessage(ServerLogger.msgType.infoStandard, this.soiName + ".HandleStringWebRequest()", 200, "Request received in Sample Object Interceptor for HandleStringWebRequest");
 
            /*
             * Add code to manipulate requests here
             */
 
            IWebRequestHandler webRequestHandler = FindRequestHandlerDelegate<IWebRequestHandler>();
            if (webRequestHandler != null)
            {
                return webRequestHandler.HandleStringWebRequest(httpMethod, requestURL, queryString, Capabilities, requestData, out responseContentType, out respDataType);
            }
 
            responseContentType = null;
            respDataType = esriWebResponseDataType.esriWRDTPayload;
 
            // Insert error response here.
            return null;
        }
 
        /// <summary>
        /// handling soap binary 
        /// </summary>
        /// <param name="request">request</param>
        /// <returns>response</returns>
        public byte[] HandleBinaryRequest(ref byte[] request)
        {
            this.serverLog.LogMessage(ServerLogger.msgType.infoStandard, this.soiName + ".HandleBinaryRequest()", 200, "Request received in Sample Object Interceptor for HandleBinaryRequest");
 
            /*
             * Add code to manipulate requests here
             */
 
            IRequestHandler requestHandler = FindRequestHandlerDelegate<IRequestHandler>();
            if (requestHandler != null)
            {
                return requestHandler.HandleBinaryRequest(request);
            }
 
            // Insert error response here.
            return null;
        }
 
        /// <summary>
        /// handling soap binary 
        /// </summary>
        /// <param name="Capabilities">list of capabilities</param>
        /// <param name="request">request</param>
        /// <returns>response</returns>
        public byte[] HandleBinaryRequest2(string Capabilities, ref byte[] request)
        {
            this.serverLog.LogMessage(ServerLogger.msgType.infoStandard, this.soiName + ".HandleBinaryRequest2()", 200, "Request received in Sample Object Interceptor for HandleBinaryRequest2");
 
            /*
             * Add code to manipulate requests here
             */
 
            IRequestHandler2 requestHandler = FindRequestHandlerDelegate<IRequestHandler2>();
            if (requestHandler != null)
            {
                return requestHandler.HandleBinaryRequest2(Capabilities, request);
            }
 
            // Insert error response here.
            return null;
        }
 
        /// <summary>
        /// handler soap xml
        /// </summary>
        /// <param name="Capabilities">list of capabilities</param>
        /// <param name="request">request</param>
        /// <returns>response</returns>
        public string HandleStringRequest(string Capabilities, string request)
        {
            this.serverLog.LogMessage(ServerLogger.msgType.infoStandard, this.soiName + ".HandleStringRequest()", 200, "Request received in Sample Object Interceptor for HandleStringRequest");
 
            /*
             * Add code to manipulate requests here
             */
 
            IRequestHandler requestHandler = FindRequestHandlerDelegate<IRequestHandler>();
            if (requestHandler != null)
            {
                return requestHandler.HandleStringRequest(Capabilities, request);
            }
 
            // Insert error response here.
            return null;
        }
 
        #endregion
 
        #region Utility code
        /// <summary>
        /// find handler rest for server object or server object extension
        /// </summary>
        /// <typeparam name="THandlerInterface">interface that implements handler rest</typeparam>
        /// <returns>object that implements handler rest</returns>
        private THandlerInterface FindRequestHandlerDelegate<THandlerInterface>() where THandlerInterface : class
        {
            try
            {
                IPropertySet props = this.ServerEnvironment.Properties;
                string extensionName;
                try
                {
                    extensionName = (string)props.GetProperty("ExtensionName");
                }
                catch (Exception /*e*/)
                {
                    extensionName = null;
                }
 
                if (string.IsNullOrEmpty(extensionName))
                {
                    return this.soHelper.ServerObject as THandlerInterface;
                }
 
                // Get the extension reference from cache if available
                if (this.extensionCache.ContainsKey(extensionName))
                {
                    return this.extensionCache[extensionName] as THandlerInterface;
                }
 
                // This request is to be made on a specific extension
                // so we find the extension from the extension manager
                IServerObjectExtensionManager extnMgr = this.soHelper.ServerObject as IServerObjectExtensionManager;
                IServerObjectExtension soe = extnMgr.FindExtensionByTypeName(extensionName);
                return soe as THandlerInterface;
            }
            catch (Exception e)
            {
                this.serverLog.LogMessage(ServerLogger.msgType.error, this.soiName + ".FindRequestHandlerDelegate()", 500, e.ToString());
                throw;
            }
        }
 
        /// <summary>
        /// Generate physical file path from virtual path
        /// </summary>
        /// <param name="virtualPath">virtualPath Path returned by the MapServer SO</param>
        /// <returns>physical file path</returns>
        private string GetOutputImageFileLocation(string virtualPath)
        {
            /*
             * Sample output returned by MapServer SO
             * example : /rest/directories/arcgisoutput/SampleWorldCities_MapServer/
             * _ags_map26c62f8c2c0c4965b53e87e300e1912f.png
             */
            var virtualPathParts = virtualPath.Split('/');
            string imageFileLocation = this.outputDirectory;
 
            // build the physical path to the image file
            bool buildPath = false;
            foreach (string virtualPathPart in virtualPathParts)
            {
                if (buildPath)
                {
                    imageFileLocation += "\\" + virtualPathPart;
                }
 
                if (virtualPathPart.Equals("arcgisoutput"StringComparison.CurrentCultureIgnoreCase))
                {
                    buildPath = true;
                }
            }
 
            return imageFileLocation;
        }
        #endregion
    }
}

Nel codice possiamo notare come, una volta disponibile la risposta dal SO, aggiungiamo il watermark all'immagine prima di inviarla al client.

Una volta compilato lo installiamo in ArcGIS Server Manager come avviene per le SOE.


e successivamente abilitiamo il SOI nel servizio GIS.



Possiamo effettuare il test direttamente dall'export del Rest Services Directory



Link live demo

Mentre in questo esempio vediamo una PoC di come gestire un controllo a livello di layer basato su permessi impostati su file esterno.
Se si implementa il controllo a livello di layer attraverso un SOI, occorre anche disabilitare il caching di tutti i layer delle risorse inclusi nel servizio. Questo consente di intercettare le operazione e filtrare i layer che non sono consentiti. Nell'ArcGIS Server Administrator Directory occorre impostare la proprietà disableCaching a true del servizio .
I passi da seguire nel dettaglio sono:
- Aprire l'ArcGIS Server Administrator Directory (l'url è http://myserver:6080/arcgis/admin);
- cliccare su services e cliccare il nome del servizio interessato;
- cliccare edit;
- nella sezione properties del json del servizio, aggiungere la proprietà disableCaching ed impostare il valore a true
"properties": {
  ...
  "disableCaching": "true",
  ...
 },
- cliccare save Edits.

public class NetLayerAccessSOI : IServerObjectExtensionIRESTRequestHandlerIWebRequestHandlerIRequestHandler2IRequestHandler
    {
        private string _soiName;
        private IServerObjectHelper _soHelper;
        private ServerLogger _serverLog;
        private IServerObject _serverObject;
        RestServiceSOI _restServiceSOI;
        SoapServiceSOI _soapServiceSOI;
 
        private Dictionary<RESTHandlerOpCodeRestFilterOperation> _operationMap;
 
        /*
         * Map used to store permission information. Permission rules for each
         * service is read form the permisson.json file.
         */
        private Dictionary<StringString> _servicePermissionMap = null;
        /*
         * Permission are read from this external file. Advantage of an external file is that 
         * same SOI can be used for multiple services and permission for all of these services
         * is read from the permission.json file. 
         *  
         */
        private String _permissionFilePath = "C:\\arcgisserver\\permission.json"//default path
 
        private String _wsdlFilePath = "C:\\Program Files\\ArcGIS\\Server\\XmlSchema\\MapServer.wsdl"//default path
 
 
        public NetLayerAccessSOI ()
        {
            _soiName = this.GetType().Name;
        }
 
        private void InitFiltering ()
        {
            _operationMap = new Dictionary<RESTHandlerOpCodeRestFilterOperation>
            {
                { RESTHandlerOpCode.root, new RestFilterOperation
                                            { 
                                                PreFilter = null, 
                                                PostFilter = PostFilterRESTRoot
                                            } },
                { RESTHandlerOpCode.rootExport, new RestFilterOperation
                                            { 
                                                PreFilter = PreFilterExport, 
                                                PostFilter = null 
                                            } },
                { RESTHandlerOpCode.rootFind, new RestFilterOperation
                                            { 
                                                PreFilter = PreFilterFindAndKml, 
                                                PostFilter = null 
                                            } },
                { RESTHandlerOpCode.rootGenerateKml, new RestFilterOperation
                                            { 
                                                PreFilter = PreFilterFindAndKml, 
                                                PostFilter = null 
                                            } },
                { RESTHandlerOpCode.rootIdentify, new RestFilterOperation
                                            { 
                                                PreFilter = PreFilterIdentify, 
                                                PostFilter = null 
                                            } },
                { RESTHandlerOpCode.rootLayers, new RestFilterOperation 
                                            { 
                                                PreFilter = null, 
                                                PostFilter = PostFilterRootLayers
                                            } },
                { RESTHandlerOpCode.rootLegend, new RestFilterOperation
                                            { 
                                                PreFilter = null, 
                                                PostFilter = PostFilterRootLegend
                                            } },
                { RESTHandlerOpCode.layerGenerateRenderer, new RestFilterOperation 
                                            { 
                                                PreFilter = PreFilterLayerQuery, 
                                                PostFilter = null 
                                            } },
                { RESTHandlerOpCode.layerQuery, new RestFilterOperation
                                            { 
                                                PreFilter = PreFilterLayerQuery, 
                                                PostFilter = null 
                                            } },
                { RESTHandlerOpCode.layerQueryRelatedRecords, new RestFilterOperation 
                                            { 
                                                PreFilter = PreFilterLayerQuery, 
                                                PostFilter = null 
                                            } }
            };
        }
 
        public void Init ( IServerObjectHelper pSOH )
        {
            try
            {
                _soHelper = pSOH;
                _serverLog = new ServerLogger();
                _serverObject = pSOH.ServerObject;
 
                _restServiceSOI = new RestServiceSOI(_soHelper);
                _soapServiceSOI = new SoapServiceSOI(_soHelper, _wsdlFilePath);
 
                if (File.Exists(_permissionFilePath))
                {
                    //TODO REMOVE
                    _serverLog.LogMessage(ServerLogger.msgType.infoStandard, _soiName + ".init()", 200, "Reading permissions from " + _permissionFilePath);
 
                    _servicePermissionMap = ReadPermissionFile(_permissionFilePath);
 
                    //TODO REMOVE
                    _serverLog.LogMessage(ServerLogger.msgType.infoStandard, _soiName + ".init()", 200, "Total permission entries: " + _servicePermissionMap.Count());
                }
                else
                {
                    _serverLog.LogMessage(ServerLogger.msgType.error, _soiName + ".init()", 500, "Permission file does not exist at " + _permissionFilePath);
                }
 
                InitFiltering();
 
                _serverLog.LogMessage(ServerLogger.msgType.infoStandard, _soiName + ".init()", 200, "Initialized " + _soiName + " SOI.");
            }
            catch (Exception e)
            {
                _serverLog.LogMessage(ServerLogger.msgType.error, _soiName + ".init()", 500, "Exception: " + e.Message + " in " + e.StackTrace);
            }
        }
 
        public void Shutdown ()
        {
            _serverLog.LogMessage(ServerLogger.msgType.infoStandard, _soiName + ".init()", 200, "Shutting down " + _soiName + " SOE.");
        }
 
        #region REST interceptors
 
        public string GetSchema ()
        {
            try
            {
                IRESTRequestHandler restRequestHandler = _restServiceSOI.FindRequestHandlerDelegate<IRESTRequestHandler>();
                if (restRequestHandler == null)
                    return null;
 
                return restRequestHandler.GetSchema();
            }
            catch (Exception e)
            {
                _serverLog.LogMessage(ServerLogger.msgType.error, _soiName + ".GetSchema()", 500, "Exception: " + e.Message + " in " + e.StackTrace);
                return null;
            }
        }
 
        private byte[] FilterRESTRequest (
                                          RestFilterOperation restFilterOp,
                                          RESTRequestParameters restInput,
                                          HashSet<string> authorizedLayers,
                                          out string responseProperties )
        {
            try
            {
                responseProperties = null;
                IRESTRequestHandler restRequestHandler = _restServiceSOI.FindRequestHandlerDelegate<IRESTRequestHandler>();
                if (restRequestHandler == null)
                    throw new RESTErrorException("Service handler not found");
 
                if (null != restFilterOp && null != restFilterOp.PreFilter)
                    restInput = restFilterOp.PreFilter(restInput, authorizedLayers);
 
                byte[] response =
                    restRequestHandler.HandleRESTRequest(restInput.Capabilities, restInput.ResourceName, restInput.OperationName, restInput.OperationInput,
                        restInput.OutputFormat, restInput.RequestProperties, out responseProperties);
 
                if (null == restFilterOp || null == restFilterOp.PostFilter)
                    return response;
 
                String responseStr = System.Text.Encoding.UTF8.GetString(response);
 
                responseStr = restFilterOp.PostFilter(restInput, responseStr, authorizedLayers);
 
                // TODO: remove ------------------------------------
                _serverLog.LogMessage(ServerLogger.msgType.infoDetailed, "HandleRESTRequest.FilterRESTRequest", 0, "Filtered REST resource response :: " + responseStr);
                // TODO: remove ------------------------------------
                return System.Text.Encoding.UTF8.GetBytes(responseStr);
            }
            catch (RESTErrorException restException)
            {
                // pre- or post- filters can throw restException with the error JSON output in the Message property.
                // we catch them here and return JSON response.
                responseProperties = "{\"Content-Type\":\"text/plain;charset=utf-8\"}";
                //catch and return a JSON error from the pre- or postFilter.
                return System.Text.Encoding.UTF8.GetBytes(restException.Message);
            }
        }
 
 
 
        private static RESTHandlerOpCode GetHandlerOpCode ( RESTRequestParameters restInput )
        {
            var resName = restInput.ResourceName.TrimStart('/'); //remove leading '/' to prevent empty string at index 0
            var resourceTokens = (resName ?? "").ToLower().Split('/');
            string opName = (restInput.OperationName ?? "").ToLower();
 
            switch (resourceTokens[0])
            {
                case "":
                    switch (opName)
                    {
                        case "":
                            return RESTHandlerOpCode.root;
                        case "export":
                            return RESTHandlerOpCode.rootExport;
                        case "find":
                            return RESTHandlerOpCode.rootFind;
                        case "identify":
                            return RESTHandlerOpCode.rootIdentify;
                        case "generatekml":
                            return RESTHandlerOpCode.rootGenerateKml;
                        default:
                            return RESTHandlerOpCode.defaultNoOp;
                    }
                case "layers":
                    {
                        var tokenCount = resourceTokens.GetLength(0);
                        if (1 == tokenCount)
                            return RESTHandlerOpCode.rootLayers;
                        if (2 == tokenCount)
                            switch (opName)
                            {
                                case "":
                                    return RESTHandlerOpCode.layerRoot;
                                case "query":
                                    return RESTHandlerOpCode.layerQuery;
                                case "queryRelatedRecords":
                                    return RESTHandlerOpCode.layerQueryRelatedRecords;
                                default:
                                    return RESTHandlerOpCode.defaultNoOp;
                            }
                    }
                    break;
                case "legend":
                    return RESTHandlerOpCode.rootLegend;
                default:
                    return RESTHandlerOpCode.defaultNoOp;
            }
            return RESTHandlerOpCode.defaultNoOp;
        }
 
 
        public byte[] HandleRESTRequest ( string capabilities, string resourceName, string operationName,
            string operationInput, string outputFormat, string requestProperties, out string responseProperties )
        {
            try
            {
                responseProperties = null;
                _serverLog.LogMessage(ServerLogger.msgType.infoStandard, _soiName + ".HandleRESTRequest()",
                    200, "Request received in Layer Access SOI for handleRESTRequest");
 
                var authorizedLayerSet = GetAuthorizedLayers(_restServiceSOI);
 
                var restInput = new RESTRequestParameters
                        {
                            Capabilities = capabilities,
                            ResourceName = resourceName,
                            OperationName = operationName,
                            OperationInput = operationInput,
                            OutputFormat = outputFormat,
                            RequestProperties = requestProperties
                        };
 
                var opCode = GetHandlerOpCode(restInput);
                RestFilterOperation filterOp = _operationMap.ContainsKey(opCode) ? _operationMap[opCode] : null;
                return FilterRESTRequest(filterOp, restInput, authorizedLayerSet, out responseProperties);
            }
            catch (Exception e)
            {
                _serverLog.LogMessage(ServerLogger.msgType.error, _soiName + ".HandleRESTRequest()", 500, "Exception: " + e.Message + " in " + e.StackTrace);
                throw;
            }
        }
        #endregion
 
        #region REST Pre-filters
 
        private static RESTRequestParameters PreFilterExport ( RESTRequestParameters restInput, HashSet<string> authorizedLayers )
        {
            JavaScriptSerializer sr = new JavaScriptSerializer { MaxJsonLength = int.MaxValue };
            var operationInputJson = sr.DeserializeObject(restInput.OperationInput) as IDictionary<stringobject>;
 
            operationInputJson["layers"] = "show:" + String.Join(",", authorizedLayers);
 
            restInput.OperationInput = sr.Serialize(operationInputJson);
            return restInput;
        }
 
 
        private static RESTRequestParameters PreFilterFindAndKml ( RESTRequestParameters restInput, HashSet<string> authorizedLayers )
        {
            JavaScriptSerializer sr = new JavaScriptSerializer { MaxJsonLength = int.MaxValue };
            var operationInputJson = sr.DeserializeObject(restInput.OperationInput) as IDictionary<stringobject>;
 
            var removeSpacesRegEx = new Regex("\\s+");
            String requestedLayers = operationInputJson.ContainsKey("layers") ? operationInputJson["layers"].ToString() : "";
            requestedLayers = removeSpacesRegEx.Replace(requestedLayers, "");
            operationInputJson["layers"] = RemoveUnauthorizedLayersFromRequestedLayers(requestedLayers, authorizedLayers);
 
            restInput.OperationInput = sr.Serialize(operationInputJson);
            return restInput;
        }
 
        private static RESTRequestParameters PreFilterIdentify ( RESTRequestParameters restInput, HashSet<string> authorizedLayers )
        {
            JavaScriptSerializer sr = new JavaScriptSerializer { MaxJsonLength = int.MaxValue };
            var operationInputJson = sr.DeserializeObject(restInput.OperationInput) as IDictionary<stringobject>;
 
            String requestedLayers = operationInputJson.ContainsKey("layers") ? operationInputJson["layers"].ToString() : "";
            if (string.IsNullOrEmpty(requestedLayers) || requestedLayers.StartsWith("top") || requestedLayers.StartsWith("all"))
            {
                operationInputJson["layers"] = "visible:" + authorizedLayers;
            }
            else if (requestedLayers.StartsWith("visible"))
            {
                var reqLayerParts = requestedLayers.Split(':');
                var removeSpacesRegEx = new Regex("\\s+");
                operationInputJson["layers"] = "visible:" +
                    RemoveUnauthorizedLayersFromRequestedLayers(
                        removeSpacesRegEx.Replace(reqLayerParts[1], ""), authorizedLayers);
            }
            else
            {
                operationInputJson["layers"] = "visible:" + authorizedLayers;
            }
 
            restInput.OperationInput = sr.Serialize(operationInputJson);
            return restInput;
        }
 
        private static RESTRequestParameters PreFilterLayerQuery ( RESTRequestParameters restInput, HashSet<string> authorizedLayers )
        {
            JavaScriptSerializer sr = new JavaScriptSerializer { MaxJsonLength = int.MaxValue };
            var operationInputJson = sr.DeserializeObject(restInput.OperationInput) as IDictionary<stringobject>;
            var resName = restInput.ResourceName.TrimStart('/');
            var rnameParts = resName.Split('/');
            var requestedLayerId = rnameParts[1];
 
            if (!authorizedLayers.Contains(requestedLayerId))
            {
                var errorReturn = new Dictionary<stringobject>
                                    {
                                        {"error"new Dictionary<stringobject>
                                                {
                                                    {"code", 404},
                                                    {"message""Access Denied"}
                                                }
                                        }
                                    };
 
                throw new RESTErrorException(sr.Serialize(errorReturn));
            }
 
            restInput.OperationInput = sr.Serialize(operationInputJson);
            return restInput;
        }
 
        #endregion
 
 
        #region REST Post-filters
 
        /// <summary>
        /// 
        /// Filter REST root service info response.
        /// IMPORTANT: the JSON structure returned by the REST handler root resource 
        /// differs from what you usually see as the response from the service REST endpoint.
        /// 
        /// </summary>
        /// <param name="restInput"></param>
        /// <param name="originalResponse"></param>
        /// <param name="authorizedLayerSet"></param>
        /// <returns>Filtered json</returns>
        private static String PostFilterRESTRoot ( RESTRequestParameters restInput, String originalResponse, HashSet<String> authorizedLayerSet )
        {
            if (null == authorizedLayerSet)
                return null;
            //restInput is not used here, but may be used later as needed
 
            try
            {
                // Perform JSON filtering
                // Note: The JSON syntax can change and you might have to adjust this piece of code accordingly
                if (authorizedLayerSet == null)
                    return null;
 
                /*
                 * Remove unauthorized layer information from 1. Under 'contents' tag 2. Under 'resources' tag
                 * 2.1 'layers' 2.2 'tables' 2.3 'legend'
                 */
 
                JavaScriptSerializer sr = new JavaScriptSerializer { MaxJsonLength = int.MaxValue };
                var jsonResObj = sr.DeserializeObject(originalResponse) as IDictionary<stringobject>;
 
                // Filter for 'contents' tag
                var contentsJO = jsonResObj["contents"as IDictionary<stringobject>;
                var layersJA = contentsJO["layers"as object[];
                var updatedLayers = new List<object>();
                foreach (var layerO in layersJA)
                {
                    var layerJO = layerO as IDictionary<stringobject>;
                    var id = layerJO["id"].ToString();
                    if (authorizedLayerSet.Contains(id))
                    {
                        updatedLayers.Add(layerJO);
                    }
                }
                contentsJO["layers"] = updatedLayers.ToArray();
 
 
                // Filter for 'resources' tag, very simplified filtering, may fail if ordering changes
                var allResourcesJA = jsonResObj["resources"as object[];
                var layersRJO = allResourcesJA.FirstOrDefault(e =>
                {
                    var jo = e as IDictionary<stringobject>;
                    if (!jo.ContainsKey("name"))
                        return false;
                    var name = jo["name"].ToString();
                    return ("layers" == name);
                }) as IDictionary<stringobject>;
 
                var tablesRJO = allResourcesJA.FirstOrDefault(e =>
                {
                    var jo = e as IDictionary<stringobject>;
                    if (!jo.ContainsKey("name"))
                        return false;
                    var name = jo["name"].ToString();
                    return ("tables" == name);
                }) as IDictionary<stringobject>;
 
                var legendRJO = allResourcesJA.FirstOrDefault(e =>
                {
                    var jo = e as IDictionary<stringobject>;
                    if (!jo.ContainsKey("name"))
                        return false;
                    var name = jo["name"].ToString();
                    return ("legend" == name);
                }) as IDictionary<stringobject>;
 
                //filter and replace layers
                var filteredLayerResourceJA = new List<object>();
                if (null != layersRJO)
                {
                    // Filter for 'resources -> layers -> resources' tag
                    var layerResourceJA = layersRJO["resources"as object[];
                    foreach (var lrJO in layerResourceJA)
                    {
                        var lrJODict = lrJO as IDictionary<stringobject>;
                        if (authorizedLayerSet.Contains(lrJODict["name"].ToString()))
                        {
                            filteredLayerResourceJA.Add(lrJO);
                        }
                    }
                }
                layersRJO["resources"] = filteredLayerResourceJA.ToArray();
 
                //filter and replace tables
                var filteredTableResourceJA = new List<object>();
                if (null != tablesRJO)
                {
                    // Filter for 'resources -> tables -> resources' tag
                    var tableResourceJA = tablesRJO["resources"as object[];
                    foreach (var tbJO in tableResourceJA)
                    {
                        var tbJODict = tbJO as IDictionary<stringobject>;
                        if (authorizedLayerSet.Contains(tbJODict["name"].ToString()))
                        {
                            filteredTableResourceJA.Add(tbJO);
                        }
                    }
                }
                tablesRJO["resources"] = filteredTableResourceJA.ToArray();
 
                //filter and replace legend layers
                var filteredLegendLayersRJA = new List<object>();
                if (null != legendRJO)
                {
                    // Filter for 'resources -> legend -> contents->layers' tag
                    var legendContentsJO = legendRJO["contents"as IDictionary<stringobject>;
                    var legendLayersJA = legendContentsJO["layers"as object[];
                    foreach (var lgJO in legendLayersJA)
                    {
                        var lgJODict = lgJO as IDictionary<stringobject>;
                        if (authorizedLayerSet.Contains(lgJODict["layerId"].ToString()))
                        {
                            filteredLegendLayersRJA.Add(lgJO);
                        }
                    }
                    legendContentsJO["layers"] = filteredLegendLayersRJA.ToArray();
                }
 
                // Return the filter response
                return sr.Serialize(jsonResObj);
            }
            catch (Exception ignore)
            {
                return null;
            }
        }
 
 
        private static string PostFilterRootLayers ( RESTRequestParameters restInput, String originalResponse, HashSet<String> authorizedLayerSet )
        {
            if (null == authorizedLayerSet)
                return null;
            //restInput is not used here, but may be used later as needed
 
            try
            {
                // Perform JSON filtering
                // Note: The JSON syntax can change and you might have to adjust this piece of code accordingly
                if (authorizedLayerSet == null)
                    return null;
 
                /*
                 * Remove unauthorized layer information from 1. Under 'contents' tag 2. Under 'resources' tag
                 * 2.1 'layers' 2.2 'tables' 2.3 'legend'
                 */
 
                JavaScriptSerializer sr = new JavaScriptSerializer { MaxJsonLength = int.MaxValue };
                var jsonResObj = sr.DeserializeObject(originalResponse) as IDictionary<stringobject>;
 
                // Filter for 'contents' tag
                var layersJA = jsonResObj["layers"as object[];
                var updatedLayers = new List<object>();
                foreach (var layerO in layersJA)
                {
                    var layerJO = layerO as IDictionary<stringobject>;
                    var id = layerJO["id"].ToString();
                    if (authorizedLayerSet.Contains(id))
                    {
                        updatedLayers.Add(layerJO);
                    }
                }
                jsonResObj["layers"] = updatedLayers.ToArray();
                // Return the filter response
                return sr.Serialize(jsonResObj);
            }
            catch (Exception ignore)
            {
                return null;
            }
        }
 
        private static string PostFilterRootLegend ( RESTRequestParameters restInput, String originalResponse, HashSet<String> authorizedLayerSet )
        {
            if (null == authorizedLayerSet)
                return null;
            //restInput is not used here, but may be used later as needed
 
            try
            {
                // Perform JSON filtering
                // Note: The JSON syntax can change and you might have to adjust this piece of code accordingly
                if (authorizedLayerSet == null)
                    return null;
 
                /*
                 * Remove unauthorized layer information from 1. Under 'contents' tag 2. Under 'resources' tag
                 * 2.1 'layers' 2.2 'tables' 2.3 'legend'
                 */
 
                JavaScriptSerializer sr = new JavaScriptSerializer { MaxJsonLength = int.MaxValue };
                var jsonResObj = sr.DeserializeObject(originalResponse) as IDictionary<stringobject>;
 
                // Filter for 'contents' tag
                var layersJA = jsonResObj["layers"as object[];
                var updatedLayers = new List<object>();
                foreach (var layerO in layersJA)
                {
                    var layerJO = layerO as IDictionary<stringobject>;
                    var id = layerJO["layerId"].ToString();
                    if (authorizedLayerSet.Contains(id))
                    {
                        updatedLayers.Add(layerJO);
                    }
                }
                jsonResObj["layers"] = updatedLayers.ToArray();
                // Return the filter response
                return sr.Serialize(jsonResObj);
            }
            catch (Exception ignore)
            {
                return null;
            }
        }
 
        #endregion
 
        #region SOAP interceptors
 
        public byte[] HandleStringWebRequest ( esriHttpMethod httpMethod, string requestURL,
            string queryString, string Capabilities, string requestData,
            out string responseContentType, out esriWebResponseDataType respDataType )
        {
            _serverLog.LogMessage(ServerLogger.msgType.infoStandard, _soiName + ".HandleStringWebRequest()",
                200, "Request received in Layer Access SOI for HandleStringWebRequest");
 
            /*
             * Add code to manipulate OGC (WMS, WFC, WCS etc) requests here 
             */
 
            IWebRequestHandler webRequestHandler = _restServiceSOI.FindRequestHandlerDelegate<IWebRequestHandler>();
            if (webRequestHandler != null)
            {
                var response = webRequestHandler.HandleStringWebRequest(
                        httpMethod, requestURL, queryString, Capabilities, requestData, out responseContentType, out respDataType);
 
 
                return response;
            }
 
            responseContentType = null;
            respDataType = esriWebResponseDataType.esriWRDTPayload;
            //Insert error response here.
            return null;
        }
 
        public byte[] HandleBinaryRequest ( ref byte[] request )
        {
            _serverLog.LogMessage(ServerLogger.msgType.infoStandard, _soiName + ".HandleBinaryRequest()",
                  200, "Request received in Layer Access SOI for HandleBinaryRequest");
 
            /*
             * Add code to manipulate Binary requests from desktop here
             */
 
            // Generate a set of authorized layers for the user
            var authorizedLayerSet = GetAuthorizedLayers(_soapServiceSOI);
 
            IMessage requestMessage = SoapServiceSOI.ConvertBinaryRequestToMessage(request);
 
            SoapBinaryRequest filteredRequest = FilterSoapRequest(requestMessage, SoapRequestMode.Binary, authorizedLayerSet) as SoapBinaryRequest;
            if (null == filteredRequest)
            {
                filteredRequest = new SoapBinaryRequest { Body = request };
            }
 
            IRequestHandler requestHandler = _restServiceSOI.FindRequestHandlerDelegate<IRequestHandler>();
            if (requestHandler != null)
            {
                var response = requestHandler.HandleBinaryRequest(filteredRequest.Body);
 
                // Perform filtering for GetServerInfoResponse
                // Convert the XML request into a generic IMessage
                IMessage responseMessage = SoapServiceSOI.ConvertBinaryRequestToMessage(response);
                // Get operation name
                String name = responseMessage.Name;
                if ("GetServerInfoResponse".Equals(name, StringComparison.CurrentCultureIgnoreCase))
                {
                    // Intercept the response and perform filtering
                    var filteredResponse = FilterSoapRequest(responseMessage, SoapRequestMode.Binary, authorizedLayerSet) as SoapBinaryRequest;
                    if (filteredResponse != null)
                    {
                        response = filteredResponse.Body;
                    }
                }
                return response;
            }
 
            //Insert error response here.
            return null;
        }
 
        public byte[] HandleBinaryRequest2 ( string Capabilities, ref byte[] request )
        {
            _serverLog.LogMessage(ServerLogger.msgType.infoStandard, _soiName + ".HandleBinaryRequest2()",
                  200, "Request received in Layer Access SOI for HandleBinaryRequest2");
 
            IMessage requestMessage = SoapServiceSOI.ConvertBinaryRequestToMessage(request);
 
            var authorizedLayerSet = GetAuthorizedLayers(_soapServiceSOI);
 
            SoapBinaryRequest filteredRequest = FilterSoapRequest(requestMessage, SoapRequestMode.Binary, authorizedLayerSet) as SoapBinaryRequest;
            if (null == filteredRequest)
            {
                filteredRequest = new SoapBinaryRequest { Body = request };
            }
 
            IRequestHandler2 requestHandler = _restServiceSOI.FindRequestHandlerDelegate<IRequestHandler2>();
            if (requestHandler != null)
            {
                var response = requestHandler.HandleBinaryRequest2(Capabilities, filteredRequest.Body);
                
                // Perform filtering for GetServerInfoResponse
                // Convert the XML request into a generic IMessage
                IMessage responseMessage = SoapServiceSOI.ConvertBinaryRequestToMessage(response);
                // Get operation name
                String name = responseMessage.Name;
                if ("GetServerInfoResponse".Equals(name, StringComparison.CurrentCultureIgnoreCase))
                {
                    // Intercept the response and perform filtering
                    var filteredResponse = FilterSoapRequest(responseMessage, SoapRequestMode.Binary, authorizedLayerSet) as SoapBinaryRequest;
                    if (filteredResponse != null)
                    {
                        response = filteredResponse.Body;
                    }
                }
                return response;
            }
 
            //Insert error response here.
            return null;
        }
 
        public string HandleStringRequest ( string Capabilities, string request )
        {
            _serverLog.LogMessage(ServerLogger.msgType.infoStandard, _soiName + ".HandleStringRequest()",
                   200, "Request received in Layer Access SOI for HandleStringRequest");
 
            // Convert the XML request into a generic IMessage
            IMessage requestMessage = SoapServiceSOI.ConvertStringRequestToMessage(request);
 
            // Generate a set of authorized layers for the user
            var authorizedLayerSet = GetAuthorizedLayers(_soapServiceSOI);
 
            // Intercept the request and perform filtering
            var filteredRequest = FilterSoapRequest(requestMessage, SoapRequestMode.Xml, authorizedLayerSet) as SoapStringRequest;
            if (filteredRequest == null)
            {
                filteredRequest = new SoapStringRequest { Body = request };
            }
 
            IRequestHandler requestHandler = _restServiceSOI.FindRequestHandlerDelegate<IRequestHandler>();
            if (requestHandler != null)
            {
                var response = requestHandler.HandleStringRequest(Capabilities, filteredRequest.Body);
 
                // Perform filtering for GetServerInfoResponse
                // Convert the XML request into a generic IMessage
                IMessage responseMessage = SoapServiceSOI.ConvertStringRequestToMessage(response);
                // Get operation name
                String name = responseMessage.Name;
                if ("GetServerInfoResponse".Equals(name, StringComparison.CurrentCultureIgnoreCase))
                {
                    // Intercept the response and perform filtering
                    var filteredResponse = FilterSoapRequest(responseMessage, SoapRequestMode.Xml, authorizedLayerSet) as SoapStringRequest;
                    if (filteredResponse != null)
                    {
                        response = filteredResponse.Body;
                    }
                }
                return response;
            }
 
            //Insert error response here.
            return null;
        }
 
 
        /// <summary>
        /// 
        /// </summary>
        /// <param name="inRequest"></param>
        /// <param name="mode"></param>
        /// <returns></returns>
        private SoapRequest FilterSoapRequest ( IMessage inRequest, SoapRequestMode mode, HashSet<string> authorizedLayerSet )
        {
            // Get operation name
            String name = inRequest.Name;
 
            // Apply filter only on those operations we care about
            var nameInLowerCase = name.ToLower();
            switch (nameInLowerCase)
            {
                case "find":
                    inRequest = FilterLayerIds(inRequest, mode, authorizedLayerSet);
                    break;
                case "exportmapimage":
                    inRequest = FilterMapDescription(inRequest, mode, authorizedLayerSet);
                    break;
                case "identify":
                    inRequest = FilterLayerIds(inRequest, mode, authorizedLayerSet);
                    break;
                case "getlegendinfo":
                    inRequest = FilterLayerIds(inRequest, mode, authorizedLayerSet);
                    break;
                case "getserverinforesponse":
                    inRequest = FilterGetServerInfoResponse(inRequest, mode, authorizedLayerSet);
                    break;
            }
 
            return SoapRequestFactory.Create(mode, inRequest);
        }
 
        /// <summary>
        /// 
        /// </summary>
        /// <param name="inRequest"></param>
        /// <param name="mode"></param>
        /// <param name="authorizedLayerSet"></param>
        /// <returns></returns>
        private IMessage FilterLayerIds ( IMessage inRequest, SoapRequestMode mode, HashSet<String> authorizedLayerSet )
        {
            // 1. Find the index of the layers parameter
            // 2. Get the value for the interested parameter
            // 3. Manipulate it
            // 4. Put it back into IMessage
 
            int idx = -1;
            IXMLSerializeData inRequestData = inRequest.Parameters;
            try
            {
                idx = inRequestData.Find("LayerIDs");
            }
            catch (Exception ignore)
            {
            }
 
            LongArray layerIdInLA = null;
            if (idx >= 0)
            {
                // Get all the requested layers
                layerIdInLA = (LongArray)inRequestData.GetObject(idx, inRequest.NamespaceURI, "ArrayOfInt");
 
                // Perform filtering based on access to different layers. 
                // Remove restricted ids in-place
                for (int i = layerIdInLA.Count - 1; i >= 0; i--)
                {
 
                    if (!authorizedLayerSet.Contains(layerIdInLA.Element[i].ToString()))
                    {
                        layerIdInLA.Remove(i);
                    }
                }
            }
            else //no LayerIDs specified, attaching authorized layer list instead
            {
                layerIdInLA = new LongArrayClass();
                foreach (var goodLayerId in authorizedLayerSet)
                {
                    layerIdInLA.Add(Int32.Parse(goodLayerId));
                }
                inRequestData.AddObject("LayerIDs", layerIdInLA);
            }
 
            // If binary request we dont have to create and copy in a new Message object
            if (mode == SoapRequestMode.Binary)
            {
                return inRequest;
            }
 
            // Create new request message
            IMessage modifiedInRequest = _soapServiceSOI.CreateNewIMessage(inRequest);
            IXMLSerializeData modifiedInRequestData = modifiedInRequest.Parameters;
 
            // Put all parameters back in IMessage
            for (int i = 0; i < inRequestData.Count; i++)
            {
                if (_soapServiceSOI.GetSoapOperationParameterName(inRequest.Name, i).Equals("LayerIDs"StringComparison.CurrentCultureIgnoreCase))
                {
                    // Add the modified MapDescription
                    _soapServiceSOI.AddObjectToXMLSerializeData(_soapServiceSOI.GetSoapOperationParameterName(inRequest.Name, i),
                      layerIdInLA, _soapServiceSOI.GetSoapOperationParameterTypeName(inRequest.Name, i), modifiedInRequestData);
                }
                else
                {
                    /*
                     * Add other parameters as is. Here we are using the SOI helper to add and get parameters
                     * because we don't care about the type we just want to copy from one IMessage object to
                     * another.
                     */
                    _soapServiceSOI.AddObjectToXMLSerializeData(
                      _soapServiceSOI.GetSoapOperationParameterName(inRequest.Name, i),
                      _soapServiceSOI.GetObjectFromXMLSerializeData(i, inRequest.NamespaceURI,
                          _soapServiceSOI.GetSoapOperationParameterTypeName(inRequest.Name, i), inRequestData),
                      _soapServiceSOI.GetSoapOperationParameterTypeName(inRequest.Name, i), modifiedInRequestData);
                }
            }
 
            return modifiedInRequest;
        }
 
        private IMessage FilterMapDescription ( IMessage inRequest, SoapRequestMode mode, HashSet<string> authorizedLayerSet )
        {
            // 1. Find the index of the MapDescription parameter
            // 2. Get the value for the interested parameter
            // 3. Manipulate it
            // 4. Put it back into IMessage
 
            // Get the parameters out from the request object
            int idx = -1;
            IXMLSerializeData inRequestData = inRequest.Parameters;
            idx = inRequestData.Find("MapDescription");
 
            MapDescription md =
                (MapDescription)inRequestData.GetObject(idx, inRequest.NamespaceURI,
                    _soapServiceSOI.GetSoapOperationParameterTypeName(inRequest.Name, idx));
            // Manipulate the MapDescription to perform layer level security
            ILayerDescriptions lds = md.LayerDescriptions;
            for (int i = 0; i < lds.Count; i++)
            {
                ILayerDescription ld = lds.Element[i];
                if (!authorizedLayerSet.Contains(ld.ID.ToString()))
                {
                    ld.Visible = false;
                }
            }
 
            // If binary request we dont have to create and copy in a new Message object
            if (mode == SoapRequestMode.Binary)
            {
                return inRequest;
            }
 
            // Create new request message
            IMessage modifiedInRequest = _soapServiceSOI.CreateNewIMessage(inRequest);
            IXMLSerializeData modifiedInRequestData = modifiedInRequest.Parameters;
 
            // Put all parameters back in IMessage
            for (int i = 0; i < inRequestData.Count; i++)
            {
                if (_soapServiceSOI.GetSoapOperationParameterName(inRequest.Name, i).Equals("MapDescription"StringComparison.CurrentCultureIgnoreCase))
                {
                    // Add the modified MapDescription
                    modifiedInRequestData.AddObject(_soapServiceSOI.GetSoapOperationParameterName(inRequest.Name, i), md);
                }
                else
                {
                    /*
                     * Add other parameters as is. Here we are using the SOI helper to add and get parameters
                     * because we don't care about the type we just want to copy from one IMessage object to
                     * another.
                     */
                    modifiedInRequestData.AddObject(
                        _soapServiceSOI.GetSoapOperationParameterName(inRequest.Name, i),
                        inRequestData.GetObject(i, inRequest.NamespaceURI,
                            _soapServiceSOI.GetSoapOperationParameterTypeName(inRequest.Name, i)));
                }
            }
 
            return modifiedInRequest;
        }
 
        private IMessage FilterGetServerInfoResponse ( IMessage inRequest, SoapRequestMode mode, HashSet<string> authorizedLayerSet )
        {
            int idx = -1;
            IXMLSerializeData inRequestData = inRequest.Parameters;
            idx = inRequestData.Find("Result");
 
            // Get MapServerInfo
            MapServerInfo mapServerInfo = (MapServerInfo)inRequestData.GetObject(idx, inRequest.NamespaceURI,
                    "MapServerInfo");
 
            // Perform filtering based on access to different layers
            IMapLayerInfos layerInfos = mapServerInfo.MapLayerInfos;
            for (int i = layerInfos.Count - 1; i >= 0; i--)
            {
                if (!authorizedLayerSet.Contains(layerInfos.Element[i].ID.ToString()))
                {
                    layerInfos.Remove(i);
                }
            }
 
            IMapDescription mapDescription = mapServerInfo.DefaultMapDescription;
            ILayerDescriptions lds = mapDescription.LayerDescriptions;
            for (int i = lds.Count - 1; i >= 0; i--)
            {
                if (!authorizedLayerSet.Contains(lds.Element[i].ID.ToString()))
                {
                    lds.Remove(i);
                }
            }
 
            // If binary request we don't have to create and copy in a new Message object
            if (mode == SoapRequestMode.Binary)
            {
                return inRequest;
            }
 
            // Create new request message
            IMessage modifiedInRequest = _soapServiceSOI.CreateNewIMessage(inRequest);
            IXMLSerializeData modifiedInRequestData = modifiedInRequest.Parameters;
 
            // Put all parameters back in IMessage
            for (int i = 0; i < inRequestData.Count; i++)
            {
                if (_soapServiceSOI.GetSoapOperationParameterName(inRequest.Name, i).Equals("Result"StringComparison.CurrentCultureIgnoreCase))
                {
                    // Add the modified MapDescription
                    modifiedInRequestData.AddObject(_soapServiceSOI.GetSoapOperationParameterName(inRequest.Name, i), mapServerInfo);
                }
                else
                {
                    /*
                     * Add other parameters as is. Here we are using the SOI helper to add and get parameters
                     * because we don't care about the type we just want to copy from one IMessage object to
                     * another.
                     */
                    modifiedInRequestData.AddObject(
                        _soapServiceSOI.GetSoapOperationParameterName(inRequest.Name, i),
                        inRequestData.GetObject(i, inRequest.NamespaceURI,
                            _soapServiceSOI.GetSoapOperationParameterTypeName(inRequest.Name, i)));
                }
            }
 
            return modifiedInRequest;
        }
 
        #endregion
 
        #region Utility code
 
        /// <summary>
        /// Remove unauthorized layers from request. 
        /// </summary>
        /// <param name="requestedLayers">layer user is requesting information from</param>
        /// <param name="authorizedLayers">layers user is authorized to fetch information from</param>
        /// <returns></returns>
        private static String RemoveUnauthorizedLayersFromRequestedLayers (
                String requestedLayers, HashSet<String> authorizedLayers )
        {
            if (0 == authorizedLayers.Count)
                return "-1";
 
            // requested layers
            IEnumerable<String> requestedLayersList = null;
            try
            {
                requestedLayersList = requestedLayers.Split(new[] { ',' }).Select(e => e.Trim());
            }
            catch (Exception ignore) { }
 
            if (authorizedLayers != null)
            {
                var filteredLayers = requestedLayersList.Where(e => authorizedLayers.Contains(e));
                if (!filteredLayers.Any())
                    return "-1";
 
                return String.Join(",", filteredLayers.ToArray());
            }
            return "-1";
        }
 
 
        /// <summary>
        ///  Read permission information from disk
        /// </summary>
        /// <param name="fileName">path and name of the file to read permissions from</param>
        /// <returns></returns>
        private Dictionary<StringString> ReadPermissionFile ( String fileName )
        {
            // read the permissions file
            Dictionary<StringString> permissionMap = new Dictionary<StringString>();
            try
            {
 
                if (!File.Exists(fileName))
                    return permissionMap;
 
                String jsonStr = File.ReadAllText(fileName);
 
 
                var json = new ESRI.ArcGIS.SOESupport.JsonObject(jsonStr);
                System.Object[] permissions = null;
                // create a map of permissions
                // read the permissions array
                if (!json.TryGetArray("permissions"out permissions) || permissions == null)
                    return permissionMap;
 
 
                // add to map
                foreach (var permsObj in permissions)
                {
                    ESRI.ArcGIS.SOESupport.JsonObject permsJO = permsObj as ESRI.ArcGIS.SOESupport.JsonObject;
                    if (null == permsJO) continue;
 
                    // get the fqsn or service name
                    String fqsn = string.Empty;
                    permsJO.TryGetString("fqsn"out fqsn);
                    // read the permission for that service
                    System.Object[] permArray = null;
                    if (!permsJO.TryGetArray("permission"out permArray) || permArray == null)
                        continue;
 
                    foreach (var permObj in permArray)
                    {
                        ESRI.ArcGIS.SOESupport.JsonObject permJO = permObj as ESRI.ArcGIS.SOESupport.JsonObject;
                        String role = string.Empty;
                        if (!permJO.TryGetString("role"out role))
                            continue;
 
                        String authorizedLayers = string.Empty;
                        permJO.TryGetString("authorizedLayers"out authorizedLayers); //may be empty or null
                        permissionMap.Add(fqsn + "." + role, authorizedLayers);
                    }
                }
            }
            catch (Exception ignore)
            {
                //TODO error handling
            }
            return permissionMap;
        }
 
        private HashSet<string> GetAuthorizedLayers ( ServiceSOIBase soi )
        {
            var userRoleSet = soi.GetRoleInformation(soi.ServerEnvironment.UserInfo);
            HashSet<String> authorizedLayerSet = null;
 
            if (null == userRoleSet)
                return authorizedLayerSet;
 
            /*
            * Generate a set of authorized layers for the user
            */
 
            var fullServiceName = _serverObject.ConfigurationName + "." + _serverObject.TypeName;
 
            var removeSpacesRegEx = new Regex("\\s+");
            var authorizedRoles = userRoleSet.Where(role=>_servicePermissionMap.ContainsKey(fullServiceName + "." + role));
            var authorizedLayerList = authorizedRoles.SelectMany(role => removeSpacesRegEx.
                                                                                  Replace(_servicePermissionMap[fullServiceName + "." + role], "").
                                                                                  Split(','));
            return new HashSet<string>(authorizedLayerList);
        }
        #endregion
    }