WCF

De Banane Atomic
Version datée du 7 novembre 2017 à 14:49 par Nicolas (discussion | contributions) (→‎WCF et SSL)
(diff) ← Version précédente | Voir la version actuelle (diff) | Version suivante → (diff)
Aller à la navigationAller à la recherche

WCF

WCF permet l'échange de données (lecture et écriture) au travers d'un réseau ou d'un pipe.
WCF introduit le moteur de sérialisation DataContract.
Bon choix pour les communications non-HTTP (MSMQ, Named Pipes) et SOAP.
Pour les services web basés sur HTTP, préférer ASP.NET Core ou ASP.NET Web API 2.x.

Historique
WCF .NET & VS
4.5 4.5 - VS 2012
4.0 4.0 - VS 2010
3.5 3.5 - VS 2008
3.0 3.0 - VS 2005

Concept

Chaque service expose son contrat via un ou plusieurs endpoints.
Un client WCF se connecte à un service WCF via un endpoint.

endpoint
adresse URL
binding définit le mécanisme de transport des messages
  • protocole de communication
  • encodage (texte, binaire)
  • transport (TCP, HTTP)
contract

Comparatif des bindings

Binding Protocoles de transport Encodage Commentaire
BasicHttpBinding SOAP 1.1 HTTP(S) Texte Basique
WSHttpBinding SOAP 1.2 HTTP(S) Texte Complet: fonctionnalités WS-*
WsDualHttpBinding SOAP 1.2 HTTP(S) UTF-8 Communication bi-directionnelle, les clients et services peuvent envoyer et recevoir des messages
NetTcpBinding SOAP 1.2 TCP Binaire Communication sur un intranet. Service et client WCF. Plus rapide et fiable.
NetNamedPipeBinding SOAP 1.2 IPC Binaire Communication entre 2 processus d'une même machine
NetMsmqBinding SOAP 1.2 MSMQ Binaire Communication déconnectée
WebHttpBinding REST HTTP(S) Texte

BindingChoiceInverted.svg

Types d'hébergement

Type d'hébergement Description
Self-Hosting in a Managed Application dans une applications console, WPF
Managed Windows Services dans un service Windows
IIS HTTP transport seulement
Windows Process Activation Service (WAS) utilisé par IIS 7.0

Liens

Serveur

Assembly System.ServiceModel.dll
IHelloService.cs
// Création d'une interface de contrat
[ServiceContract]
public interface IHelloService
{
    [OperationContract]
    string Hello(Name name);
}
HelloService.cs
// Implémentation du service
public class HelloService : IHelloService
{
    public string Hello(Name name)
    {
        return $"Hello {name.First} {name.Last}";
    }
}
Name.cs
[DataContract]
public class Name
{
    [DataMember]
    public string First { get; set; }

    [DataMember]
    public string First { get; set; }
}
App.conf
<configuration>
  <system.serviceModel>
    <!-- Liste des services -->
    <services>
      <service name="SoapDataService.HelloService">

        <!-- Liste des endpoints -->
        <endpoint address="net.tcp://localhost:8080/HelloService/"
                  binding="netTcpBinding"
                  contract="Contracts.IHelloService" />

        <endpoint address="http://localhost/HelloService/"
                  binding="basicHttpBinding"
                  contract="Contracts.IHelloService" />

        <endpoint address="net.pipe://localhost/HelloService/"
                  binding="netNamedPipeBinding"
                  contract="Contracts.IHelloService" />

Web Host - IIS - Fichier *.svc

  • VS → New Projet → WCF → WCF Service Application
  • Ajouter un service: Add → New Item → WCF Service
  • Project Properties → Web → Servers → Project Url : http://localhost:port
HelloService.svc
<%@ ServiceHost Language="C#" Debug="true" Service="Namespace.HelloService" CodeBehind="HelloService.svc.cs" %>
Web.config
<system.serviceModel>
  <behaviors>
    <serviceBehaviors>
      <behavior>
        <!-- To avoid disclosing metadata information, set the values below to false before deployment -->
        <serviceMetadata httpGetEnabled="true" httpsGetEnabled="true"/>
        <!-- To receive exception details in faults for debugging purposes, set the value below to true.
             Set to false before deployment to avoid disclosing exception information -->
        <serviceDebug includeExceptionDetailInFaults="false"/>
      </behavior>
    </serviceBehaviors>
  </behaviors>
    
  <!-- définit les binding par défaut en fonction de scheme -->
  <protocolMapping>
      <add scheme="http" binding="wsHttpBinding" />
      <add scheme="https" binding="wsHttpBinding" />
  </protocolMapping> 
    
  <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />

  <!-- WCF configure lui-même les services à partir des fichiers *.svc
       il n'est donc pas nécessaire d'ajouter le code suivant -->
  <services>
    <service name="Namespace.HelloService">
      <endpoint address=""
                binding="wsHttpBinding"
                contract="Namespace.IHelloService" />
    </service>
  </services>
</system.serviceModel>

Web Host - IIS

