Comment journaliser dans un flux UDP visible du réseau local avec NLog

Pressés? Sautez directement à la solution.

Le contexte

La librairie NLog est très bien documentée sur Internet pour une utilisation régulière. Par exemple, la configuration suivante (fichier NLog.config) permet de journaliser dans un fichier portant la date du jour dans un répertoire logs en mode Trace :

<nlog xmlns= »http://www.nlog-project.org/schemas/NLog.xsd »
      xmlns:xsi= »http://www.w3.org/2001/XMLSchema-instance »
      xsi:schemaLocation= »http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd »
autoReload= »true »
throwExceptions= »false »
internalLogLevel= »Off » internalLogFile= »c:\temp\nlog-internal.log »>

  <targets>
<target xsi:type= »File » name= »f » fileName= »${basedir}/logs/${shortdate}.log »
layout= »${longdate} ${uppercase:${level}} ${message} ${when:when=(level = LogLevel.Error):inner=${newline}${exception:format=ToString}} » deleteOldFileOnStartup= »false » />
</targets>
<rules>
<logger name= »* » minlevel= »Trace » writeTo= »f » />
</rules>
</nlog>

Par contre, la journalisation dans un fichier n’est pas toujours pratique. Par exemple, si vous développez une composante serveur, il se peut que les équipes souhaitant consulter le journal n’aient pas un accès physique au répertoire contenant le fichier. Aussi, le compte Windows configuré pour le processus (EXE) qui exécute le code qui journalise doit avoir les droits pour écrire ce fichier.

Je me suis donc mis à chercher une solution qui permettrait de visualiser le journal à distance, à partir d’un poste client, sans passer par un fichier.

La démarche

Je savais que la librairie NLog pouvait être utilisée avec un outil de visualisation de journal tel que Sentinel ou Log2Console. Il suffit d’ajouter la cible suivante et  de l’ajouter dans les règles :

<target xsi:type= »NLogViewer » name= »v » address= »udp://127.0.0.1:9999″/>

<logger name= »* » minlevel= »Trace » writeTo= »v » />

Mais cela fonctionne uniquement lorsque l’outil de visualisation est installé et exécuté localement sur le serveur. Ce n’est pas très avantageux vis-à-vis l’utilisation d’un fichier.

Ma première idée a été d’utiliser une adresse IP multicast (224.0.0.0 à 239.255.255.255) pour le flux UDP. Malheureusement, cela n’a pas fonctionné lors de mes tests. J’ai épuisé toutes mes idées de combinaisons de mots clé à rechercher sur Google pour NLog et multicast et je n’ai rien trouvé.

L’avantage d’un projet à code source libre est que la meilleure documentation reste encore le code en soi. J’ai découvert que les cibles Chainsaw et NLogViewer étaient des spécialisations de la cible Network :

image

