Schon toll was die WCF alles so kann. In einem meiner aktuellen Projekte arbeite ich an einem WCF-Dienst. Dieser soll auch binäre Daten empfangen können. Also habe ich ganz pragmatisch folgenden Service-Contract implementiert… 

[ServiceContract()]
public interface IUploadService
{
  [OperationContract()]
  void Upload(byte[] data);
}

Das ist funktional – aber nicht wirklich toll. Denn diese Art des Contract verhindert, das die Daten an den Server gestreamt werden können, sprich: die Daten werden im RAM des Servers gepuffert und dann “am Stück” verarbeitet. Selbst wenn nur wenige Anfragen kommen, kann dies den Speicherverbrauch massiv beeinträchtigen, etwa wenn große Datenmengen übertragen werden. 

Also habe ich einen Blick in die MSDN geworfen und bin auf: http://msdn.microsoft.com/en-us/library/ms733742.aspx gestoßen. Das MTOM-Encoding hat dann mein Interesse geweckt, allerdings hat sich mein “Nucleus accumbens” ziemlich gelangweilt, weil sich die Erfolgserlebnisse etwas rar gemacht hatten :-) 

Im ersten Schritt habe ich den Contract angepasst… 

[ServiceContract()]
public interface IUploadService
{
  [OperationContract()]
  void Upload(Stream dataStream);
}

Ich war dann sofort in der Annahme, das dies ja wohl ausreichen müsste :-) 

Tatsächlich kommt nur durch die Deklaration einer Service-Methode, die einen Stream-Parameter bereitstellt, keine automatische Verwendung des MTOM-MessageEncoding zu Stande (Schade eigentlich). Bei MTOM handelt es sich um eine Optimierung, die natürlich entsprechend konfiguriert werden muss. Beim Anlegen eines neuen WCF-Dienstprojekts in Visual Studio 2010 sieht die web.config so aus… 


<?xml version="1.0"?>
<configuration>

  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>

  <system.serviceModel>

    <serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />

    <behaviors>
      <serviceBehaviors>
        <behavior>
          <serviceMetadata httpGetEnabled="true" />
          <serviceDebug includeExceptionDetailInFaults="true"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>

  </system.serviceModel>

  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
  </system.webServer>

</configuration>

Damit der UploadService nun mittels MTOM kommuniziert, sind einige Änderungen an der web.config notwendig. Als erstes habe ich ein bindings-Element hinzugefügt… 


...
<bindings>

  <basicHttpBinding>

    <binding name="BasicHttpBinding_IUploadService"
             messageEncoding="Mtom"
             transferMode="Streamed"
             maxReceivedMessageSize="2147483647">
      <security mode="None">
        <transport clientCredentialType="None" />
      </security>
    </binding>

  </basicHttpBinding>

</bindings>
...

Entscheidend ist hier die Verwendung von basicHttpBinding. MTOM kann nicht mit allen Bindings, die die WCF zur Verfügung stellt verwendet werden; da ich meinen Dienst im IIS hosten möchte, sind die Bindings netTcpBinding und netNamedPipeBinding nicht relevant. Der Name des Bindings spielt keine tragende Rolle, wichtig ist allerdings, das das messageEncoding- und transferMode-Attribut wie im Beispiel gesetzt werden. 

Anschließend muss für den UploadService ein Endpoint konfiguriert werden, der mit dem Binding verknüpft wird. Dazu wird ein services-Element hinzugefügt (man hätte sich wohl nichts abgebrochen, wenn diese Einstellungen in Form eines Kommentars bereits in der Projektvorlage enthalten gewesen wäre). 

...

<services>

  <service name="MyServiceDemo.UploadService"
           behaviorConfiguration="defaultBehaviour">
    <endpoint binding="basicHttpBinding"
              contract="MyServiceDemo.IUploadService"
              bindingConfiguration="BasicHttpBindiung_IUploadService"></endpoint>

  </service>