Project Properties → Web → Servers → Project Url : http://localhost:port

Web.config
  <system.serviceModel>
    <services>
      <service name="Service.HelloService">
        <!-- adresse est vide ici car elle est définie dans le projet -->
        <endpoint address=""
                  binding="wsHttpBinding"
                  contract="Contracts.IHelloService" />
      </service>
    </services>
    <!-- fichier svc virtuel -->
    <serviceHostingEnvironment>
      <serviceActivations>
        <add service="Service.HelloService" relativeAddress="HelloService.svc"/>
      </serviceActivations>
    </serviceHostingEnvironment>

Lancement du service par le code - Self Host

Cs.svg
// assembly System.ServiceModel
ServiceHost host = new ServiceHost(typeof(HelloService));
try
{
    // Open the service host to accept incoming calls  
    host.Open();

    // The service can now be accessed.  
    Console.WriteLine("The service is ready.");
    Console.WriteLine("Press <ENTER> to terminate service.");
    Console.WriteLine();
    Console.ReadLine();

    // Close the ServiceHostBase to shutdown the service.  
    host.Close();
}
catch (CommunicationException commProblem)
{
    Console.WriteLine("There was a communication problem. " + commProblem.Message);
    Console.Read();
}

Client

Le client doit connaître l'interface. Ceci peut être fait par une copie du fichier contenant l'interface dans le projet client ou bien par référence de service.

Génération automatique du proxy

  1. Lancer le service
  2. Dans le projet client → clique-droit sur References → Add Service Reference → Coller l'adresse http://domain:port/service.svc
  3. Changer le namespace: HelloServiceReference
Cs.svg
// BasicHttpBinding_IHelloService: nom du endpoint du client, à récupérer dans le fichier App.conf du projet client
var proxy = new HelloServiceClient("BasicHttpBinding_IHelloService");

proxy.Endpoint.Address;   // http://localhost:53129/HelloService.svc
proxy.Endpoint.Binding;   // System.ServiceModel.BasicHttpBinding
proxy.Endpoint.Contract;  // System.ServiceModel.Description.ContractDescription

Console.WriteLine(proxy.Hello("everybody !!!"));
App.config
<!-- code généré par l'ajout de la référence à HelloService -->
<system.serviceModel>
    <bindings>
        <basicHttpBinding>
            <binding name="BasicHttpBinding_IHelloService" />
        </basicHttpBinding>
    </bindings>
    <client>
        <endpoint address="http://localhost:56307/HelloService.svc" 
                  binding="basicHttpBinding"
                  bindingConfiguration="BasicHttpBinding_IHelloService"
                  contract="HelloServiceReference.IHelloService"
                  name="BasicHttpBinding_IHelloService" />
    </client>
</system.serviceModel>

Génération manuelle du proxy

Hardcodé

Csharp.svg
IHelloService proxy = ChannelFactory<IHelloService>.CreateChannel(
    new BasicHttpBinding(),
    new EndpointAddress("http://localhost:8733/Design_Time_Addresses/SoapDataService/HelloService/"));

IHelloService proxy = ChannelFactory<IHelloService>.CreateChannel(
    new NetTcpBinding(),
    new EndpointAddress("net.tcp://localhost/HelloService"));

IHelloService proxy = ChannelFactory<IHelloService>.CreateChannel(
    new NetNamedPipeBinding(),
    new EndpointAddress("net.pipe://localhost/HelloService"));

Console.WriteLine(proxy.Hello("everybody !!!"));

Utilisation de la config

App.config
<configuration>
    <system.serviceModel>
      <client>
        <!-- s'il y a plusieurs endpoint avec le même contract, il faut leur donner un name pour les différencier. -->
        <endpoint address="net.tcp://localhost:8080/HelloService/"
                  binding="netTcpBinding"
                  contract="Contracts.IHelloService"
                  name="tcpEP"/>

        <endpoint address="http://localhost/HelloService/"
                  binding="basicHttpBinding"
                  contract="Contracts.IHelloService"
                  name="httpEP"/>
Cs.svg
var factory = new ChannelFactory<IHelloServiceLocal>("httpEP");
IHelloServiceLocal proxy = factory.CreateChannel();


// avec ClientBase
HelloClient proxy = new HelloClient("tcpEP");
proxy.Hello("You!");

// génère un objet proxy à partir de la config
class HelloClient : ClientBase<IHelloService>, IHelloService
{
    public HelloClient(string endpointName)
        : base(endpointName)
    {
    }

    public string Hello(string name)
    {
        return Channel.Hello(name);
    }
}

Configuration

Binding

La configuration est à faire sur le serveur et sur les clients.
Web.config
<services>
  <service name="Namespace.PersonService">
    <endpoint address="http://localhost/PersonService/"
              binding="wsHttpBinding"
              contract="Namespace.IPersonService"
              bindingConfiguration="specificWsHttpBindingConfig"/>
  </service>
</services>