Je suis donc allé étudier le code pour voir s’il n’y avait pas une manière de diffuser le flux UDP sur le réseau, plutôt que de toujours fonctionner localement. Lorsque la classe NetworkTarget est configurée avec une adresse UDP (udp://ip:port), sa fabrique de “NetworkSender” crée un UdpNetworkSender. C’est dans cette classe que se cachait le secret pour diffuser le journal sur le réseau local: la méthode CreateSocket contient une condition intéressante:

Uri uri;

if (Uri.TryCreate(this.Address, UriKind.Absolute, out uri) && uri.Host.Equals(IPAddress.Broadcast.ToString(), StringComparison.InvariantCultureIgnoreCase))
{
proxy.UnderlyingSocket.EnableBroadcast = true;
}

La documentation de la propriété EnableBroadcast indique clairement qu’il peut s’agir d’une technique pour diffuser le journal:

Broadcasting is limited to a specific subnet, and must use User Datagram Protocol (UDP.) For Internet Protocol version 4, you can broadcast to your local subnet by sending a packet to 255.255.255.255; or you can use the directed broadcast address, which is the network portion of an Internet Protocol (IP) address with all bits set in the host portion. For example, if your IP address is 192.168.1.40 (a Class C address, with a netmask of 255.255.255.0 — the network portion is the first three octets, and the host portion is the last octet), your directed broadcast address is 192.168.1.255.

Et la condition de la méthode CreateSocket teste si l’adresse de la cible est égale à IPAddress.Broadcast (255.255.255.255) avant de la mettre à true. Bingo!

La solution

La recette est donc la suivante :

<nlog xmlns= »http://www.nlog-project.org/schemas/NLog.xsd »
      xmlns:xsi= »http://www.w3.org/2001/XMLSchema-instance »
      xsi:schemaLocation= »http://www.nlog-project.org/schemas/NLog.xsd NLog.xsd »
autoReload= »true »
throwExceptions= »false »
internalLogLevel= »Off » internalLogFile= »c:\temp\nlog-internal.log »>

  <targets>
<target xsi:type= »Chainsaw » name= »c »  address= »udp://255.255.255.255:9998″ />
</targets>
<rules>
<logger name= »* » minlevel= »Trace » writeTo= »c » />
</rules>
</nlog>

Personnellement je trouve que l’outil Log2Console est moins long à mettre en place sur le poste client. Il suffit de configurer un Receiver sur le même port que celui spécifié pour le « target » du programme qui utilise NLog pour journaliser :

clip_image002

image

Par contre, Sentinel offre plus d’options et permet de sauvegarder la configuration dans un fichier pour une utilisation ultérieure. Voici les étapes :

clip_image002[4]

image

image

image

image

image

image

image

Projet de démonstration

J’ai publié un projet de démonstration sur mon GitHub que vous pouvez utiliser pour tester les cibles NLog décrites dans cette entrée de blogue.

image

En cliquant sur le bouton, le journal contiendra une entrée pour chaque niveau de log supporté par NLog. ll y a trois cibles: File, NLogViewer et Chainsaw. La première cible crée un fichier portant la date du jour dans le répertoire logs. La seconde peut être utilisée avec un outil de visualisation de journal, s’exécutant sur le même poste. La dernière cible démontre l’utilisation de la fonctionnalité de Broadcast en UDP et il est alors possible d’exécuter le programme de visualisation de journal sur un autre poste.

<target xsi:type= »File » name= »f » fileName= »${basedir}/logs/${shortdate}.log »
layout= »${longdate} ${uppercase:${level}} ${message} ${when:when=(level = LogLevel.Error):inner=${newline}${exception:format=ToString}} » deleteOldFileOnStartup= »false » />

<target xsi:type= »NLogViewer » name= »v » address= »udp://127.0.0.1:9999″/>

<target xsi:type= »Chainsaw » name= »c »  address= »udp://255.255.255.255:9998″ />

La conclusion

J’aurais préféré trouver une solution utilisant le protocole UDP en mode multicast. Les routeurs sont intelligents et peuvent envoyer les packets uniquement aux ordinateurs “inscrits” pour un groupe multicast (une adresse IP). De plus, l’outil Log2Console a un champ pour inscrire l’adresse de ce groupe dans sa configuration:

image

Malheureusement je n’y suis pas arrivé. Je préfère ne pas avoir à modifier le code source de NLog alors je devrai me contenter de l’approche utilisant le Broadcast.

Utiliser BIDS 2012 pour déployer vers Reporting Services 2008 (R1 ou R2)

Contexte

Si on prend pour acquis que Reporting Services 2008 est la première version acceptable pour le développement d’applications BI, il y a quand même plusieurs options pour le développement de rapports:

BIDS Version

Visual Studio

Description

2008 R1 2008 Installé à partir du DVD de SQL Server 2008.
2008 R2 2008 Installé à partir du DVD de SQL Server 2008 R2.
2012 2010 ou 2012 Installé à partir du DVD de SQL Server 2012. S’enregistre dans VStudio 2010 et / ou 2013.

Au niveau de la du schéma XML des fichiers de rapports (RDL), il y a une différence entre R1 et R2. La version 2012 n’apporte aucun changement de ce côté. Lors du déploiement vers un serveur, il est possible de spécifier R1 ou R2:

image

Au niveau des Dlls de Reporting Services installés dans le répertoire PrivateAssemblies de Visual Studio, le nom de l’assembly change entre les versions 2008 et la version 2012:

Ex. Microsoft.ReportingServices.ProcessingObjectModel.dll

BIDS Version

Assembly Version

File Version

2008 R1 10.0.0.0 10.0.1600.22
2008 R2 10.0.0.0 10.50.2500.0
2012 11.0.0.0 11.0.0.3360.12

Pour ceux qui sont familier avec le développement .NET, c’est la version de l’assembly qui compte lorsque l’on crée une référence vers un Dll. La version du fichier permet au développeur de corriger des problèmes sans forcer la recompilation de tous les programmes utilisant la librairie.

Description de la problématique

Ce qui (finalement) nous amène au sujet de ce blogue. Qu’est-ce qui arrive si l’on est dans la situation suivante:

  1. On utilise BIDS 2012;
  2. On déploie vers Reporting Services 2008 R1 ou R2;
  3. On utilise un Dll en support à nos rapports;
  4. Ce Dll réfère un Dll de Microsoft (ex. ProcessingObjectModel.dll) dans sa version 10 afin de fonctionner avec le serveur lors du déploiement.

Dans cette situation, le rapport déployé fonctionnera car le Dll du serveur aura le bon Assembly Name. Par contre, ni le débogage, ni la prévisualisation dans BIDS ne fonctionneront. L’erreur sera que la version 10.0.0.0 ne peut être trouvée. Effectivement, la version chargée par BIDS est 11.0.0.0.

Si vous changez la référence de votre Dll pour utiliser la version 11, vous aurez le problème inverse! Tout fonctionne localemenet mais pas une fois déployé! Sad smile

Solution: Binding Redirect

Ceux qui ont fait du ASP.Net sont peut-être habitué d’ajouter des Binding Redirect à leur Web.Config afin de rediriger les références à de vielles versions automatiquement à la toute dernière version disponible. Dans ce cas, c’est facile de savoir où l’ajouter (Web.Config de l’application Web).

Mais quand on exécute le rapport dans BIDS, à quel endroit doit-on ajouter le Binding Redirect? Vous avez répondu DevEnv.config? Ce n’est pas la bonne réponse!

Lorsque l’on utilise le Preview:

image

L’exécutable qui charge nos Dlls et leurs dépendances pour exécuter le rapport est PreviewProcessingService.exe:

image

Tandis que lorsqu’on veut déboguer un rapport:

image

Il s’agit plutôt de l’exécutable RSReportHost.exe

image

La solution au problème est donc d’ajouter ceci:

<dependentAssembly>
<assemblyIdentity name= »Microsoft.ReportingServices.ProcessingObjectModel » publicKeyToken= »89845dcd8080cc91″ culture= »neutral » />
<bindingRedirect oldVersion= »10.0.0.0″ newVersion= »11.0.0.0″ />
</dependentAssembly>

Aux fichiers:

  1. PreviewProcessingService.exe.configimage
  2. RSReportDesigner.configimage

Du répertoire PrivateAssemblies du Visual Studio dans lequel on utilise Report Designer.

Références

<bindingRedirect> Element
http://msdn.microsoft.com/fr-fr/library/eftw1fys(v=vs.110).aspx

Collection de sites en lecture seule: procédure et comportement


Contexte

Supposons que vous avez un programme qui extrait de l’information de plusieurs sites SharePoint. Lors de l’extraction, vous aimeriez empêcher les modifications pour la durée de l’extraction.

Procédure

Voici trois manières de procéder pour placer une collection de site en lecture seule.

Via le site d’administration :

Application Management –> Site Collection Quotos and Locks

image

https://community.dynamics.com/crm/b/marcellotonarelli/archive/2012/03/14/sharepoint-site-collection-lock-and-unlocking.aspx#.UYlRxbWsh8E

Via STSADM :

Mettre en lecture seule:
stsadm –o setsitelock –url http://<VotreCollectionDeSites&gt; –lock readonly

Remettre en lecture / écriture:
stsadm –o setsitelock –url http://<VotreCollectionDeSites&gt; –lock none

http://manish-sharepoint.blogspot.ca/2008/07/locking-sharepoint-site-for-readedit.html

Via l’API de SharePoint :

using (SPSite pCurSite = new SPSite(sSPSiteUrl))
{
   pCurSite.ReadOnly = bLock;
}

http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spsite.readonly.aspx

Comportement

C’est presqu’instantané de placer une collection de sites en lecture seule ou en lecture / écriture. Mais de quelle manière cela affecte-t-il vos utilisateurs?

L’interface utilisateur de SharePoint s’ajuste pour empêcher les nouvelles modifications

Par exemple, le menu contextuel d’un élément de liste devient:

clip_image002

Plutôt que cela:

clip_image002[4]

Et le menu contextuel Actions est plus limité:

image

Si un utilisateur avait commencé à modifier un élément, la sauvegarde va échouer

Si l’utilisateur avait déjà commencé la saisie:

image

Et que le site devient en lecture seule, la sauvegarde va échouer:

image

Mais si l’utilisateur attend que le site redevienne en lecture / écriture, revient en arrière (via le bouton Back), il va pouvoir appliquer ses changements.

Le mode feuille de données a un comportement différent

Qu’on utilise Afficher dans la feuille de données ou Modifier dans la feuille de données, le contrôle permet de changer le texte. Cependant, la barre de statut affichera un indicateur dans le premier cas (Read-Only en bas à droite):

image

Si le site devient en lecture seule pendant l’édition dans la feuille de données, la sauvegarde va échouer. Voici les différentes étapes:

1) On lance la modification en mode feuille de données
image