</services>
...

Das service-Element definiert den Service; analog zum contract-Attribut des endpoint-Elements, habe ich im name-Attribut den Typ meiner Implementierung samt Namespace eingesetzt. Das behaviourConfiguration-Attribut verweist auf ein behaviour-Element, wobei ich den Bezeichner “defaultBehaviour” einfach im bereits vorhandenen Code hinzugefügt habe (siehe nächstes Beispiel). Das endpoint-Element verknüpft letztendlich den Service-Contract mit einem Binding. Im bindingConfiguration-Attribut wird der Name des Bindings gesetzt. 


...
<behavior name="defaultBehaviour">
  <serviceMetadata httpGetEnabled="true" />
  <serviceDebug includeExceptionDetailInFaults="true"/>
</behavior>
...

Eigentlich ist der Service damit ausreichend konfiguriert. 

Der Cassini-Webserver unterstützt leider kein Streaming, daher kann diese Funktionalität nur getestet werden, wenn der WCF-Dienst selbst, oder im IIS gehostet wird. Wenn der Dienst im IIS gehostet werden soll, muss in der web.config ein serviceHostingEnvironment-Element hinzugefügt werden, dessen aspNetCompatibilityEnabled-Attribut auf den Wert “true” gesetzt ist.


...
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" />
...

Außerdem muss die jeweilige Service-Implementierung mit einem AspNetCompatibilityRequirements-Attribut  versehen werden.


[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
public class UploadService : IUploadService
{

}

Okay. Aber wie sieht es nun am Client aus? In Visual Studio 2010 kann ein Verweis auf den WCF-Dienst einfach über den Wizard hinzugefügt werden…

Der Assistent fügt entsprechende Konfigurationsabschnitte in die app- bzw. web.config hinzu. Das Beispiel zeigt nicht exakt den generierten Code – hier habe bereits etwas abgespeckt, um es etwas übersichtlicher zu machen.


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <system.serviceModel>
    <bindings>
      <basicHttpBinding>
        <binding name="BasicHttpBinding_IUploadService"
                 allowCookies="false"
                 bypassProxyOnLocal="false"
                 hostNameComparisonMode="StrongWildcard"
                 maxBufferSize="65536"
                 maxBufferPoolSize="524288"
                 maxReceivedMessageSize="65536"
                 messageEncoding="Mtom"
                 textEncoding="utf-8"
                 transferMode="Buffered"
                 useDefaultWebProxy="true">

          <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
                        maxBytesPerRead="4096" maxNameTableCharCount="16384" />

          <security mode="None">
            <transport clientCredentialType="None"
                       proxyCredentialType="None"
                       realm="" />
            <message clientCredentialType="UserName"
                     algorithmSuite="Default" />
          </security>
         
        </binding>
      </basicHttpBinding>
    </bindings>
    <client>
      <endpoint address="http://localhost/MyService/UploadService.svc"
                binding="basicHttpBinding"
                bindingConfiguration="BasicHttpBinding_IUploadService"
                contract="ServiceReference1.IUploadService"
                name="BasicHttpBinding_IUploadService" />
    </client>
  </system.serviceModel>
</configuration>

Das Binding, das durch den Assistenten in die Konfiguration hinzugefügt wurde, kann so nicht verwendet werden. Das transferMode-Attribut des binding-Elements ist auf den Wert “Buffered” gesetzt – hier muss die Voreinstellung auf “Streaming” geändert werden. Erstaunlicherweise ist der Wert des messageEncoding-Attributs bereits korrekt gesetzt. Nachdem die Konfiguration angepasst wurde, kann der Client wie folgt, an den Service binäre Daten übertragen…


ServiceReference1.IUploadService dataService = new ServiceReference1.UploadServiceClient();
   
using (FileStream fs = File.Open(@"C:\temp\really-good-picture.jpg", FileMode.Open))
 dataService.Upload(fs);