Introduction

L’objectif de cet article est tout d’abord de démontrer comment monitorer les appels à un service WCF (en enregistrant tous les appels ainsi que leurs temps d’exécution et les utilisateurs). Par ailleurs nous verrons que cela peut s’appliquer également pour fournir une gestion de la sécurité sans dupliquer le code ou appeler des méthodes explicitement.
WCF offre la possibilité d’ajouter des comportements sur un service ou sur des méthodes d’un service. Nous allons donc créer nos propres comportements (le premier pour logguer, le second pour la sécurité), et les appliquer sur les méthodes de notre choix.

WCF Attributes Sources Download    (Nécessite le framework .net 4.5 et Visual Studio 2013).
Exécuter Visual Studio en mode administrateur pour démarrer le service WCF (ou autorisez le port)

Création du service

Pour notre exemple nous allons définir un contrat d’interface « IMyCompanyService » composé de 2 méthodes tel que :

[ServiceContract]
public interface IMyCompanyService
{
    /// <summary>
    /// Provides User object according to the username/password
    /// </summary>
    /// <param name="UserName"></param>
    /// <param name="Password"></param>
    /// <returns>User object</returns>
    [OperationContract]
    User Login(string UserName, string Password);
 
 
    /// <summary>
    /// Returns a simple string
    /// </summary>
    /// <param name="value"></param>
    /// <param name="currentUserId">Logged user</param>
    /// <returns>String which contains the input parameter named value</returns>
    [OperationContract]
    [FaultContract(typeof(string))]
    string GetData(int value, Guid currentUserId);
}

Logique d’utilisation du service

wcf_attributes_1

Le client fait un premier appel à la méthode « Login », si l’authentification est correcte, il reçoit un objet de type « User » contenant son identifiant unique (GUID).

Une fois cet ID obtenu, il l’utilise pour tous les appels à la méthode « GetData »

Implémentation du service

  1. Nous utilisons et configurons log4net pour afficher les logs à l’aide du ConsoleColoredAppender.
  2. Nous définissons un business service « UserService » qui contient l’accès aux données et la logique.
  3. Bien que non indispensable, le framework Unity est utilisé pour l’injection de dépendance entre l’interface du business service et son implémentation
  4. L’implémentation de la méthode « GetData » proposée par le service WCF contient l’instruction

« Thread.Sleep(waitingTimeInMs); » pour simuler des temps d’exécution aléatoires.

 

Hébergement du service

Le projet Console « MyCompany.ServiceLauncher » héberge notre service WCF, cela permet notamment de bénéficier de la console et de voir ainsi l’output de log4net directement.

 

Création du client

Nous créons une application console cliente (MyCompany.Client) qui va appeler le webservice. Afin de simuler des appels autorisés et non-autorisés. Le client se termine quand l’utilisateur appuie sur « ESPACE » ou « ENTER »

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Type {ENTER} or {ESCAPE} to exit.");
        try
        {
            using (MyCompanyServiceReference.MyCompanyServiceClient client = new MyCompanyServiceReference.MyCompanyServiceClient())
            {
                var userId = client.Login("ohelin", "test").Id;
                var wrongUserId = Guid.Empty;
 
                //Performs requests
                do
                {
                    while (!Console.KeyAvailable)
                    {
                        //Call with valid user
                        try
                        {
                            Random rnd1 = new Random();
                            var result = client.GetData(rnd1.Next(100), userId);
                            Console.WriteLine(result);
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine(ex.Message);
                        }
 
                        //Call with invalid user
                        try
                        {
                            Random rnd1 = new Random();
                            var result = client.GetData(rnd1.Next(100), wrongUserId);
                            Console.WriteLine(result);
                        }
                        catch(Exception ex)
                        {
                            Console.WriteLine(ex.Message);
                        }
 
                        Thread.Sleep(100);
                    }
                }
                while (Console.ReadKey(true).Key != ConsoleKey.Escape &amp;&amp; Console.ReadKey(true).Key != ConsoleKey.Enter);
            }
        }
        catch(Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
    }
}

Définition et utilisation de l’attribut « LoggerBehavior »

Jusqu’à présent nous avons notre service WCF et son client. Il reste à gérer l’enregistrement des appels à travers la création d’un attribut personnalisé que nous nommerons «LoggerBehavior ».

Il existe 4 types de « behavior » qu’il est possible d’implémenter à travers les interfaces suivantes : IServiceBehavior, IEndpointBehavior, IContractBehavior, IOperationBehavior.

Nous pouvons représenter leur champ d’application de la manière suivante :

WCF Behavior types

 

A titre d’exemple, il est donc possible de créer un service behavior qui va automatiquement ajouter le « LoggerBehavior » sur toutes les méthodes d’un contrat, d’un endpoint ou même du service entier.

1. Définition du LoggerBehavior :

