Want to show your appreciation? Please to my charity.

Wednesday, August 20, 2008

Generate Fast Startup .Net Web Service Client That Implements Domain Interface

In my previous blog, I provided a manual solution to the slow startup problem of Spring.Net dynamic proxy Web Service client. And I promised that I'll continue my venture to provide a better solution for applications with a few dozens of Web Service clients.

 

In this blog, I'm going to show how to use CodeDom to generate the very same source code that I have posted in the manual solution.

 

The WebServiceClientGenerator is the class that will generate the source code of a full functional Web Service client that implements a domain interface. So that the class can be used to directly replace the WebServiceProxyFactory in the application context.

 

With the help of the generator class, the same MyTestClient.cs can be generate by the code below:

 

            new WebServiceClientGenerator(

                "Example.Client", "MyTestClient",

                typeof(IMyTest), "http://tempuri.org/"

            ).Generate("..\\..");

 

The next step is some how to integrate this to the VS 2005 or NAnt build process. I need some idea...

 

Here is the generator source code. You can find the CodeUtil class in this blog.

 

/// <summary>

/// Generate source code of a Web Service client that implements a given

/// interface.

/// </summary>

public class WebServiceClientGenerator

{

    #region Private Fields

 

    private static readonly CodeTypeReference _typeSoapHttpClientProtocol =

        new CodeTypeReference(typeof(SoapHttpClientProtocol));

 

    private static readonly CodeTypeReference _typeWebServiceBindingAttribute =

        new CodeTypeReference(typeof(WebServiceBindingAttribute));

 

    private static readonly CodeTypeReference _objectArrayType =

        new CodeTypeReference(typeof(object[]));

 

    private static readonly CodeAttributeDeclaration _soapDocumentMethodAttribute =

        new CodeAttributeDeclaration(

            new CodeTypeReference(typeof(SoapDocumentMethodAttribute)));

 

    private string _fileName;

    private string _fileExtension;

    private string _classNamespace;

    private string _className;

    private CodeDomProvider _codeDomProvider;

    private CodeGeneratorOptions _codeGeneratorOptions;

    private string _wsdlNamespace;

    private Type _interfaceType;

 

    #endregion

 

    #region Public Properites

 

    /// <summary>

    /// Gets and sets the namespace for the generated class.

    /// </summary>

    public string ClassNamespace

    {

        get

        {

            return _classNamespace;

        }

        set

        {

            CheckNullSetter(value);

            _classNamespace = value;

        }

    }

 

    /// <summary>

    /// Gets and sets the name of the generated class.

    /// </summary>

    public string ClassName

    {

        get

        {

            return _className;

        }

        set

        {

            CheckNullSetter(value);

            _className = value;

        }

    }

 

    /// <summary>

    /// Gets and sets the file name without extension. Default to be same

    /// as <see cref="ClassName"/>.

    /// </summary>

    public string FileName

    {

        get { return _fileName ?? _className; }

        set

        {

            _fileName = value;

        }

    }

 

    /// <summary>

    /// Gets and sets the file extension.

    /// </summary>

    public string FileExtension

    {

        get

        {

            return _fileExtension ?? CodeDomProvider.FileExtension;

        }

        set { _fileExtension = value; }

    }

 

    /// <summary>

    /// Gets and sets the interface that the generated class will implement.

    /// </summary>

    public Type Interface

    {

        get

        {

            return _interfaceType;

        }

        set

        {

            CheckNullSetter(value);

            _interfaceType = value;

        }

    }

 

    /// <summary>

    /// Gets and sets the namespace of the Web Service.

    /// </summary>

    public string WsdlNamespace

    {

        get

        {

            return _wsdlNamespace;

        }

        set

        {

            CheckNullSetter(value);

            _wsdlNamespace = value;

        }

    }

 

    /// <summary>

    /// Gets and sets the <see cref="CodeDomProvider"/>.

    /// Default is <see cref="CSharpCodeProvider"/>.

    /// </summary>

    public CodeDomProvider CodeDomProvider

    {

        get

        {

            if (_codeDomProvider == null)

            {

                _codeDomProvider = new CSharpCodeProvider();

            }

            return _codeDomProvider;

        }

        set

        {

            CheckNullSetter(value);

            _codeDomProvider = value;

        }

    }

 

    /// <summary>

    /// Gets and sets the <see cref="CodeGeneratorOptions"/>.

    /// </summary>

