DCSIMG
May 2008 - Posts - Offir Shvartz

Offir Shvartz

We code with love or we code not...

May 2008 - Posts

Automatic updating web reference .NET

Hi all

In this post I'll discuss the problems and solutions regarding an assembly or web site containing a web reference (using a web service) to a web service that had its schema modified and updating web reference is needed.

The Problem

You have an assembly or web site containing a Web Reference and the web service schema is modified. The possible modification result will be:

  • Runtime error - calling to a web method that it's prototype changed or the web method has been removed.
  • No Error just empty result - one of the parameter names is modified (even only upper to lower case change) the deserialize of the soap request just is not recognize so the parameter is deserialized into null (see note).
  • Missing info - for example add values to Enum is not updated.

Note: the framework deserialization of XML throws an exception only in case of a mismatch of types in the inner XML. In case of a changed or missing  property or member name the mismatch name will be set to its default value - so if no exception was thrown it doesn't  mean that the deserialized process succeeded.

The trivial solution is to right click in VS on the web reference and just press update web reference in order to update the web service proxy. Then rebuild the assembly or web site containing the web reference. The problems raised are:

  • The web service modification requires an action (update web reference) and is not automatic.
  • The need for rebuilding after updating web reference - sometimes the whole web site.

The problem arose in my work many times because we have a lot of web services that several web sites use. Sometimes a small modification in  a web service requires updating a web reference and then rebuilding of many web sites (just for adding new Enum value for example).Often, we don't get a compilation error due to a changed web method prototype,  because we forgot to update the web reference and we crash at runtime. I hope the motivation is clear, let's move to the solution.

The Solutions

There are 2 ways to solve this problem:

  1. Runtime proxy solution.
  2. Build process solution.

1. Runtime proxy solution

This solution is suitable for external web services that are not maintained by you.

You can generate the web service proxy at runtime. Read the following link about Web Reference Class . I won't go into detail about this solution I'll just mention some of its disadvantages:

  • This method is not strong type so we will crash in runtime only if the web method prototype was change, and the code is less legible in that case.
  • Performance: do not generate to proxy more then once  because this action has an high cost (you could use a static object or single tone design pattern to counter this problem).

 

2. Build process solution

This solution is suitable for in house web services part of the same product.

This solution is a way to update the web reference automatically during the build process.  The client that uses the web service doesn't add a web reference, instead simply add a regular reference (DLL file) which contains the proxy.

In order to create this proxy DLL follow these steps:

  • Create a new class Library project.
  • Add to the project two classes: QueryHandler.cs and SDQueryHandelr.cs

QueryHandler.cs is empty, it will be generated automatically during the build process (pre-build event).

SDQueryHandelr.cs contains the constructor of the proxy: we can set the web service URL,  time out and more...

SDQueryHandelr.cs:

public partial class QueryHandler :  
                      System.Web.Services.Protocols.SoapHttpClientProtocol
{
    public QueryHandler()
    {
        WebServiceProxyConfigSection confSec = ConfigMgr.
                 Read(ConfigSectionTypes.AdminWebServiceProxy) 
                                                 as WebServiceProxyConfigSection;
        if (confSec == null)
            throw new Exception("Problem in reading admin WS proxy config file");
        Url = confSec.URL;
        Timeout = confSec.TimeOut;
        if (confSec.PreAuthenticate)
        {
            PreAuthenticate = true;
            Credentials = System.Net.CredentialCache.DefaultCredentials;
        }
    }
}
  • Go to Project properties and in the build events set the pre-build event command line:

 image

The Pre-build Content (this is the main idea):

  1. WebServiceReflector.exe X:\Binaries\Debug\Facade.QueryDispatcher.ServiceImplementation.dll  $(ProjectDir)\..\AdminHost\Service.asmx   - This Runs an application that generates a WSDL file without a need of a web host. We are using the in house application WebServiceReflector (written by Ami Yolovich) which generates a WSDL file by the web service .asmx file and the service implementation DLL. Here is another application that generates a  WSDL file without web hosting of web service.
  2. "C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\"wsdl.exe  /out:$(ProjectDir)\QueryHandler.cs $(ProjectDir)\..\AdminHost\Service.wsdl - Generating the code inside the file QueryHandler.cs using the SDK application WSDL.exe.
  3. cscript replace.vbs "$(ProjectDir)\QueryHandler.cs" "public QueryHandler()" "private void DummyCtor()" - Replacing the constructor generated in the previous line to a private method called DummyCtor: this allow us to implement the constructor in the SDQueryHandler.cs file, using  vbs script for replacing string inside a file.

If you aren't building the web service you can remove stage 1 and just use the existing WSDL by adding to your web service URL the suffix: ?wsdl (e.g. http://localhost/mywebservice.asmx?wsdl) if the web service is online.

  • build the project and the proxy is updated automatically.
  • now we can add this class library to each client that wishes to use this web service:

QueryHandler queryHandler = new QueryHandler();

Response res = queryHandler.InvokeMethod(request);