public class LoggerBehavior : Attribute, IOperationBehavior
{
    public void AddBindingParameters(OperationDescription operationDescription, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
    {
    }
 
    public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
    {
    }
 
    public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
    {
        //Get all methods parameters names
        string[] operationParameters = operationDescription.Messages[0].Body.Parts.OrderBy(m =&gt; m.Index).Select(m =&gt; m.Name).ToArray();
        dispatchOperation.ParameterInspectors.Add(new LoggerOperationInspector(operationParameters));
    }
 
    public void Validate(OperationDescription operationDescription)
    {
    }
}

Nous utilisons la méthode ApplyDispatchBehavior pour enregistrer notre OperationInspector qui va traiter la requête en fonction des paramètres passés pendant l’appel.

 

 2. Création du « ParameterInspector » (qui contient la logique du logging) :

public class LoggerOperationInspector : IParameterInspector
{
    /// <summary>
    /// Business logger
    /// </summary>
    private static readonly ILog BusinessLogger = LogManager.GetLogger(typeof(MyCompanyService));
 
    /// <summary>
    /// Parameter's names
    /// </summary>
    public string[] _operationParameters { get; set; }
 
    /// <summary>
    /// Prameter's values
    /// </summary>
    public object[] _inputParameters { get; set; }
 
    /// <summary>
    /// Business service to access data
    /// </summary>
    public IUserService _userService { get; set; }
 
    /// <summary>
    /// Stopwatch to get execution times
    /// </summary>
    public Stopwatch _stopwatch { get; set; }
 
 
    /// <summary>
    /// Initialization of the Inspector
    /// </summary>
    /// <param name="operationParameters"></param>
    public LoggerOperationInspector(string[] operationParameters)
    {
        _stopwatch = new Stopwatch();
 
        //Service to get all users
        _userService = MyCompanyService.Container.Resolve<IUserService>();
 
        //Keep parameters names
        _operationParameters = operationParameters;
    }
 
 
    /// <summary>
    /// Keep input values then start the stopwatch
    /// </summary>
    /// <param name="operationName"></param>
    /// <param name="inputs"></param>
    /// <returns></returns>
    public object BeforeCall(string operationName, object[] inputs)
    {
        _inputParameters = inputs;
        _stopwatch.Restart();
        return null;
    }
 
 
    /// <summary>
    /// Log methodname, user, execution time
    /// </summary>
    /// <param name="operationName"></param>
    /// <param name="outputs"></param>
    /// <param name="returnValue"></param>
    /// <param name="correlationState"></param>
    public void AfterCall(string operationName, object[] outputs, object returnValue, object correlationState)
    {
        _stopwatch.Stop();
 
        try
        {
            //Get currentuser
            int position = Array.IndexOf(_operationParameters, "currentUserId");
            if (position == -1 || !(_inputParameters[position] is Guid))
            {
                log4net.ThreadContext.Stacks["Method"].Push(operationName);
                log4net.ThreadContext.Stacks["ExecutionTime"].Push(_stopwatch.ElapsedMilliseconds.ToString());
 
                BusinessLogger.DebugFormat("Operation \"{0}\" was executed in {1} ms", operationName, _stopwatch.ElapsedMilliseconds);
 
                log4net.ThreadContext.Stacks["Method"].Pop();
                log4net.ThreadContext.Stacks["ExecutionTime"].Pop();
            }
            else
            {
                Guid userId = (Guid)_inputParameters[position];
                var CurrentUser = _userService.GetUserNameById(userId);
 
                log4net.ThreadContext.Stacks["Method"].Push(operationName);
                log4net.ThreadContext.Stacks["ExecutionTime"].Push(_stopwatch.ElapsedMilliseconds.ToString());
                log4net.ThreadContext.Stacks["User"].Push(userId.ToString());
 
                BusinessLogger.DebugFormat("Operation \"{0}\" was executed by the user {1} in {2} ms", operationName, CurrentUser, _stopwatch.ElapsedMilliseconds);
 
                log4net.ThreadContext.Stacks["Method"].Pop();
                log4net.ThreadContext.Stacks["ExecutionTime"].Pop();
                log4net.ThreadContext.Stacks["User"].Pop();
            }
        }
        catch (Exception ex)
        {
            BusinessLogger.Error("Unable to log the time spent in the execution of the method \"" + operationName + "\"", ex);
        }
    }
}

 

La méthode BeforeCall est appelée avant l’exécution de la méthode et nous permet de démarrer notre chronomètre et de garder les paramètres d’entrée en mémoire.

De la même manière, la méthode AfterCall est appelée une fois la méthode exécutée, on peut donc récupérer son temps d’exécution, et en déduire l’utilisateur qui a fait l’appel à travers le paramètre « currentUserId » s’il existe dans la signature.

L’utilisation des ThreadContext de log4net nous permet simplement de déclarer des champs personnalisés supplémentaires pour ne pas à avoir à parser les logs. Cela est particulièrement utile si l’on souhaite créer des statistiques, ou des représentations graphiques pour suivre l’évolution de l’état de santé du service dans le temps.

3. Utilisation de notre attribut

Dans l’interface du service WCF « IMyCompanyService » nous ajoutons simplement l’attribut LoggerBehavior.

/// <summary>
/// Returns a simple string
/// </summary>
/// <param name="value"></param>
/// <param name="currentUserId">Logged user</param>
/// <returns>String which contains the input parameter named value</returns>
[OperationContract]
[LoggerBehavior]
[FaultContract(typeof(string))]
string GetData(int value, Guid currentUserId);

4. Test du service avec le client

On démarre le service puis on lance le client de test pour qu’il exécute un appel sur la méthode « Login » puis la méthode «GetData ».

Côté service, on peut voir le résultat dans la console d’exécution :

wcf_attributes_3

 

Définition et utilisation de l’attribut « SecurityBehavior »

A l’instar du LoggerBehavior , il est possible de créer un attribut qui se chargera de valider ou non l’accès à une méthode.

1. SecurityBehavior

public class SecurityBehavior : Attribute, IOperationBehavior
{
    public void AddBindingParameters(OperationDescription operationDescription, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
    {
    }
 
    public void ApplyClientBehavior(OperationDescription operationDescription, ClientOperation clientOperation)
    {
    }
 
    public void ApplyDispatchBehavior(OperationDescription operationDescription, DispatchOperation dispatchOperation)
    {
        //Get all methods parameters names
        string[] operationParameters = operationDescription.Messages[0].Body.Parts.OrderBy(m =&gt; m.Index).Select(m =&gt; m.Name).ToArray();
        dispatchOperation.ParameterInspectors.Add(new SecurityOperationInspector(operationParameters));
    }
 
    public void Validate(OperationDescription operationDescription)
    {
    }
}

2. SecurityOperationInspector

public class SecurityOperationInspector : IParameterInspector
{
    /// <summary>
    /// Business logger
    /// </summary>
    private static readonly ILog BusinessLogger = LogManager.GetLogger(typeof(MyCompanyService));
 