    public CodeGeneratorOptions CodeGeneratorOptions

    {

        get

        {

            if (_codeGeneratorOptions == null)

            {

                _codeGeneratorOptions = new CodeGeneratorOptions();

            }

            return _codeGeneratorOptions;

        }

        set

        {

            CheckNullSetter(value);

            _codeGeneratorOptions = value;

        }

    }

 

    #endregion

 

    #region Constructors

    /// <summary>

    /// Construct a new generator instance.

    /// </summary>

    /// <param name="classNamespace">

    /// The namespace for the class being generated.

    /// </param>

    /// <param name="className">

    /// The name of the class.

    /// </param>

    /// <param name="interfaceType">

    /// The interface to implement.

    /// </param>

    /// <param name="wsdlNamespace">

    /// The namespace of the Web Service.

    /// </param>

    /// <exception cref="ArgumentNullException">

    /// When any parameter is <see langword="null"/>.

    /// </exception>

    public WebServiceClientGenerator(

        string classNamespace, string className,

        Type interfaceType, string wsdlNamespace)

    {

        CheckArgumentNull(classNamespace, "classNamespace");

        CheckArgumentNull(className, "className");

        CheckArgumentNull(interfaceType, "interfaceType");

        CheckArgumentNull(wsdlNamespace, "wsdlNamespace");

 

        _classNamespace = classNamespace;

        _className = className;

        _interfaceType = interfaceType;

        _wsdlNamespace = wsdlNamespace;

    }

    #endregion

 

    #region Public Instance Methods

 

    /// <summary>

    /// Generate the source file in a directory specified by

    /// <paramref name="filePath"/>.

    /// </summary>

    /// <remarks>

    /// The name of the file is determined by the <see cref="FileName"/>

    /// property and the <see cref="FileExtension"/> property.

    /// </remarks>

    /// <param name="filePath">

    /// The directory where the source file will be generated.

    /// </param>

    /// <exception cref="ArgumentNullException">

    /// When <paramref name="filePath"/> is <see langword="null"/>.

    /// </exception>

    public void Generate(string filePath)

    {

        CheckArgumentNull(filePath, "filePath");

        string fullPath = Path.Combine(filePath, FileName + "." + FileExtension);

        using (Stream s = File.Open(fullPath, FileMode.Create))

        {

            StreamWriter sw = new StreamWriter(s);

            Generate(sw);

            //Close StreamWriter

            sw.Close();

            s.Close();

        }

    }

 

    /// <summary>

    /// Write out the code to the <paramref name="writer"/>.

    /// </summary>

    /// <param name="writer">

    /// The writer to where the generated code will be sent.

    /// </param>

    /// <exception cref="ArgumentNullException">

    /// When <paramref name="writer"/> is <see langword="null"/>.

    /// </exception>

    public void Generate(TextWriter writer)

    {

        CheckArgumentNull(writer, "writer");

 

        ICodeGenerator generator = CodeDomProvider.CreateGenerator(writer);

        CodeGeneratorOptions options = CodeGeneratorOptions;

 

        //Disable documentation warning

        generator.GenerateCodeFromCompileUnit(

            new CodeSnippetCompileUnit("#pragma warning disable 1591"),

            writer, options);

 

        // generate the code

        generator.GenerateCodeFromNamespace(

            ImplementWebServiceClient(ClassNamespace, ClassName, Interface, WsdlNamespace),

            writer, options);

 

        //Restore documentation warning

        generator.GenerateCodeFromCompileUnit(

            new CodeSnippetCompileUnit("#pragma warning restore 1591"),

            writer, options);

    }

 

    #endregion

 

    #region Private Methods

 

    // implements the web service client

    private static CodeNamespace ImplementWebServiceClient(

        string classNamespace,

        string className,

        Type interfaceType,

        string wsdlNamespace)

    {

        //Create Namespaces

        CodeNamespace codeNamespace = new CodeNamespace(classNamespace);

 

        //Create web service binding attribute

        CodeAttributeDeclaration webServiceBindingAttribute =

            new CodeAttributeDeclaration(

                _typeWebServiceBindingAttribute,

                new CodeAttributeArgument(

                    "Namespace",

                    new CodePrimitiveExpression(wsdlNamespace)

                    )

                );

 

        //Create Class Declaration

        CodeTypeDeclaration codeClass = CodeUtils.DefineClass(

            new CodeAttributeDeclaration[] { webServiceBindingAttribute },

            TypeAttributes.Public,

            className,

            _typeSoapHttpClientProtocol,

            new CodeTypeReference(interfaceType));

 

        // implements interface

        codeClass.Members.AddRange(ImplementInterface(interfaceType));

 

        codeNamespace.Types.Add(codeClass);

 

        return codeNamespace;

    }

 