<bindings>
  <netTcpBinding>
    <!-- configuration par défaut pour tous les netTcpBinding -->
    <binding></binding>
  </netTcpBinding>

  <wsHttpBinding>
    <!-- configuration nommée -->
    <binding name="specificWsHttpBindingConfig"
             sendTimeOut="00:01:00"
             maxReceivedMessageSize="64000">
      <reliableSession inactivityTimeout="00:20:00" order="false"/>
    </binding>
  </wsHttpBinding>
</bindings>

Behavior

Seulement sur le serveur.
Web.config
<services>
  <service name="Namespace.PersonService" behaviorConfiguration="MyBehavior">
    <endpoint address="http://localhost/PersonService/"
              binding="wsHttpBinding"
              contract="Namespace.IPersonService"/>
  </service>
</services>

<behaviors>
  <serviceBehaviors>
    <!-- sans name, devient le behavior par défaut -->
    <behavior name="MyBehavior">
      <serviceDebug includeExceptionDetailInFaults="true" />
      <serviceThrottling maxConcurrentSessions="100"
                         maxConcurrentCalls="16"
                         maxConcurrentInstances="116" />
    </behavior>
  </serviceBehaviors>

  <endpointBehaviors>
        
  </endpointBehaviors>
</behaviors>

Metadata

À la différence de WebAPI, WCF fournit des metadata. Ce qui permet d'avoir une description complète de l'utilisation du service.
Dans VS, l'ajout d'une Service Reference utilise les metadata pour la création automatique de:

  • l'interface du contrat
  • la classe proxy
  • les classe de DataContract

Ceci via l’outil svcutil

Configuration de l'exposition des metadata: http://localhost:8080/mex

Web.config
<services>
  <service name="Namespace.PersonService">
    <host>
      <baseAddresses>
        <add baseAddress="http://localhost:8080" />
      </baseAddresses>
    </host>

    <endpoint address="net.tcp://localhost:8009/PersonService"
              binding="netTcpBinding"
              contract="Namespace.IPersonService" />
  </service>
</services>

<behaviors>
  <serviceBehaviors>
    <behavior>
      <serviceMetadata httpGetEnabled="true" />
    </behavior>
  </serviceBehaviors>
</behaviors>
Web.config
<services>
  <service name="Namespace.PersonService">
    <host>
      <baseAddresses>
        <add baseAddress="http://localhost:8080" />
      </baseAddresses>
    </host>

    <endpoint address="net.tcp://localhost:8009/PersonService"
              binding="netTcpBinding"
              contract="Namespace.IPersonService" />

    <endpoint address="mex"
              binding="mexHttpBinding"
              contract="IMetadataExchange" />
  </service>
</services>

<behaviors>
  <serviceBehaviors>
    <behavior>
      <serviceMetadata />
    </behavior>
  </serviceBehaviors>
</behaviors>

Instancing

PerSession par défaut.
Cs.svg
[ServiceBehavior(InstanceContextMode=InstanceContextMode.PerSession)]
public class HelloService : IHelloService
InstanceContextMode Description
PerCall A new InstanceContext is created for each client request.
PerSession A new InstanceContext is created for each new client session and maintained for the lifetime of that session.
This requires a binding that supports transport sessions: TCP, IPC, WS-HTTP with Reliability or Security
Single A single InstanceContext handles all client requests for the lifetime of the application.

Concurrency

Single par défaut.
Cs.svg
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single,
    ConcurrencyMode = ConcurrencyMode.Multiple)]
public class HelloService : IHelloService
ConcurrencyMode Description
Single Un seul appel à la fois par proxy. Les autres appels sont mis en attente. ThreadSafe.
Multiple Tous les appels sont gérés en même temps. Locker les modifications des données.
Reentrant Similaire à Single, mais permet plusieurs appels en même temps pour le même client (callback).

Contract Mismatch Equivalency

L'interface entre le serveur et le client doit être la même.
Si ce n'est pas le cas, il faut gérer les différences: Namespace, nom de l'interface, noms des méthodes.

Server/IHelloService.cs
namespace Server
{
    [ServiceContract(Namespace = "http://localhost/HelloService")]
    public interface IHelloService
    {
        [OperationContract]
        string Hello(string name);
    }
}
Client/IHelloServiceClient.cs
namespace Client
{
    // même Namespace que pour le server
    // changement du nom de l'interface
    [ServiceContract(Namespace = "http://localhost/HelloService", Name = "IHelloService")]
    interface IHelloServiceClient
    {
        // changement du nom de la méthode
        [OperationContract(Name = "Hello")]
        string HelloClient(string name);
    }
}

ContractNamesapce Assembly Attribut

Permet de définir le Namespace de tous les contrats de cette assembly.
La définition du Namespace dans l'atribut ServiceContract peut donc être supprimée.

AssemblyInfo.cs
//  assembly System.Runtime.Serialization
[assembly: ContractNamespace("http://localhost/HelloService", ClrNamespace = "Server")]

ExtensibleDataObject

Si on ajoute une propriété sur une classe de données sur le serveur mais pas sur celle du client, permet de récupérer quand même cette propriété sur le client.

