Ashutosh Goyal
Ashutosh Goyal

Reputation: 21

WCF Client proxy implementation

I recently appeared for an interview where I was asked following two questions:-

  1. How will you create a WCF client proxy that will automatically update itself when something changes in the Service implementation without building the client again and without using the Visual studio Update Service Reference functionality.

  2. How will you override the session management (PerSession, PerCall and Single) to create custom functionality in your service code bypassing the normal functionality provided by WCF for above 3 session management options.

Any help is much appreciated.

Upvotes: 2

Views: 850

Answers (1)

knyu
knyu

Reputation: 996

To answer you first question I create a simple prototype project to demonstrate a dynamic WCF proxy implementation: https://github.com/knyu15/wcf-dynclient

This is very basic implementation, just some kind of proof-of-concept. This service functionality implements generating MD5-hash of string provided by a client.

The basic idea is to retrieve metadata from the server, compile it in the proxy object and use dynamic object to access them.

It consists of four projects:

  • wcf_dynclient_lib: represents service interface (IDynClientLib) and implementation. It consists of one method returning hash algorithm name and hash itself, computed on the server-side:

    public HashInfo GetHash(string value)

  • wcf_dynclient: just a console host of the service, exposing two endpoints - wsHttpBinding and metadata exchange.

  • wcf_dynclient_proxy: this is a core proxy implementation. To create a proxy you need a service address and contract you want to use.

  • wcf_dynclient_proxy_client: this is a client. Please note, that it is not contains a reference to wcf_dynclient_lib and use a proxy (wcf_dynclient_proxy) to access service method and data.

One of the key issue is to implement auto-update scenario for the service proxy. In case of duplex binding or WebSocket implementation it may be a signal sending to the proxy client to update it's metadata.

But in my scenario I put attention on most common situation, when metadata just exposed by the server and client checks if update needed. There are no problem to implement auto-update scenario for proxy using duplex channel if needed.

Here is a full usage scenario:

class Program
{
    // persistent proxy object
    private static readonly Proxy Proxy = new Proxy("http://localhost:8100/mex", "IDynClientLib");

    // persistent dynamic client-side instance of the WCF service contract implementation
    private static dynamic Instance;

    static void Main(string[] args)
    {
        const string testString = "test string";            
        Demo(testString);
    }

    private static void Demo(string testString)
    {
        // Check if we have an instance, create otherwise
        if (Instance == null)
            Instance = Proxy.CreateNewInstance();

        // Check if update of contract needed. 
        // Please note, that if contract was updated (some methods added, some removed, etc.) the
        // existent code may be not valid!
        if (Proxy.IsUpdateNeeded())
            Instance = Proxy.CreateNewInstance();

        // Get response of the server (HashInfo instance)
        var serverHash = Instance.GetHash(testString);

        // Server-side hash bytes
        var serverHashBytes = serverHash.Hash;

        // Client-side hash bytes
        var clientHashBytes = GetHash(testString);

        // Check if server returns correct hash algorithm info
        if (serverHash.HashAlgorithm != "MD5")
            throw new InvalidOperationException("Wrong hash algorithm");

        // Dump algorithm info to console
        Console.WriteLine(serverHash.HashAlgorithm);

        // Check if hash valid
        if (CompareHash(serverHashBytes, clientHashBytes) == false)
            throw new InvalidOperationException("Hash does not equals");
        else
            Console.WriteLine("Hash equals!");
    }

    static byte[] GetHash(string value)
    {
        using (MD5 hashAlgorithm = new MD5Cng())
        {
            return hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(value));
        }
    }

    static bool CompareHash(byte[] hash1, byte[] hash2)
    {
        return StructuralComparisons.StructuralEqualityComparer.Equals(hash1, hash2);
    }
}

Proxy actually retreive metadata from the server, compiles it and returns dynamic object wrapper which allows to use an unknown contract.

Below are snippets from proxy class. CreateInstance method creates service instance object using previously generated metadata.

public dynamic CreateNewInstance()
{
    Debug.Assert(string.IsNullOrWhiteSpace(m_contractName) == false);

    // update metadata
    Update();

    // compile and return dynamic wrapper
    return Compile();
}