    /// <summary>
    /// Parameter's names
    /// </summary>
    public string[] _operationParameters { get; set; }
 
    /// <summary>
    /// Prameter's values
    /// </summary>
    public object[] _inputParameters { get; set; }
 
    /// <summary>
    /// Business service to access data
    /// </summary>
    public IUserService _userService { get; set; }
 
 
    /// <summary>
    /// Initialization of the Inspector
    /// </summary>
    /// <param name="operationParameters"></param>
    public SecurityOperationInspector(string[] operationParameters)
    {
        IUnityContainer container = MyCompanyService.Container;
        _userService = container.Resolve<IUserService>();
 
        //Keep parameters names
        _operationParameters = operationParameters;
    }
 
    /// <summary>
    /// Retrieve the current user and check rights.
    /// Throw a FaultException if access denied
    /// </summary>
    /// <param name="operationName"></param>
    /// <param name="inputs"></param>
    /// <returns></returns>
    public object BeforeCall(string operationName, object[] inputs)
    {
        _inputParameters = inputs;
        int position = Array.IndexOf(_operationParameters, "currentUserId");
 
        if (position > -1 && _inputParameters[position] is Guid)
        {
            Guid userId = (Guid)_inputParameters[position];
            var CurrentUser = _userService.GetUserNameById(userId);
 
            bool isValid = _userService.IsValidUser(userId);
 
            if (isValid == false)
            {
                log4net.ThreadContext.Stacks["Method"].Push(operationName);
                log4net.ThreadContext.Stacks["User"].Push(userId.ToString());
 
                BusinessLogger.ErrorFormat("Access Denied for the operation \"{0}\" requested by the user {1} - {2}", operationName, CurrentUser, userId);
 
                log4net.ThreadContext.Stacks["Method"].Pop();
                log4net.ThreadContext.Stacks["User"].Pop();
 
                //Throw accessdenied exception
                throw new FaultException(string.Format("Access Denied for the operation \"{0}\" requested by the user {1}", operationName, CurrentUser));
            }
        }
 
        return null;
    }
 
    /// <summary>
    /// No nothing, security check was already processed
    /// </summary>
    /// <param name="operationName"></param>
    /// <param name="outputs"></param>
    /// <param name="returnValue"></param>
    /// <param name="correlationState"></param>
    public void AfterCall(string operationName, object[] outputs, object returnValue, object correlationState)
    {
    }
}

 

Nous déclenchons une FaultException si l’accès est refuse et le corps de la méthode ne sera pas exécutée.

4. Test du service avec le client

On démarre le service puis on lance le client de test pour qu’il exécute un appel sur la méthode « Login » puis la méthode «GetData » une fois avec un identifiant correct l’autre avec un identifiant invalide.

Côté service, on peut voir le résultat dans la console d’exécution :

WCF Attributes Result

Conclusion

Les applications des attributs dans WCF sont nombreuses et permettent d’éviter la duplication de code et d’ajouter des comportements de manière élégante dans le code.

 

Last modified: 4 February 2015

Author

Comments

Write a Reply or Comment

Your email address will not be published.