Note: building the proxies must be post building of the web service.

Note: we are building the clients (the web service consumers) after building the proxy project --> now if one of the web service methods was modified and the client uses this web method we'll get compilation error.

 

Choose your suitable solution by your the web services characteristics and your product demands.

So that's all for now d-:

ASP.NET Error Handling Using HttpModule, Full and Partial Post Back (Ajax UpdatePanel)

This post is the reason for me opening a blog. Recently I encountered a very simple problem that took me around 3 days to solve. I found many posts on this issue but nothing was what I needed.

My task was quite simple I wanted a single point of logic to deal with the fatal errors:

  1. When an error occurs that is not a result of invalid input by the user - and we cannot recover from it.
  2. exception we didn't or couldn't catch (unhandled exceptions).

I must say that most of my experience is with server side so my knowledge in ASP.NET is quite poor (Many thanks to Avi Pinto for all his help ;)

The error handling flow is:

  • Log the Exception
  • Open a generic error page with the error code number and more info.

In ASP.NET there are two ways of doing this. The first  is using the Global.asax file ( which I will not discuss here) and the other is using HttpModule ( a detailed explanation about HttpModules is beyond the scope of this article. You can read about it here).

I choose the HttpModule because it's more reusable - if it needs to be altered you don't need to rebuild your whole web site, just add a reference DLL of the class that implements the interface IHttpModule and add to the web.config file one line:

<configuration>
  <system.web>
    <httpModules>
      <add name="MyHttpModule" 
                      type="MyHttpModule, MyHttpModule" /> 
    </httpModules>
  </system.web>
</configuration>
 

Implementing the IHttpModule:

  • In the Init method enroll to the context error event.
  • Implementing the error handler (my logic): just write to log, build query string for the error page and then Server.Transfer to the error page with the query string generated.

Here I must add two notes about best practice of Exception handling in my opinion:

Note 1: Some developers tend to use the exception technology for logic implementation (e.g. throw DataNotFoundException for empty data). I think it's bad practice, just return empty result. The Exception technology has very high overhead and wasn't design for logic implementation.

Note 2: in your lower levels of logic ( e.g. code behind of a page ) don't catch Exception or WebException unless you have something to do with them: the only reason to catch general Excption is maybe to add information and then rethrow it.

I tested the HttpModule just by removing the catch(Exception ex) lines from the solution I was working on.

It appeared to be working :-) but trying it on another web page it crashed. I checked why and discovered that the error was thrown from ajax UpdatePanel control. Looking for an answer on the web I found that many people encountered this problem - when you modify the response during a partial post back  the response is turned into gibberish). Some solutions allowed redirect from an ajax update panel but not inside a HttpModule which is called during the request life cycle.

First I noticed that in case of a partial post back my module doesn't catch the error. I found the solution to that on the web:

  • enroll to the event PostMapRequestHandler in the module's Init method (there is a parallel way with the Global.asax)
 public void Init(HttpApplication context)
        {
            context.Error += new EventHandler(context_Error);
            context.PostMapRequestHandler += new EventHandler(context_PostMapRequestHandler);
 
        }
  • In the PostMapRequest event handler cast the handler to a Page object and enroll to the Page.Error event
 void context_PostMapRequestHandler(object sender, EventArgs e)
        {
            Page aux = HttpContext.Current.Handler as Page;
            if (aux != null)
            {
                aux.Error += new EventHandler(aux_Error);
            }
        }