HostConsole/Person.cs
namespace HostConsole
{
    [DataContract]
    class Person
    {
        [DataMember]
        public string Name { get; set; }

        [DataMember]
        public int Age { get; set; }
    }
}
ClientConsole/Person.cs
namespace ClientConsole
{
    [DataContract]
    class Person : IExtensibleDataObject
    {
        [DataMember]
        public string Name { get; set; }

        public ExtensionDataObject ExtensionData { get; set; }
    }
}

Faults and Exceptions

Pour transmettre les exceptions CLR du serveur au client, WCF utilise les SOAP Faults.

  1. Service lance une exception
  2. WCF package l'exception dans une SOAP fault et l'envoie comme message de réponse
  3. Client recréé l'exception CLR à partir de la SOAP fault
Unhandled
IncludeExceptionDetailInFaults = false
  • Le client reçoit une FaultException
  • pas d'informations supplémentaires
  • le proxy a un State Faulted et ne peut plus être utilisé
Unhandled
IncludeExceptionDetailInFaults = true
  • Le client reçoit une FaultException<ExceptionDetail>
  • accès au message et à la StackTrace de l'exception
  • le proxy a un State Faulted et ne peut plus être utilisé
FaultException
FaultException<T>
  • Le client reçoit une FaultException<T>
  • accès au message et à la StackTrace de l'exception
  • le proxy est toujours viable
Web.config
<behaviors>
  <serviceBehaviors>
    <behavior>
      <serviceDebug includeExceptionDetailInFaults="true" />
HelloService.svc.cs
[ServiceBehavior(IncludeExceptionDetailInFaults = true)]
public class HelloService : IHelloService
{ ... }

FaultException<T>

