Search code examples
c#wcfasp.net-web-apiserializationdeserialization

C# Owin self hosted service controller reads all property values of the method argument objects


I am trying to replace the WCF services published with my application with Asp.Net Rest WebApi services self hosted with Owin.

I thought serialization / deserialization was done for DataMember properties only, but it seems to do something with properties that aren't too.

I am going to explain broadly what the background of the problem is:

  • The server application is a console app that can be run as a service.
  • Every WCF Service with its methods is going to be replaced by a Web Api Controller with the same methods
  • For a certain period of time, the application will publish both service endpoints (WCF and WebApi) to allow clients to connect using the protocol of their choice
  • Every WCF/WebApi method have only one parameter represented by an object of a certain type (which contains all the needed parameters as properties), and the response is always an object of another certain type (which contains the result info as class properties).
  • Every class used as request argument or response result is decorated with appropriate DataContract and DataMember attributes
  • The WebApi version uses default serialization/deserialization (Json)

For example, a WCF service interface is something like this:

[ServiceContract]
public interface ISessionService
{
    [OperationContract]
    LoginResult Login(LoginRequest request);
}

The WebApi version controller is something like this:

[RoutePrefix(nameof(ISessionService))]
public class SessionServiceController : ApiController
{
    [HttpPost]
    [Route(nameof(ISessionService.Login))]
    public IHttpActionResult Login([FromBody] LoginRequest request)
    {
        return this.Ok(this.LoginExecute(request));
    }
}

The LoginExecute method is not important, it only returns an instance of the LoginResult class and is used only to run the service method logic.

I am in trouble with the deserialization of the parameters of the service because seems that all properties of the object used as parameter is beeing read by something in the Owin pipeline. I am not going to deeply explain why this is causing problems, but I can say, in simple words, that in some cases there are circular references between various classes. This causes infinite looping when reading ALL the properties of the object instance, and should be avoided.
I noticed this problem pausing the execution of the app, and seeing that the stack trace of the call is performing the DelegatingHandler.SendAsync method. From here I start to press F10 to debug step by step and see that many property getters is read.

I know that, to solve the problem, I should avoid the circular references, but for now it is a task that I cannot afford.

So I am asking: is there a workaround to prevent the Web Api deserialization to read through all properties of the deserialized parameter object as WCF does with DataContractSerializer?

Edit 24/11/2020 14:18

After many searches I found what is causing the reading of all properties of the model: the IBodyModelValidator service. I found a solution that works for me, and I am going to answer myself with it for anyone who had this "problem"


Solution

  • After some research I found that the problem was not the serialization, but the IBodyModelValidator service, which was reading all the properties Metadata of the model, including property values.

    The solution I found is to override the BodyModelValidator service in HttpConfiguration.Services.

    This class has a method called bool ShouldValidateType(Type type) that can avoid validation on a type and, since I am not using any integrated validation on the model, I can skip the entire type from it.

    I used a custom attribute to determine if the class has to perform validation or not

    The steps are the following:

    Create WebApiOptionsAttribute class

    [AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
    public sealed class WebApiOptionsAttribute : Attribute
    {
    
        public bool IsBodyModelValidatorDisabled { get; set; } = false;
    
        public static bool GetIsBodyModelValidatorDisabled(Type type)
        {
            return type.GetCustomAttributes(typeof(WebApiOptionsAttribute), true).FirstOrDefault() is WebApiOptionsAttribute attribute ? attribute.IsBodyModelValidatorDisabled : false;
        }
    
    }
    

    You can apply the attribute to any class to control if validation should be avoided

    Create a WebApiOptionsBodyModelValidator class

    public class WebApiOptionsBodyModelValidator : DefaultBodyModelValidator
    {
    
        public override bool ShouldValidateType(Type type)
        {
            if (WebApiOptionsAttribute.GetIsBodyModelValidatorDisabled(type))
            {
                return false;
            }
            else
            {
                return true;
            }
        }
    }
    

    Override the default IBodyModelValidator service

    in Global.asax for Asp.Net:

    protected void Application_Start(object sender, EventArgs e)
    {
        GlobalConfiguration.Configure(Register);
    }
    
    public static void Register(HttpConfiguration config)
    {
        config.Services.Replace(typeof(IBodyModelValidator), new WebApiOptionsBodyModelValidator());
    }
    

    or in Owin startup for a Self hosting application:

    public void StartWebApi()
    {
        this.WebService = WebApp.Start(new StartOptions(), Configuration);
    }
    
    private static void Configuration(IAppBuilder appBuilder)
    {
        // Configure Web API for self-host.
        var config = new HttpConfiguration();
        config.Services.Replace(typeof(IBodyModelValidator), new NSBodyModelValidator());
        appBuilder.UseWebApi(config);
    }
    

    Use the attribute to decorate the classes

    [WebApiOptions(IsBodyModelValidatorDisabled = true)]
    public class LoginRequest
    {
        public bool Test
        {
            get
            {
                //if you place a breakpoint here, it should never hit
                //if you set IsBodyModelValidatorDisabled = false, the breakpoint should hit
                return true;
            }
        }
    }