private object CreateInstance(CompilerResults compilerResults)
{
    ServiceEndpoint serviceEndpoint = GetServiceEndpoint();
    if (serviceEndpoint == null)
        throw new InvalidOperationException("ServiceEndpoint is not initialized");

    var clientProxyType = compilerResults.CompiledAssembly.GetTypes().First(
        t => t.IsClass &&
             t.GetInterface(m_contractName) != null &&
             t.GetInterface(typeof (ICommunicationObject).Name) != null);

    var instance = compilerResults.CompiledAssembly.CreateInstance(
        clientProxyType.Name,
        false,
        BindingFlags.CreateInstance,
        null,
        new object[] {serviceEndpoint.Binding, serviceEndpoint.Address},
        CultureInfo.CurrentCulture, null);

    return instance;
}

This is how service metadata generated:

    // Create MetadataSet from address provided
    private MetadataSet GetMetadata()
    {
        Debug.Assert(string.IsNullOrWhiteSpace(m_mexAddress) == false);

        var mexUri = new Uri(m_mexAddress);
        var mexClient = new MetadataExchangeClient(mexUri, MetadataExchangeClientMode.MetadataExchange)
        {
            ResolveMetadataReferences = true
        };

        return mexClient.GetMetadata();
    }

    // Getting or updating contract descriptions and service endpoints
    private void UpdateMetadata(MetadataSet metadataSet)
    {
        if (metadataSet == null) 
            throw new ArgumentNullException("metadataSet");

        MetadataImporter metadataImporter = new WsdlImporter(metadataSet);
        m_contractDescriptions = metadataImporter.ImportAllContracts();
        m_serviceEndpoints = metadataImporter.ImportAllEndpoints();
    }

    // Compile metadata
    private CompilerResults CompileMetadata()
    {
        Debug.Assert(string.IsNullOrWhiteSpace(m_contractName) == false);
        Debug.Assert(m_contractDescriptions != null);
        Debug.Assert(m_serviceEndpoints != null);

        var generator = new ServiceContractGenerator();

        m_serviceContractEndpoints.Clear();
        foreach (var contract in m_contractDescriptions)
        {
            generator.GenerateServiceContractType(contract);
            m_serviceContractEndpoints[contract.Name] = m_serviceEndpoints.Where(
                se => se.Contract.Name == contract.Name).ToList();
        }

        if (generator.Errors.Count != 0)
            throw new InvalidOperationException("Compilation errors");

        var codeDomProvider = CodeDomProvider.CreateProvider("C#");
        var compilerParameters = new CompilerParameters(
            new[]
            {
                "System.dll", "System.ServiceModel.dll",
                "System.Runtime.Serialization.dll"
            }) { GenerateInMemory = true };


        var compilerResults = codeDomProvider.CompileAssemblyFromDom(compilerParameters,
            generator.TargetCompileUnit);

        if (compilerResults.Errors.Count > 0)
            throw new InvalidOperationException("Compilation errors");

        return compilerResults;
    }

The full source code available by link I posted above.

Check if update needed compares service metadata. It is not efficient way to do this, and added just as an example:

public bool IsUpdateNeeded()
{
    if (m_metadataSet == null)
        return true;

    var newServerMetadata = GetMetadata();

    var newMetadataString = SerializeMetadataSetToString(newServerMetadata);
    var currentMetadataString = SerializeMetadataSetToString(m_metadataSet);

    return newMetadataString == currentMetadataString;
}

And custom dynamic object implementation just wraps a method calls:

internal class DynamicProxy : DynamicObject
{
    public DynamicProxy(object wcfObjectInstance)
    {
        if (wcfObjectInstance == null) 
            throw new ArgumentNullException("wcfObjectInstance");

        m_wcfObjectInstance = wcfObjectInstance;
    }

    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
        Debug.Assert(m_wcfObjectInstance != null);

        result = null;

        var method = m_wcfObjectInstance.GetType().GetMethod(binder.Name);
        if (method == null)
            return false;

        result = method.Invoke(m_wcfObjectInstance, args);
        return true;
    }

    private readonly object m_wcfObjectInstance;
}

To answer you second question you need to use custom implementation of IInstanceProvider Interface. Here is a good article on this topic:http://blogs.msdn.com/b/carlosfigueira/archive/2011/05/31/wcf-extensibility-iinstanceprovider.aspx.

Upvotes: 2

Related Questions