Comment transmettre des valeurs au constructeur sur mon service wcf?

103

Je voudrais transmettre des valeurs au constructeur sur la classe qui implémente mon service.

Cependant ServiceHost ne me laisse passer que le nom du type à créer, pas les arguments à passer à son constructeur.

Je voudrais pouvoir passer dans une fabrique qui crée mon objet de service.

Ce que j'ai trouvé jusqu'à présent:

Ian Ringrose
la source
6
Je crains que la complexité soit inhérente à WCF et que vous ne puissiez pas faire grand-chose pour l'atténuer, à part ne pas utiliser WCF ou le cacher derrière une façade plus conviviale, comme l'installation WCF de Windsor si vous utilisez Windsor
Krzysztof Kozmic

Réponses:

122

Vous aurez besoin de mettre en œuvre une combinaison de la coutume ServiceHostFactory, ServiceHostet IInstanceProvider.

Étant donné un service avec cette signature de constructeur:

public MyService(IDependency dep)

Voici un exemple qui peut lancer MyService:

public class MyServiceHostFactory : ServiceHostFactory
{
    private readonly IDependency dep;

    public MyServiceHostFactory()
    {
        this.dep = new MyClass();
    }

    protected override ServiceHost CreateServiceHost(Type serviceType,
        Uri[] baseAddresses)
    {
        return new MyServiceHost(this.dep, serviceType, baseAddresses);
    }
}

public class MyServiceHost : ServiceHost
{
    public MyServiceHost(IDependency dep, Type serviceType, params Uri[] baseAddresses)
        : base(serviceType, baseAddresses)
    {
        if (dep == null)
        {
            throw new ArgumentNullException("dep");
        }

        foreach (var cd in this.ImplementedContracts.Values)
        {
            cd.Behaviors.Add(new MyInstanceProvider(dep));
        }
    }
}

public class MyInstanceProvider : IInstanceProvider, IContractBehavior
{
    private readonly IDependency dep;

    public MyInstanceProvider(IDependency dep)
    {
        if (dep == null)
        {
            throw new ArgumentNullException("dep");
        }

        this.dep = dep;
    }

    #region IInstanceProvider Members

    public object GetInstance(InstanceContext instanceContext, Message message)
    {
        return this.GetInstance(instanceContext);
    }

    public object GetInstance(InstanceContext instanceContext)
    {
        return new MyService(this.dep);
    }

    public void ReleaseInstance(InstanceContext instanceContext, object instance)
    {
        var disposable = instance as IDisposable;
        if (disposable != null)
        {
            disposable.Dispose();
        }
    }

    #endregion

    #region IContractBehavior Members

    public void AddBindingParameters(ContractDescription contractDescription, ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
    }

    public void ApplyClientBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, ClientRuntime clientRuntime)
    {
    }

    public void ApplyDispatchBehavior(ContractDescription contractDescription, ServiceEndpoint endpoint, DispatchRuntime dispatchRuntime)
    {
        dispatchRuntime.InstanceProvider = this;
    }

    public void Validate(ContractDescription contractDescription, ServiceEndpoint endpoint)
    {
    }

    #endregion
}

Enregistrez MyServiceHostFactory dans votre fichier MyService.svc ou utilisez MyServiceHost directement dans le code pour les scénarios d'auto-hébergement.

Vous pouvez facilement généraliser cette approche, et en fait certains conteneurs DI l'ont déjà fait pour vous (cue: Windsor's WCF Facility).