This way we catch all the errors (partial and full post back). This allowed me to log the error but the Server.Transfer (and Response.Redirect) was not working :-(  - again, because you cannot change the request in partial post back. This sent me back to the web for more information. I found one answer that suggested I add the module to the web.config file:    

<add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=1.0.61025.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>

This will enable the use of Response.Redirect, but.....not from a HttpModule. In one of my tests I entered the wrong error page URL and then got a PageNotFound Error (404).  When I checked, i found that the error was back in a full post back. From here the way to the solution was near.

The solution (pay attention it's tricky):

  • I am enrolling to both errors of full post back and errors of all types (full and partial post back).
  • In the error handler of all typed post back: log my error and redirect to URL that doesn't exist.
  • In the error handler of full post back: check if the error was as a result of my page not found error (404) or not. If not just clear the error otherwise Server.Transfer to the error page.

the code:

public class ClientErrorHandlingModule : IHttpModule
{
    public const string USER_ID_QS = "UserId";
    public const string ERROR_CODE_QS = "ErrorCode";
    public const string DATE_TIME_QS = "DateTime";
    public const string SOURCE_PAGE_URL_QS = "SorucePageUrl";
    public const string ADMIN_MAIL_ADD_QS = "AdminMailAdd";
    private const string ERROR_PAGE_URL = "Error.aspx";
    private const string NO_PAGE_STR = "No+Page.aspx";
 
    public ClientErrorHandlingModule()
    {
        //
        // TODO: Add constructor logic here
        //
    }
 
    #region IHttpModule Members
 
    public void Dispose()
    {
    }
 
    public void Init(HttpApplication context)
    {
        context.Error += new EventHandler(context_Error);
        context.PostMapRequestHandler += 
                new EventHandler(context_PostMapRequestHandler);
 
    }
 
    void context_PostMapRequestHandler(object sender, EventArgs e)
    {
        Page aux = HttpContext.Current.Handler as Page;
        if (aux != null)
        {
            aux.Error += new EventHandler(aux_Error);
        }
    }
 
    //all error full and partial post back handler
    void aux_Error(object sender, EventArgs e)
    {
        string srcPageUrl = 
                   HttpContext.Current.Request.Url.ToString();
        Exception ex = HttpContext.Current.Server.GetLastError();
        int userId = retrieveUserId();
        //log the error
        ExceptionMgr.HandleClientException(ex, 
               EventIDTypes.GeneralWebError, userId, srcPageUrl);
        //retrieve the error page query string
        string errorPageQueryStr = getErrorPageQueryStr
                                             (srcPageUrl, ex);
        //redirect to no place in order to generate post back
        // normal error
        HttpContext.Current.Response.Redirect(NO_PAGE_STR + "?"
                                    + errorPageQueryStr, false);
 
 
 
    }
 
    //only full postback handler
    void context_Error(object sender, EventArgs e)
    {
        try
        {
            string errorPageQueryStr = string.Empty;
            string srcPageUrl = 
                    HttpContext.Current.Request.Url.ToString();
 
            //if the error comes our handler we retrieve the
            //query string
            if (srcPageUrl.Contains(NO_PAGE_STR))
            {
 
                errorPageQueryStr = srcPageUrl.Substring
                                (srcPageUrl.IndexOf(NO_PAGE_STR) 
                                     + NO_PAGE_STR.Length + 1);
            }
            //otherwise we just clear the error
            else
            {
                HttpContext.Current.Server.ClearError();
                return;
            }
 
            //clear the error and redirect it to the error page
            HttpContext.Current.Server.ClearError();
            HttpContext.Current.Server.Transfer(ERROR_PAGE_URL +
                               "?" + errorPageQueryStr, true);
 
        }
        catch (Exception ex)
        {
            LogMgr.Write(LogCategories.Fatal, 
               "context_Error exception:"
              + ex.Message,EventIDTypes.GeneralInfraError, ex);
        }
    }
 
    private string getErrorPageQueryStr(string i_srcPageUrl,
                                                Exception i_ex)
    {
        ClientErrorConfigurationSection confSec = ConfigMgr.Read
        (ConfigSectionTypes.ClientError) as 
                              ClientErrorConfigurationSection;
        if (confSec == null)
            throw new Exception("Problem in reading Client 
                                           Error config file");
        SoapExceptionInfo sei = getSoapExceptionInfo(i_ex);
        string errorPageQueryStr = getErrorPageQueryString(sei, 
             (int)EventIDTypes.GeneralWebError, 
                        confSec.AdminMailAddress, i_srcPageUrl);
        return errorPageQueryStr;
    }
 
 
 
    private string getErrorPageQueryString(SoapExceptionInfo i_sei,
                    int i_errorCode, string i_adminMailAddress,
                                            string i_srcPageUrl)
    {
        StringBuilder sb = new StringBuilder();
        string template = "{0}={1}&";
        sb.Append(string.Format(template, DATE_TIME_QS,
                                                  DateTime.Now));
        sb.Append(string.Format(template, ADMIN_MAIL_ADD_QS,
                                            i_adminMailAddress));
        sb.Append(string.Format(template, SOURCE_PAGE_URL_QS,
                                                   i_srcPageUrl));
        if (i_sei != null)
        {
            sb.Append(string.Format(template, ERROR_CODE_QS,
                                                i_sei.ErrorCode));
        }
        else
        {
 
            sb.Append(string.Format(template, ERROR_CODE_QS,
                                                   i_errorCode));
        }
 
        return sb.ToString();
    }
 
    private SoapExceptionInfo getSoapExceptionInfo(Exception i_ex)
    {
        SoapExceptionInfo res = null;
        if (i_ex is SoapException)
        {
            res = SoapExceptionHandler.TranslateSoapException(
                                            (SoapException)i_ex);
 
        }
        return res;
    }
 
    private static int retrieveUserId()
    {
        int userId = -1;
        string userIdStr = 
                  HttpContext.Current.Request.Params[USER_ID_QS];
        if (!string.IsNullOrEmpty(userIdStr))
        {
            userId = int.Parse(userIdStr);
        }
        return userId;
    }
 
    #endregion
}

To conclude, the goal was reached: only one place holding the logic for error handling. It works not only with ajax updatepanel but also for all regular post backs.

So that's all for now I hope this post will save others some time.

Cheers Offir

My First Post(take 2)

Hi

My name is Offir Shvartz. I Have around 5 years experience in .NET mainly server side and some client side.I currently work at super derivatives as software infrastructure team leader.

So to be continue...