2) On modifie un élément, un petit crayon apparaît à gauche
image

3) On place le site en lecture seule

4) On complète l’édition de la ligne en sortant de la ligne
image

On voit qu’il y a des erreurs. On le voit sur la ligne (icône) et dans la barre de statut.

5) On clique sur Resolve et une boîte s’affiche.
image

On ne peut qu’annuler nos changements et ce même si le site est revenu en lecture / écriture depuis. SharePoint rafraîchit la barre de statut pour indiquer que la grille est en lecture seule.

image

Je n’ai pas trouvé de manière de conserver mes modifications. On peut que les copier pour les mettre dans Excel et les remettre plus tard, lorsque le site est revenu en lecture / écriture.

Si on “colle” un grand nombre de lignes en mode feuille …

… et que le site devient en lecture seule, aucune ligne ne sera sauvegardée et la seule chose à faire sera d’annuler toutes les modifications et recommencer plus tard. C’est quand même mieux que d’avoir à distinguer celles qui ont réussi de celles qui ont échouées.

Conclusion

Si votre collection de sites n’est pas volumineuse et que votre opération d’extraction est rapide, il peut être envisageable d’effectuer un lock avant l’opération.

Les tests ont été réalisés sur Windows SharePoint Services 3.0 mais je m’attend à ce que les résultats soient similaires sous SharePoint 2010/2013. Quelqu’un l’a essayé?