Mark Seemann
la source
+1 (Mais beurk, #regions même si c'est le cas le moins grave de l'infraction, je me convertis à l'interface explicite m'implique: P)
Ruben Bartelink
5
Comment puis-je l'utiliser pour l'auto-hébergement? Je reçois une exception après avoir appelé CreateServiceHost. Je ne peux appeler que la méthode protégée publique override ServiceHostBase CreateServiceHost (string constructorString, Uri [] baseAddresses); L'exception est Le message d'exception était: «ServiceHostFactory.CreateServiceHost» ne peut pas être appelé dans l'environnement d'hébergement actuel. Cette API nécessite que l'application appelante soit hébergée dans IIS ou WAS.
Guy
2
@Guy J'ai le problème d'échantillon. Parce que la fonction est que protectedje ne peux pas l'appeler moi-même depuis Main ()
Andriy Drozdyuk
1
Il existe un problème inhérent à cette approche, et c'est que votre dépendance n'est vraiment créée qu'une seule fois dans un environnement hébergé IIS. ServiceHostFactory, ServiceHost et InstanceProvider ne sont tous créés qu'une seule fois jusqu'à ce que le pool d'applications soit recyclé, ce qui signifie que votre dépendance ne peut pas vraiment être actualisée par appel (DbContext par exemple), ce qui introduit une mise en cache involontaire des valeurs et une durée de vie plus longue de la dépendance qui est non désiré. Je ne sais pas vraiment comment résoudre ça, des pensées?
David Anderson
2
@MarkSeemann Je me demande simplement pourquoi vous avez injecté depdans chaque InstanceProvider de contrat . Vous pourriez faire: ImplementedContracts.Values.First(c => c.Name == "IMyService").ContractBehaviors.Add(new MyInstanceProvider(dep));IMyService est une interface de contrat de votre MyService(IDependency dep). Donc, injectez IDependencyuniquement dans InstanceProvider qui en a réellement besoin.
voytek
14

Vous pouvez simplement créer une instance de votre Serviceet la transmettre à l' ServiceHostobjet. La seule chose que vous avez à faire est d'ajouter un [ServiceBehaviour]attribut pour votre service et de marquer tous les objets retournés avec un [DataContract]attribut.

Voici une maquette:

namespace Service
{
    [ServiceContract]
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
    public class MyService
    {
        private readonly IDependency _dep;

        public MyService(IDependency dep)
        {
            _dep = dep;
        }

        public MyDataObject GetData()
        {
            return _dep.GetData();
        }
    }

    [DataContract]
    public class MyDataObject
    {
        public MyDataObject(string name)
        {
            Name = name;
        }

        public string Name { get; private set; }
    }

    public interface IDependency
    {
        MyDataObject GetData();
    }
}

et l'utilisation:

var dep = new Dependecy();
var myService = new MyService(dep);
var host = new ServiceHost(myService);

host.Open();

J'espère que cela facilitera la vie de quelqu'un.

Kerim
la source
5
Cela ne fonctionne que pour les singletons (comme indiqué par InstanceContextMode.Single).
John Reynolds
11

La réponse de Mark avec le IInstanceProviderest correcte.

Au lieu d'utiliser le ServiceHostFactory personnalisé, vous pouvez également utiliser un attribut personnalisé (par exemple MyInstanceProviderBehaviorAttribute). Dérivez-le Attribute, faites-le implémenter IServiceBehavioret implémentez la IServiceBehavior.ApplyDispatchBehaviorméthode comme

// YourInstanceProvider implements IInstanceProvider
var instanceProvider = new YourInstanceProvider(<yourargs>);

foreach (ChannelDispatcher dispatcher in serviceHostBase.ChannelDispatchers)
{
    foreach (var epDispatcher in dispatcher.Endpoints)
    {
        // this registers your custom IInstanceProvider
        epDispatcher.DispatchRuntime.InstanceProvider = instanceProvider;
    }
}

Ensuite, appliquez l'attribut à votre classe d'implémentation de service

[ServiceBehavior]
[MyInstanceProviderBehavior(<params as you want>)]
public class MyService : IMyContract

La troisième option: vous pouvez également appliquer un comportement de service à l'aide du fichier de configuration.

Dalo
la source
2
Techniquement, cela ressemble également à une solution, mais avec cette approche, vous associez étroitement IInstanceProvider au service.
Mark Seemann
2
Juste une deuxième option, aucune évaluation de ce qui est mieux. J'ai utilisé le ServiceHostFactory personnalisé à plusieurs reprises (en particulier lorsque vous souhaitez enregistrer plusieurs comportements).
dalo
1
Le problème est que vous pouvez lancer par exemple le conteneur DI uniquement dans le constructeur d'attribut .. vous ne pouvez pas envoyer de données existantes.
Guy
5

J'ai travaillé à partir de la réponse de Mark, mais (pour mon scénario du moins), c'était inutilement complexe. L'un des ServiceHostconstructeurs accepte une instance du service, que vous pouvez transmettre directement depuis l' ServiceHostFactoryimplémentation.

Pour reprendre l'exemple de Mark, cela ressemblerait à ceci:

public class MyServiceHostFactory : ServiceHostFactory
{
    private readonly IDependency _dep;

    public MyServiceHostFactory()
    {
        _dep = new MyClass();
    }

    protected override ServiceHost CreateServiceHost(Type serviceType,
        Uri[] baseAddresses)
    {
        var instance = new MyService(_dep);
        return new MyServiceHost(instance, serviceType, baseAddresses);
    }
}

public class MyServiceHost : ServiceHost
{
    public MyServiceHost(MyService instance, Type serviceType, params Uri[] baseAddresses)
        : base(instance, baseAddresses)
    {
    }
}
McGarnagle
la source
12
Cela fonctionnera si votre service et toutes les dépendances injectées sont thread-safe. Cette surcharge particulière du constructeur ServiceHost désactive essentiellement la gestion du cycle de vie de WCF. Au lieu de cela, vous dites que toutes les demandes simultanées seront traitées par instance. Cela peut ou non affecter les performances. Si vous voulez être en mesure de gérer des demandes simultanées, cela intégralité de ce graphique d'objet doit être thread-safe, ou vous obtiendrez un comportement incorrect non déterministe. Si vous pouvez garantir la sécurité des threads, ma solution est en effet inutilement complexe. Si vous ne pouvez pas garantir cela, ma solution est requise.
Mark Seemann
3

Vissez… J'ai mélangé les modèles d'injection de dépendances et de localisateur de service (mais surtout c'est toujours l'injection de dépendances et cela a même lieu dans le constructeur, ce qui signifie que vous pouvez avoir un état en lecture seule).

public class MyService : IMyService
{
    private readonly Dependencies _dependencies;

    // set this before creating service host. this can use your IOC container or whatever.
    // if you don't like the mutability shown here (IoC containers are usually immutable after being configured)
    // you can use some sort of write-once object
    // or more advanced approach like authenticated access
    public static Func<Dependencies> GetDependencies { get; set; }     
    public class Dependencies
    {
        // whatever your service needs here.
        public Thing1 Thing1 {get;}
        public Thing2 Thing2 {get;}

        public Dependencies(Thing1 thing1, Thing2 thing2)
        {
            Thing1 = thing1;
            Thing2 = thing2;
        }
    }

    public MyService ()
    {
        _dependencies = GetDependencies(); // this will blow up at run time in the exact same way your IoC container will if it hasn't been properly configured up front. NO DIFFERENCE
    }
}

Les dépendances du service sont clairement spécifiées dans le contrat de sa Dependenciesclasse imbriquée . Si vous utilisez un conteneur IoC (qui ne résout pas déjà le désordre WCF pour vous), vous pouvez le configurer pour créer l' Dependenciesinstance au lieu du service. De cette façon, vous obtenez la sensation chaleureuse et floue que votre conteneur vous donne sans avoir à sauter à travers trop de cerceaux imposés par WCF.

Je ne vais pas perdre de sommeil avec cette approche. Personne d'autre non plus. Après tout, votre conteneur IoC est une grande collection statique de délégués qui crée des choses pour vous. Qu'est-ce qui en ajoute un de plus?

Ronnie Overby
la source
Une partie du problème était que je souhaitais que l'entreprise utilise l'injection de dépendances, et si cela ne semblait pas clair et simple pour un programmeur qui n'avait jamais utilisé l'injection de dépendances, alors l'injection de dépendances ne serait jamais utilisée par aucun autre programmeur. Cependant, je n'ai pas utilisé WCF depuis de nombreuses années, et cela ne me manque pas!
Ian Ringrose
Voici mon approche d'une propriété à écriture unique stackoverflow.com/questions/839788/…
Ronnie Overby
0

Nous étions confrontés à ce même problème et l'avons résolu de la manière suivante. C'est une solution simple.

Dans Visual Studio, créez simplement une application de service WCF normale et supprimez son interface. Laissez le fichier .cs en place (renommez-le simplement) et ouvrez ce fichier cs et remplacez le nom de l'interface par votre nom de classe d'origine qui implémente la logique de service (de cette façon, la classe de service utilise l'héritage et remplace votre implémentation réelle). Ajoutez un constructeur par défaut qui appelle les constructeurs de la classe de base, comme ceci:

public class Service1 : MyLogicNamespace.MyService
{
    public Service1() : base(new MyDependency1(), new MyDependency2()) {}
}

La classe de base MyService est l'implémentation réelle du service. Cette classe de base ne doit pas avoir de constructeur sans paramètre, mais uniquement des constructeurs avec des paramètres qui acceptent les dépendances.

Le service doit utiliser cette classe au lieu du MyService d'origine.

C'est une solution simple et fonctionne comme un charme :-D

Ron Deijkers
la source
4
Vous n'avez pas découplé Service1 de ses dépendances, ce qui était un peu le point. Vous venez d'instancier les dépendances dans le constructeur pour Service1, ce que vous pouvez faire sans la classe de base.
saille
0

C'était une solution très utile - en particulier pour quelqu'un qui est un codeur WCF novice. Je voulais publier un petit conseil pour tous les utilisateurs qui pourraient l'utiliser pour un service hébergé par IIS. MyServiceHost doit hériter de WebServiceHost , pas seulement de ServiceHost.

public class MyServiceHost : WebServiceHost
{
    public MyServiceHost(MyService instance, Type serviceType, params Uri[] baseAddresses)
        : base(instance, baseAddresses)
    {
    }
}

Cela créera toutes les liaisons nécessaires, etc. pour vos points de terminaison dans IIS.

Eric Dieckman
la source
-2

J'utilise des variables statiques de mon type. Je ne sais pas si c'est le meilleur moyen, mais cela fonctionne pour moi:

public class MyServer
{   
    public static string CustomerDisplayName;
    ...
}

Lorsque j'instancie l'hôte de service, je fais ce qui suit:

protected override void OnStart(string[] args)
{
    MyServer.CustomerDisplayName = "Test customer";

    ...

    selfHost = new ServiceHost(typeof(MyServer), baseAddress);

    ....
}
Boris
la source
5
Static / Singletons sont diaboliques! - voir stackoverflow.com/questions/137975/…
Immortal Blue