    // implements all the methods defined in the interface.

    private static CodeMemberMethod[] ImplementInterface(Type theInterface)

    {

        MethodInfo[] methods = theInterface.GetMethods();

        CodeMemberMethod[] codeMethods = new CodeMemberMethod[methods.Length];

        for (int i = 0; i < methods.Length; i++)

        {

            codeMethods[i] = ImplementWebMethod(methods[i]);

        }

        return codeMethods;

    }

 

    // implement one web method

    private static CodeMemberMethod ImplementWebMethod(MethodInfo method)

    {

        CodeTypeReference returnType =

            new CodeTypeReference(method.ReturnType);

 

        CodeMemberMethod codeMethod = CodeUtils.DefineMethod(

            new CodeAttributeDeclaration[] { _soapDocumentMethodAttribute },

            MemberAttributes.Public,

            returnType,

            method.Name);

 

        // Define all parameters and seperate them by in/out parameter

        // type. Ref type is considered both in and out.

        List<CodeVariableReferenceExpression> inParams =

            new List<CodeVariableReferenceExpression>();

 

        List<OutParam> outParams = new List<OutParam>();

 

        foreach (ParameterInfo param in method.GetParameters())

        {

            // Determine the true parameter type

            Type paramType = param.ParameterType;

            if (paramType.IsByRef) paramType = paramType.GetElementType();

 

            CodeTypeReference codeParamType =

                new CodeTypeReference(paramType);

 

            FieldDirection direction =

                CodeUtils.DetermineParameterDirection(param);

 

            CodeVariableReferenceExpression codeParam =

                CodeUtils.DefineParameter(

                    codeMethod, codeParamType, direction, param.Name);

 

            if (direction != FieldDirection.Out) // In or Ref

            {

                inParams.Add(codeParam);

            }

 

            if (direction != FieldDirection.In) // Out or Ref

            {

                //outParamTypes.Add(codeParamType);

                outParams.Add(new OutParam(codeParamType, codeParam));

            }

 

        }

 

        // Calls the Invoke method on base class with all 'in' parameters

        CodeMethodInvokeExpression invokation = new CodeMethodInvokeExpression(

            new CodeThisReferenceExpression(), "Invoke",

            new CodePrimitiveExpression(method.Name),

            new CodeArrayCreateExpression(_objectArrayType, inParams.ToArray())

            );

 

        // Define the result variable and initialize it with the result of

        // the invoke method.

        CodeVariableReferenceExpression result = CodeUtils.DefineVariable(

            codeMethod, _objectArrayType, "result", invokation);

 

        // Return value and out parameter index

        int index = 0;

 

        // Define the return statement

        CodeMethodReturnStatement codeReturn = new CodeMethodReturnStatement();

        if (method.ReturnType != typeof(void))

        {

            // Return the result if return type is not void.

            codeReturn.Expression = new CodeCastExpression(

                returnType, CodeUtils.ArrayIndex(result, index++));

        }

 

        // Assign result to 'out' parameters

        foreach (OutParam outParam in outParams)

        {

            codeMethod.Statements.Add(

                new CodeAssignStatement(

                    outParam.Variable,

                    new CodeCastExpression(outParam.Type,

                                           CodeUtils.ArrayIndex(result, index++))

                    )

                );

        }

 

        // Add the return statement

        codeMethod.Statements.Add(codeReturn);

 

        return codeMethod;

    }

 

    private static void CheckArgumentNull(object value, string name)

    {

        if (value == null)

        {

            throw new ArgumentNullException(name);

        }

    }

 

    private static void CheckNullSetter(object value)

    {

        CheckArgumentNull(value, "value");

    }

 

    #endregion

 

    private struct OutParam

    {

        public CodeTypeReference Type;

        public CodeVariableReferenceExpression Variable;

 

        public OutParam(

            CodeTypeReference type,

            CodeVariableReferenceExpression variable)

        {

            Type = type;

            Variable = variable;

        }

    }

}

 

Generate Fast Startup .Net Web Service Client That Implements Domain Interface

No comments:

Post a Comment