Comment exécuter du code dans un Assembly sans le charger dans le AppDomain principal


Description du problème

Supposons que votre programme utilise un ou des Dlls définis par vos utilisateurs. Ceux-vi vont s’attendre à être en mesure de recompiler leur(s) Dll(s) plusieurs fois pendant l’exécution de votre programme. Il y a de fortes chances que vous ne puissiez pas leur dire de redémarrer votre programme dès qu’ils modifient l’un de leurs Dlls.

Le problème, c’est qu’avec le .Net Framework, dès qu’un Assembly est chargé dans un AppDomain, il ne peut plus jamais être déchargé pour la vie du AppDomain et cela empêche de modifier le Dll sur le disque.

Un programme .Net a toujours un AppDomain principal et tous les Assemblies que vous chargez dans celui-ci ne seront pas déchargés tant et aussi longtemps que votre programme s’exécutera.

Description de la solution

Il faut donc créer un nouvel AppDomain temporaire, charger les Dlls de vos utilisateurs, les utiliser, puis le détruire.

Ce premier article va aborder un cas simple mais courant, où les utilisateurs doivent définir des classes implantant une interface. L’interface sera définie dans un Dll qui sera chargé à la fois par le client (votre programme) et les composantes de vos utilisateurs.

L’exemple que je vais utiliser a 4 composants:

1) L’exécutable principal (HostApplication.exe)

image

2) Le Dll contenant l’interface (Interfaces.dll)

3) Deux Dlls d’exemple (PluginA.dll, PluginB)

On peut voir un diagramme ici qui illustre l’EXE, les AppDomains, les Dlls et les classes.

image