CustomServerException.cs
[DataContract]
public class CustomServerException
{
    [DataMember]
    public string Message { get; set; }
    [DataMember]
    public string When { get; set; }
    [DataMember]
    public string User { get; set; }
}
IHelloService.cs
[OperationContract]
[FaultContract(typeof(CustomServerException))]
string Hello();
HelloService.svc.cs
string Hello()
{
    var ex = new CustomServerException()
    {
        Message = "Error message",
        When = DateTime.Now,
        User = "Nicolas"
    };
    throw new FaultException<CustomServerException>(ex, "reason");
WcfClient.cs
catch (FaultException<CustomServerException> ex)
{
    CustomServerException csex = ex.Detail;
}

REST

DataContractSerializer ne permet pas la (dé)sérialisation entre le client et le serveur, car le xml généré contient le xmlns correspondant au namespace des classes qui est différent entre le client et le serveur.
Utiliser DataContractJsonSerializer ou [XmlSerializerFormat()]

Serveur REST

IPersonService.cs
// Création d'une interface de contrat
[ServiceContract]
[XmlSerializerFormat()]  // utilisation de XmlSerializer au lieu de DataContractSerializer
public interface IPersonService
{
    [OperationContract]
    [WebGet(ResponseFormat = WebMessageFormat.Json)]  // réponse au format JSON, XML est le format par défaut
    IEnumerable<Person> GetAllPersons();

    [OperationContract]
    [WebGet(UriTemplate = "persons/{id}")]
    Person GetPerson(string id);

    [OperationContract]
    [WebInvoke(Method = "POST", UriTemplate = "persons")]
    void AddPerson(Person person);

    [OperationContract]
    [WebInvoke(Method = "PUT", UriTemplate = "persons/{id}")]
    void ModifyPerson(Person person);

    [OperationContract]
    [WebInvoke(Method = "DELETE", UriTemplate = "persons/{id}")]
    void DeletePerson(int id);
}
PersonService.cs
// Implémentation du service
public class PersonService : IPersonService
{
    public IEnumerable<Person> GetAllPersons() {}
    public Person GetPerson(string id) {}
    public void AddPerson(Person person) {}
    public void ModifyPerson(Person person) {}
    public void DeletePerson(int id) {}
}
Person.cs
[DataContract]
public class Person
{
    [DataMember]
    public string Id { get; set; }

    [DataMember]
    public string Name { get; set; }
}
App.conf
  <system.serviceModel>
    <!-- Liste des services -->
    <services>
      <service name="RestDataService.PersonService">
        <host>
          <baseAddresses>
            <add baseAddress = "http://localhost:8733/Design_Time_Addresses/RestDataService/PersonService/" />
          </baseAddresses>
        </host>

        <endpoint address=""
                  binding="webHttpBinding"
                  contract="RestDataService.IPersonService"
                  behaviorConfiguration="restfulBehavior" />

        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
      </service>
    </services>

    <behaviors>
      <endpointBehaviors>
        <behavior name="restfulBehavior">
          <webHttp />
        </behavior>
      </endpointBehaviors>
    </behaviors>

Client REST

Le client doit connaître l'interface. Ceci peut être fait par une copie du fichier contenant l'interface dans le projet client ou bien par référence de service.

Csharp.svg
// récupération de données, envoie d'une requête GET
var uri = "http://localhost:8733/Design_Time_Addresses/RestDataService/PersonService/GetAllPersons";
var request = WebRequest.Create(uri) as HttpWebRequest;
request.KeepAlive = false;
request.Method = "GET";

using (var responseWeb = request.GetResponse() as HttpWebResponse)
{
    // JSON
    using (var responseStreamReader = new StreamReader(responseWeb.GetResponseStream()))
    {
        var responseString = responseStreamReader.ReadToEnd();
        var allPersons = JsonConvert.DeserializeObject<List<Person>>(responseString);
    }

    // DataContract
    using (var reader = XmlDictionaryReader.CreateTextReader(responseWeb.GetResponseStream(), new XmlDictionaryReaderQuotas()))
    {
        var dataContractSerializer = new DataContractSerializer(typeof(List<Person>));
        var allPersons = dataContractSerializer.ReadObject(reader) as List<Person>;
    }
}
Csharp.svg
// envoie de données, envoie d'une requête POST
string dataToSend = null;
var serializer = new DataContractJsonSerializer(typeof(Person));
using (var stream = new MemoryStream())
{
    serializer.WriteObject(stream, personToAdd);
    dataToSend = Encoding.UTF8.GetString(stream.ToArray(), 0, (int)stream.Length);
}

var uri = "http://localhost:8733/Design_Time_Addresses/RestDataService/PersonService/AddPerson";
var webClient = new WebClient();
webClient.Headers["Content-type"] = "application/json";
webClient.Encoding = Encoding.UTF8;
webClient.UploadString(uri, "POST", dataToSend);

REST, ODATA et POCO

Serveur REST ODATA

Utiliser un WCF Service Application et pas un WCF Service Library.
PersonService.svc
public class PersonService : DataService<DataSourceWrapper>
{
    public static void InitializeService(DataServiceConfiguration config)
    {
        config.SetEntitySetAccessRule("*", EntitySetRights.All);
        config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;
    }
}
DataSourceWrapper.cs
public class DataSourceWrapper
{
    static IEnumerable<Contact> _persons;

    public IQueryable<Contact> Persons
    {
        get
        {
            return _persons.AsQueryable();
        }
    }

    static DataSourceWrapper()
    {
        // chargement des personnes
        _persons = ...
    }
}
Person.cs
[DataServiceKey("Id")]
[DataServiceEntity]
public class Person
{
    public string Id { get; set; }
    public string Name { get; set; }
}
Web.config
<system.serviceModel>
  <services>
    <service name="PersonService">
      <endpoint address=""
                binding="webHttpBinding"
                contract="System.Data.Services.IRequestHandler" />

Url de test:

http://localhost:port/PersonService.svc/Persons Toutes les personnes
http://localhost:port/PersonService.svc/Persons('id') Sélection d'une personne par son id
http://localhost:port/PersonService.svc/Persons('id')/Name Sélection de la propriété Name d'une personne
http://localhost:port/PersonService.svc/Persons('id')/Name/$value Sélection de la valeur de la propriété Name
http://localhost:port/PersonService.svc/Persons?$filter=Firstname eq 'Billy' filtre: toute les personnes dont le nom est Billy

Client REST ODATA

Ajouter une Service Reference pour créer les classes DataSourceWrapper et Person dans le client.

Csharp.svg
Uri uri = new Uri("http://localhost:port/PersonService.svc/");
DataSourceWrapper client = new RestODataServiceReference.DataSourceWrapper(uri);

IEnumerable<Person> persons = client.Persons.Execute();

Callback

Méthode cliente appelée par le serveur en retour d'un premier appel.

Serveur

Interface de contrat

Csharp.svg
[ServiceContract()]
public interface IServicePhotoRappel
{
    [OperationContract(IsOneWay = true)]
    void PhotoTrouvee(Photo photo);
}

[ServiceContract(CallbackContract = typeof(IServicePhotoRappel))]
public interface IServicePhoto
{
    [OperationContract]
    void RecherchePhoto(string photoId);
}

Implémentation du service

Csharp.svg
public class ServicePhoto : IServicePhoto
{
    public void RecherchePhoto(string photoId)
    {
        Console.WriteLine("je recherche ...");
        Thread.Sleep(3000);
        Console.WriteLine("j'ai trouvé\n");

        IServicePhotoRappel rappel =
            OperationContext.Current.GetCallbackChannel<IServicePhotoRappel>();
        // Appel du callback sur le client
        rappel.PhotoTrouvee(new Photo()
        {
            Titre = "Photo1",
            Commentaire = "No comment",
            Image = new byte[] { 1, 2 }
        });
    }
}

La classe Photo

Csharp.svg
[DataContract]
public class Photo
{
    [DataMember]
    public string Titre { get; set; }
    [DataMember]
    public byte[] Image { get; set; }
    [DataMember]
    public string Commentaire { get; set; }
}

Client

Csharp.svg
class ServicePhotoRappel : IServicePhotoRappel
{
    // Callback appelé par le serveur
    public void PhotoTrouvee(Photo photo)
    {
        Console.WriteLine("Photo " + photo.Titre + " reçue");
    }
}

static void Main(string[] args)
{
    InstanceContext context = new InstanceContext(new ServicePhotoRappel());

    IServicePhoto proxy = DuplexChannelFactory<IServicePhoto>.CreateChannel(
        new InstanceContext(new ServicePhotoRappel()),
        new NetTcpBinding(),
        new EndpointAddress("net.tcp://localhost/ServicePhoto"));

    Console.WriteLine("Lancement d'une recherche ...");
    proxy.RecherchePhoto("id");
    Console.WriteLine("Fin de la demande");
}

Sécurité

Scénario Binding Authentification
Intranet TCP Binding Windows
Internet HTTP Binding Authentification protégée par un certificat, puis Windows ou ASP.NET

Identity

Token / Host Identity du processus hôte, celui qui accède aux ressources WindowsIdentity.GetCurrent().Name
Primary Identity du client ServiceSecurityContext.Current.PrimaryIdentity.Name
Windows Dans le cas d'une authentification Windows,
même Identity que Primary, sinon vide
ServiceSecurityContext.Current.WindowsIdentity.Name
Thread Même que Primary Thread.CurrentPrincipal.Identity.Name

Intranet Security

  • Authentification avec les Windows Credentials
  • Les Windows Credentials du client sont envoyés automatiquement au service. Ok pour une Desktop App, mais pose problème pour une WebApp, car le client est IIS.
App.config
<services>
  <service name="HostConsole.PersonService">
    <endpoint address="net.tcp://localhost:8010/Service/"
              binding="netTcpBinding"
              contract="HostConsole.IService"/>

    <endpoint address="net.tcp://localhost:8010/ServiceAdmin/"
              binding="netTcpBinding"
              contract="HostConsole.IAdminService"
              bindingConfiguration="admin"/>
  </service>
</services>

<bindings>
  <netTcpBinding>
    <binding transactionFlow="true" sendTimeout="00:20:00">
      <!-- turn off security -->
      <security mode="None" />
    </binding>
    <binding name="admin" transactionFlow="true" sendTimeout="00:20:00">
      <security mode="Transport">
        <!-- Windows Auth (token) -->
        <transport clientCredentialType="Windows" />
      </security>
    </binding>
  </netTcpBinding>
</bindings>
Cs.svg
// changer les credentials client
var factoryAdmin = new ChannelFactory<IAdminService>("tcpAdminEP");
factoryAdmin.Credentials.Windows.ClientCredential.UserName = "";
factoryAdmin.Credentials.Windows.ClientCredential.Domain = "";
factoryAdmin.Credentials.Windows.ClientCredential.Password = "";
IPersonAdminService proxyAdmin = factoryAdmin.CreateChannel(); 

var proxyAdmin = new AdminClient("tcpAdminEP");
proxyAdmin.ClientCredentials.Windows.ClientCredential.UserName = "";
proxyAdmin.ClientCredentials.Windows.ClientCredential.Domain = "";
proxyAdmin.ClientCredentials.Windows.ClientCredential.Password = "";
MyService.cs
// restreint l'accès aux utilisateur authentifiés qui appartiennent au role Administrator
// sinon SecurityAccessDeniedException : Access is denied.
[PrincipalPermission(SecurityAction.Demand, Role = "Administrators")]
public void DoSomething()
{ ... }

Impernation pour les applications web clientes

Cs.svg
using (((WindowsIdentity)User.Identity).Impersonate())
{
    var factory = new ChannelFactory<IService>("tcpEP");
    IService proxy = factory.CreateChannel();
    proxy.DoSomething();
    proxy.Close();
}

Internet Security

  • Pour permettre à tous les types de clients (non-Windows), l'authentification de fait avec un Username et un Password.
  • On utilise un certificat pour chiffrer les échanges (Username, Password)
  • Sur le serveur, l'authentification peut être fait via Windows ou ASP.NET
  • Négociation
    • PeerTrust le client a une copie du certificat (sans la clé privé)
    • ChainTrust le client a la clé publique encodé en base 64
HostConsole/App.config
<services>
  <service name="HostConsole.PersonService">
    <endpoint address="http://localhost/PersonService/"
              binding="wsHttpBinding"
              contract="HostConsole.IPersonService"/>
    <endpoint address="http://localhost/PersonServiceAdmin/"
              binding="wsHttpBinding"
              contract="HostConsole.IPersonAdminService"
              bindingConfiguration="admin"/>
  </service>
</services>

<bindings>
  <wsHttpBinding>
    <binding transactionFlow="true" sendTimeout="00:20:00">
      <security mode="None" />
    </binding>
    <binding name="admin" transactionFlow="true" sendTimeout="00:20:00">
      <security mode="Message">
        <message clientCredentialType="UserName" negotiateServiceCredential="false"/>
      </security>
    </binding>
  </wsHttpBinding>
</bindings>

<behaviors>
  <serviceBehaviors>
    <behavior>
      <serviceDebug includeExceptionDetailInFaults="true"/>
      <serviceCredentials>
        <!-- où se trouve le certificat  -->
        <serviceCertificate storeLocation="LocalMachine"
                            storeName="Root"
                            findValue="WcfEndToEnd"
                            x509FindType="FindBySubjectName" />
        <!-- authentifier le username et password -->
        <userNameAuthentication userNamePasswordValidationMode="Windows" />
      </serviceCredentials>
      <serviceAuthorization principalPermissionMode="UseWindowsGroups" />
    </behavior>
  </serviceBehaviors>
</behaviors>
Client/App.config
<client>
  <endpoint address="http://localhost/PersonService/"
            binding="wsHttpBinding"
            contract="ClientConsole.IService"/>

  <endpoint address="http://localhost/PersonServiceAdmin/"
            binding="wsHttpBinding"
            contract="ClientConsole.IAdminService"
            bindingConfiguration="admin"
            behaviorConfiguration="admin">
    <identity>
      <certificate encodedValue=
"MIIBuzCCAWWgAwIBAgIQgl2l04hiNqBOBIobr3GjLDANBgkqhkiG9w0BAQQFADAW
MRQwEgYDVQQDEwtSb290IEFnZW5jeTAeFw0xNzEwMzExNTA5NTdaFw0zOTEyMzEy
MzU5NTlaMBYxFDASBgNVBAMTC1djZkVuZHRvRW5kMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQDbVwZbsfTaTB7sM9d6SmbsEk4vO6LgCBl0hkHvYK3QHi161hFb
4uP/pS8kQ+tT1XISpVMBb9i30tb9ab/iRe5eJrBuSinCgsibx39jlFx/2yRF2rLf
qhtMm+mgpk44VVHpJK1YjlWITyL6GQTIPk7+6mEmn77o48qwSi75ehWOFQIDAQAB
o0swSTBHBgNVHQEEQDA+gBAS5AktBh0dTwCNYSHcFmRjoRgwFjEUMBIGA1UEAxML
Um9vdCBBZ2VuY3mCEAY3bACqAGSKEc+41KpcNfQwDQYJKoZIhvcNAQEEBQADQQAU
OXu6XwWpqLUMLV0J/pjz1k7yQvyJmApKy3nqiNbKFjwZSH3ODRN78gTdf9yTjwJc
HS5LaczJpR67SVowg+Kr" />
    </identity>
  </endpoint>
</client>

<behaviors>
  <endpointBehaviors>
    <behavior name="admin">
      <clientCredentials>
        <serviceCertificate>
          <authentication certificateValidationMode="ChainTrust" />

Certificat

ChainTrust

L'utilisation des metadata mex permettent d'obtenir le certificat à la création de la référence au service.
Dos.svg
REM génération d'un fichier de certificat et installation dans Trusted Root Certification Authorities
REM makecert.exe n'est pas fournit pas Windows
makecert.exe -sr LocalMachine -ss Root -pe -sky exchange -n "CN=WcfEndtoEnd" WcfEndToEnd.cer

Générer le fichier de la public key: Win → certmgr.msc → Certificates (Local Computer) → Trusted Root Certification Authorities → Certificates
→ clique-droit sur le certificat → All Tasks → Export:

  • Format: Base-64
  • Ne pas exporter la clé privée
Client/App.config
<client>
  <endpoint address="http://localhost/PersonServiceAdmin/"
            binding="wsHttpBinding"
            contract="ClientConsole.IAdminService"
            bindingConfiguration="admin"
            behaviorConfiguration="admin">
    <identity>
      <certificate encodedValue=
"MIIBuzCCAWWgAwIBAgIQgl2l04hiNqBOBIobr3GjLDANBgkqhkiG9w0BAQQFADAW
MRQwEgYDVQQDEwtSb290IEFnZW5jeTAeFw0xNzEwMzExNTA5NTdaFw0zOTEyMzEy
MzU5NTlaMBYxFDASBgNVBAMTC1djZkVuZHRvRW5kMIGfMA0GCSqGSIb3DQEBAQUA
A4GNADCBiQKBgQDbVwZbsfTaTB7sM9d6SmbsEk4vO6LgCBl0hkHvYK3QHi161hFb
4uP/pS8kQ+tT1XISpVMBb9i30tb9ab/iRe5eJrBuSinCgsibx39jlFx/2yRF2rLf
qhtMm+mgpk44VVHpJK1YjlWITyL6GQTIPk7+6mEmn77o48qwSi75ehWOFQIDAQAB
o0swSTBHBgNVHQEEQDA+gBAS5AktBh0dTwCNYSHcFmRjoRgwFjEUMBIGA1UEAxML
Um9vdCBBZ2VuY3mCEAY3bACqAGSKEc+41KpcNfQwDQYJKoZIhvcNAQEEBQADQQAU
OXu6XwWpqLUMLV0J/pjz1k7yQvyJmApKy3nqiNbKFjwZSH3ODRN78gTdf9yTjwJc
HS5LaczJpR67SVowg+Kr" />
    </identity>
  </endpoint>
</client>

<behaviors>
  <endpointBehaviors>
    <behavior name="admin">
      <clientCredentials>
        <serviceCertificate>
          <authentication certificateValidationMode="ChainTrust" />
        </serviceCertificate>
      </clientCredentials>
    </behavior>
  </endpointBehaviors>
</behaviors>

PeerTrust

Dos.svg
REM génération d'un fichier de certificat et installation dans Personal
REM makecert.exe n'est pas fournit pas Windows
makecert.exe -sr LocalMachine -ss My -pe -sky exchange -n "CN=WcfEndtoEnd" WcfEndToEnd.cer

Installer le certificat sur le client dans Trusted People.

Xml.svg
<client>
  <endpoint address="http://localhost/PersonServiceAdmin/"
            binding="wsHttpBinding"
            contract="ClientConsole.IAdminService"
            bindingConfiguration="admin"
            behaviorConfiguration="admin">
    <identity>
      <!-- par défaut, cherche un certificat ayant le même nom que le domain (localhost) -->
      <dns value="WcfEndToEnd"/>
    </identity>
  </endpoint>
</client>

<behaviors>
  <endpointBehaviors>
    <behavior name="admin">
      <clientCredentials>
        <serviceCertificate>
          <authentication certificateValidationMode="PeerTrust" />
        </serviceCertificate>
      </clientCredentials>
    </behavior>
  </endpointBehaviors>
</behaviors>

ASP.NET Provider / Identity

Permet de coder son propre moyen d'authentification.

Unity WCF

Installer Unity.Wcf qui installe Unity et CommonServiceLocator.

WcfServiceFactory.cs
public class WcfServiceFactory : UnityServiceHostFactory
{
    protected override void ConfigureContainer(IUnityContainer container)
    {
        // register all your components with the container here
        // container
        //    .RegisterType<IService1, Service1>()
        //    .RegisterType<DataContext>(new HierarchicalLifetimeManager());

        container.RegisterType<IHelloService, HelloService>()
            .RegisterType<IDataRepository, DataRepository>();;
    }
}
HelloService.svc
<!-- ancien code -->
<%@ ServiceHost Language="C#" Debug="true" Service="Namespace.HelloService" CodeBehind="HelloService.svc.cs" %>

<!-- nouveau code -->
<%@ ServiceHost Language="C#" Debug="true" Service="Namespace.HelloService" Factory="Namespace.WcfServiceFactory"" %>
HelloService.svc.cs
public HelloService(IDataRepository data)
{ ... }

Lancer le client au lancement du service

  1. clique-droit sur le projet service → Properties
  2. Debug → Start Options
    • Command line arguments : /client:"client.exe"
    • Working Directory : C:\Solution\ProjetClient\bin\Debug\

WCF Test Client

Outils livré avec VS permettant de tester les WS.
C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\Common7\IDE\WcfTestClient.exe

WCF et SSL

Création du certificat:
IIS Manager → sélectionner serveur → Server Certificates → Create Self-Signed Certificate
IIS Self-Signed Certificate créé l'erreur The certificate is not valid for the name ...

Powershell.svg
New-SelfSignedCertificate -DnsName "site.domain.ch -CertStoreLocation "cert:\LocalMachine\My"
  1. Séléctionner le site web → Bindings → Add → Type:https, sélectionner le certificat
  2. SSL Settings → Require SSL
Web.config
<services>
  <service name="ServiceTest">
    <endpoint address=""
              binding="basicHttpBinding"
              contract="IServiceTest"
              bindingConfiguration="secureHttpBinding"/>
    <endpoint address="mex"
              binding="mexHttpBinding"
              contract="IMetadataExchange" />

<bindings>
  <basicHttpBinding>
    <binding name="secureHttpBinding">
      <security mode="Transport">
        <transport clientCredentialType="None"/>

Erreurs

The caller was not authenticated by the service

IIS n’autorise l'accès qu'aux utilisateurs Windows.

Cs.svg
// Modifier les credentials du client
var proxy = new PersonServiceClient("WSHttpBinding_IPersonService");
proxy.ChannelFactory.Credentials.Windows.ClientCredential.UserName = "login";
proxy.ChannelFactory.Credentials.Windows.ClientCredential.Password = "password";

Changer la configuration d'IIS: IIS Manager → sélectionner le site → Authentication

Contract Name could not be found in the list of contracts

Dans le fichier .config du serveur, vérifier le contract du endpoint.

This configuration section cannot be used at this path

Menu → Turn windows features on or off → Internet Information Services → World Wide Web Services → Application Development Features
Cocher tous sauf CGI

The page you are requesting cannot be served because of the extension configuration

Menu → Turn windows features on or off → .NET Framework Advanced Services → WCF Services → HTTP Activation

The Web server is configured to not list the contents of this directory

Web.config
<configuration>
  <system.webServer>
    <!-- autoriser le serveur à lister son contenu -->
    <directoryBrowse enabled="true" />