On voit que Interfaces.dll sera chargé dans les deux AppDomains mais que PluginA.dll et PluginB.dll resteront dans le AppDomain temporaire. Cela aura pour effet de permettre la modification de ceux-ci sur le disque pendant que le programme s’exécute.

Détails sur ce qui se passe

Au démarrage de mon programme, on voit qu’il n’y a qu’un seul AppDomain appelé comme l’exécutable:

image

Il suffit de choisir le Dll et la classe à appeler puis cliquer sur Execute:

image

Le résultat sera affiché et un message apparaîtra afin d’empêcher l’AppDomain temporaire d’être détruit:

image

image

Si on rafraîchit Application Domain Viewer avant de fermer le message, on voit l’AppDomain temporaire qui a chargé PluginA.dll et Interfaces.dll:

image

Une fois qu’on ferme le message, l’AppDomain est détruit et on voit que Interfaces.dll est toujours chargé dans le AppDomain principal:

image

Aperçu du code source

En gros, j’ai défini une classe AppDomainProxy qui supporte IDisposable afin de pouvoir être utilisée dans une clause using.

Cette classe définit une méthode CreateInstance qui accepte un AssemblyName et un ClassName et qui retourne une référence de type ISampleInterface.

using (AppDomainProxy pAppDomainProxy = new AppDomainProxy())
{
  ISampleInterface pI = pAppDomainProxy.CreateInstance(cboAssemblyName.Text, cboClassName.Text);

  if (pI != null)
  {
    txtOutput.Text = pI.GetValue(txtParameter.Text);
    MessageBox.Show(« Now is the time to look both AppDomain before it gets deleted! »);
  }
  else
  {
    throw new ApplicationException(« Could not load assembly  » + cboAssemblyName.Text);
  }
}

Dans cette exemple, l’interface est toute simple:

public interface ISampleInterface
{
  string GetValue(string sParam);
}

Les classes des utilisateurs doivent implanter l’interface ISampleInterface et hériter de MarshallByRef:

public class ClassA : MarshalByRefObject, ISampleInterface
{
  public string GetValue(string sParam)
  {
    return String.Format(« {0} from ClassA », sParam);
  }
}

La création d’un AppDomain se fait ainsi (à l’interne de la classe AppDomainProxy):

mpAppDomain = AppDomain.CreateDomain(Guid.NewGuid().ToString(« B »));

Et sa destruction ainsi (dans Dispose):

AppDomain.Unload(mpAppDomain);

J’ai mis l’exemple au complet dans GitHub ici (VS.Net 2008).

AppDomain 102

Ma prochaine entrée de blogue vous montrera comment faire de la .Net Reflection sur des classes chargés dans un AppDomain temporaire, sans les charger dans le AppDomain principal.

C’est pratique s’il vous faut explorer dynamiquement des Assembly sans empêcher les utilisateurs de les modifier sur le disque pendant l’exécution de votre programme.

Références

AppDomainProxy (GitHub)
https://github.com/samsonfr/AppDomainProxy

AppDomain Viewer
http://labs.workshare.com/2010/08/app-domain-viewer.html#comment-form

Code C# pour convertir un rapport SSRS 2008 R2 en format SSRS 2008 R1

Cette article est le troisième de la série qui explique le comportement de BIDS R2 lors du déploiement lorsque la cible est Reporting Services 2008 R1.

J’ai développé une classe C# ainsi qu’un petit programme de test qui permet de prendre un rapport SSRS R2 et le convertir en format SSRS R1:

image

Évidemment, si vous utilisez des fonctionnalités propres à R2 il peut se produire différentes choses :

Cela peut échouer si le rapport contient des

  1. Nouveaux types de sources de données (SharePointList, Azure ou SqlAzure et SQLPDW
  2. Shared Datasets
  3. Variables en lecture/écriture

Des éléments peuvent être enlevés

  1. Cartes
  2. Indicateurs
  3. Options de bris de page (Disabled et ResetPageNumber)

Des éléments peuvent être modifiés

  1. Globals!RenderFormat.Name -> « RPL »
  2. Globals!RenderFormat.IsInteractive -> True
  3. Globals!OverallPageNumber -> Globals!PageNumber
  4. Globals!OverallTotalPages -> Globals!TotalPages
  5. Globals!PageName –> «  »
  6. Rotate270 –> Vertical (pour WritingMode)

Le déploiement peut échouer lors de l’appel aux services Web

  1. Utilisation des fonctions Lookup, LookupSet ou Multilookup
  2. Utilisation d’aggrégation d’aggrégation

Code Source

Vous pouvez obtenir le code source sur GitHub à cet endroit: https://github.com/samsonfr/BIDS2008R2Downgrade

Il y a aussi un projet de rapports SSRS contenant des éléments SSRS R2 afin de vous permettre de tester le comportement à différents niveaux d’erreur (ErrorLevel).

image

Détail sur le code source

Tout d’abord, une énumération permet de reproduire les niveaux d’erreurs de BIDS et d’ajouter un cas où on laisse le déploiement échouer:

public enum ErrorLevel
{
FatalError = 0,
  LayoutDrasticError = 1,
LayoutSignificantError = 2,
LayoutMinorError = 3,
Warning = 4,
LetDeployFail = 5
}

Puis, une classe permet d’enregistrer nos préférences sur ce qui devrait générer une erreur ou un avertissement:

public class DesiredSeverity …

Il faut créer une instance de la classe RdlDowngradeEngine en spécifiant le niveau d’erreur de la configuration et optionnellement l’instance de DesiredSeverity.

public RdlDowngradeEngine(int nBIDSErrorLevel)

public RdlDowngradeEngine(DesiredSeverity pDesiredSeverity, int nBIDSErrorLevel)

Puis il faut appeler la méthode DowngradeReport en lui passant le Xml du rapport:

public string DowngradeReport(string sReportXml, out List<R2Element> pListElementsOut)

La valeur de retour est le Xml du rapport en version 2008 R1. Aussi, tous les éléments problématiques qui ont été enlevés ou modifiés (ou qui ont causé l’échect) sont retournés dans une liste de R2Elements.

public class R2Element
{
public ErrorLevel Severity { get; set; }
public string ElementName { get; set; }
public string ElementType { get; set; }
public string ElementWithNameType { get; set; }
public string ElementXPath { get; set; }
public string LocalizedWarning { get; set; }
public string LocalizedError { get; set; }
}

La sévérité de l’erreur est présente et l’instance contient toujours le message d’erreur ET d’avertissement, ainsi l’application client peut décider d’afficher ce qu’elle veut.

Si le noeud problématique n’a pas de nom (ex. WritingMode), c’est le nom du TextBox contenant celui-ci qui sera retourné et ElementWithNameType contiendra “TextBox”.

En passant, BIDS R2 crée tous les nouveaux rapports en format SSRS R1. C’est uniquement lorsque l’on ajoute un contrôle spécifique à R2 qu’il effectue la conversion vers R2. Cependant, il ne se rend pas compte de l’utilisation des Globals, lookups ou aggrégation d’aggrégation et c’est pourquoi mon code détecte la version source mais traite quand même les Globals quand la version est R1.

Pour le reste, je travaille avec un XmlDocument, ayant le namespace 2008/01 (R1), avec du XPath. J’ai réduit le code à des appels à:

HandleElements(« dns:Body/dns:ReportItems//dns:Map », mpDesiredSeverity.SeverityForMap, RemoveNodeFromParent, null, Localization.ErrorMap, Localization.WarningMap);

Cette méthode accepte une action à effectuer lorsque le XPath trouve un élément. Il peut s’agir d’enlever le noeud, changer sa valeur ou faire un Find&Replace dans la valeur:

private static void RemoveNodeFromParent(XmlNode pNode, string[] asUnused)
{
if (pNode != null && pNode.ParentNode != null)
{
pNode.ParentNode.RemoveChild(pNode);
}
}

private static void ChangeNodeValue(XmlNode pNode, string[] asNewValue)
{
if (pNode != null)
{
pNode.InnerText = asNewValue != null && asNewValue.Length == 1 ? asNewValue[0] : String.Empty;
}
}

private static void ReplaceNodeValue(XmlNode pNode, string[] asNewValue)
{
if (pNode != null && asNewValue != null && asNewValue.Length == 2)
{
pNode.InnerText = Strings.Replace(pNode.InnerText, asNewValue[0], asNewValue[1], 1, -1, CompareMethod.Text);
}
}

Si vous avez des questions ou des suggestions, n’hésitez pas à laisser un commentaire.

Comment impersonifier temporairement l’identité du AppPool en ASP.Net

Il est fréquent d’impersonifier l’identité du l’utilisateur dans une application ASP.Net MVC. L’application est personnalisée ou sécuritée par utilisateur ce qui nous empêche d’impersonifier un compte de service précis.

Normalement, avec une connaissance de Kerberos, il est facile de faire en sorte que l’identité de l’utilisateur voyage jusqu’aux différents serveurs de données (SQL Server, Analysis Services etc.).

Mais dans certains cas, la délégation Kerberos n’est pas envisageable. C’est en autre le cas pour l’API qui se trouve dans System.DirectoryServices. Cet API permet d’interroger Active Directory afin d’obtenir les groupes dont un utilisateur fait partie ou d’aller chercher des propriétés etc.

Même si cet API est en .NET, ça semble utiliser des fonctonnalités COM et il semblerait que la délégation Kerberos n’est pas supportée dans ce scénario. En effet, lors d’un Double Hop, l’erreur suivante est obtenue lors de l’utilisation des classes de System.DirectoryServices:

[COMException (0x80072020): An operations error occurred. ]

Le KB329986propose des solutions mais aucune ne nous semblait acceptable pour notre besoin (ex. utiliser l’authentification basique).

Heureusement, nos applications ASP.Net MVC s’exécutent sous un AppPool ayant une identité d’un compte de domaine (le même que pour SharePoint lorsque l’application MVC est hébergée sous SharePoint). Il est possible d’exécuter un bloc de code sous l’identité du compte de service du AppPool en utilisant:

using (HostingEnvironment.Impersonate()) {

// Ce code s’exécute avec l’identité
// assignée au pool d’application (AppPool)

DirectorySearcher searcher …
}

Il faut ajouter une référence àSystem.Web.dll et faire un usingSystem.Web.Hosting.

Cette technique a l’avantage de contrôler les lignes de code devant s’exécuter sous le compte du AppPool. Tout le reste peut continuer de s’exécuter sous l’identité de l’utilisateur du fureteur.

Dans le cas précis de l’accès à Active Directory, cette technique offre un autre avantage. Plutôt que d’avoir à donner accès en lecture à Active Directory à tous vos utilisateurs, vous pouvez accorder ce droit uniquement au compte de domaine assigné au AppPool.

Références

How to use the System.DirectoryServices namespace in ASP.NET

Comment refaire une interface visuelle par dessus Reporting Services en utilisant les services Web

Par défaut, Reporting Services offre deux modes de fonctionnement:

  1. Natif où il utilise HTTP.SYS comme serveur Web et fournit son propre portail;
  2. Intégré à SharePoint où il utilise IIS comme serveur Web et SharePoint comme portail.

Visuellement, la plus grande différence est au niveau de l’apparence et la disposition des paramètres.

En mode natif, les paramètres s’affichent en haut de la barre d’outils, deux paramètres par ligne:

image

En mode intégré, ils s’affichent à droite de la page, un en dessous de l’autre.

image

Pour le reste, le rapport en soi ne change pas.

Que faire alors si vous désirez changer dramatiquement l’apparence générale (barre d’outils, paramètres) et quand même profiter de de Reporting Services afin de générer le rapport?

Peut-être que vous voulez développer une interface Web mieux adapté à l’apparence générale de votre application. Ou peut-être voulez-vous développer une application mobile sous iOS, Android etc.

Idéalement, pour un rapport donné, il serait possible d’obtenir la liste des paramètres avec leur définition et la dépendance entre eux. À partir de celle-ci, il serait possible de construire une interface graphique spécifique permettant à l’utilisateur de choisir des valeurs. Grâce à la définition des dépendances, il serait possible pour l’application de rafraîchir les listes de choix dépendantes lors d’un changement.

Et bien il se trouve qu’en utilisant le service Web ReportService2006.ReportingService2006 il est possible d’obtenir la définition complète des paramètres via la méthode GetReportParameters.

Ensuite, il est possible de lancer l’exécution d’un rapport SSRS via ReportExecution2005.ReportExecutionService ou tout simplement en construisant une requête HTTP POST vers le Report Server.

Si vous êtres dans un environnement C# (ex. ASP.Net MVC), il est très facile de générer un proxy vers les services Web et d’utiliser les classes ReportParameter, ValidValue directement dans l’application “client” du service Web.

Si par contre vous travaillez en Objective C sous iOS, il serait peut-être plus simple de retourner la définition complète des paramètres en XML. Dans ce cas, il suffit de vous créer (du côté serveur), un point d’entrée vous permettant de faire appel à GetReportParameters et à sérialiser le résultat en XML:


  public string GetReportParametersXml(string sReportUrl, string sHistoryID, ReportService2006.ParameterValue[] apValues,
                                         ReportService2006.DataSourceCredentials[] apDSCredentials)
    {
      ReportService2006.ReportParameter[] apParams = this.GetReportParameters(sReportUrl, sHistoryID, apValues, apDSCredentials);
      XmlSerializerNamespaces pNS = new XmlSerializerNamespaces();
      pNS.Add("", "");
      string sResult = SerializeObject(apParams, Encoding.GetEncoding("ISO-8859-1"), pNS, false);
      sResult = sResult.Replace(" xmlns=\"http://schemas.microsoft.com/sqlserver/2006/03/15/reporting/reportingservices\"", "");
      return sResult.Replace("", "");
    }
  private static string SerializeObject(object pObjectToSerialize, Encoding pEncoding, XmlSerializerNamespaces pNS, bool bOmitXmlDeclaration)
    {
      XmlSerializer pSerializer = new XmlSerializer(pObjectToSerialize.GetType());
      MemoryStream pMS = new MemoryStream();
      XmlWriterSettings pSettings = new XmlWriterSettings();
      pSettings.Indent = true;
      pSettings.OmitXmlDeclaration = bOmitXmlDeclaration;
      pSettings.Encoding = pEncoding;
      XmlWriter pWriter = XmlWriter.Create(pMS, pSettings);
      pSerializer.Serialize(pWriter, pObjectToSerialize, pNS);
      return pEncoding.GetString(pMS.ToArray());
    }

Cela vous donne toute l’information nécessaire pour construire dynamiquement votre interface de saisie de paramètres. Par la suite, vous devez donner un moyen à l’utilisateur de soumettre sa sélection afin de rafraîchir le rapport avec les nouvelles valeurs.

Lors du changement de la valeur d’un paramètre, il se peut que cela affecte les valeurs acceptées pour un autre paramètres (ex. Catégorie, Sous-Catégorie, Produit). Il vous faudra donc détecter les changements de valeurs, vérifier si un ou plusieurs paramètres sont dépendants et les rafraîchir au besoin. Il suffit de rappeler GetReportParameters en fournissant des valeurs dans le paramètre ParameterValue[] values et d’aller chercher les nouvelles valeurs valides (<ValidValues><ValidValue> …) pour ces paramètres dépendants.

Il reste un seul problème: Reporting Services ne peut pas faire de magie pour identifier les dépendances entre les paramètres. Comme il ne nous permet pas d’exprimer explicitement ces dépendances, il tente de les déduire. Si vous utilisez des expressions complexes (concaténation ou appel de fonction) pour obtenir la requête, il ne peut pas l’interpréter et place tous les paramètres précédents comme dépendance ce qui est faux 99% du temps.

Une piste de solution pour alléger le problème serait d’utiliser les paramètres avancés des paramètres et d’employer “Never Refresh” sur ceux qui ne dépendent pas d’autres paramètres que l’utilisateur peut changer.

clip_image001

Dans le XML, la valeur de cette option prendra cette forme: <UsedInQuery>False</UsedInQuery>. Si le noeud est absent, c’est Auto et “Always Refresh” équivaut à <UsedInQuery>False</UsedInQuery>.

Lorsqu’on construit la liste des paramètres à rafraîchir suite à un changement de valeur, il faut exclure ceux qui sont marqués avec « Never Refresh » même si le paramètre qui a changé est dans leur liste de dépendances.

Vous voilà mieux informé sur ce qui est possible de faire avec les services Web de Reporting Services dans le contexte d’une application souhaitant utiliser sa propre mécanique pour la saisie de paramètres.

N’hésitez pas à commenter,