Ignore invalid Enum values in SOAP deserialization

2k Views Asked by At

I have a WebMethod on an ASP.NET webservice which is returning an array of an Enum. If a new value is added, and that value is returned by a function call, then a consumer of the webservice will throw an exception, even though it doesn't care about that enum value.

[WebMethod]
public UserRole[] GetRoles(string token)

partial wsdl:

  <s:simpleType name="UserRole">
    <s:restriction base="s:string">
      <s:enumeration value="Debug" />
      <s:enumeration value="EventEditor" />
      <s:enumeration value="InvoiceEntry" />
    </s:restriction>
  </s:simpleType>

(Consumer is compiled with this wsdl, but then wsdl changes and a new value is now allowed - if that value is returned, an XML exception is thrown by the client.)

Is there any way to override SOAP deserialization for this type so that I can catch the error and either remove that item from the array or replace it with a default value? If this was using JSON instead of XML, I could register a JsonConverter to handle that type, so I guess I'm looking for a similar global "RegisterConverter" type function. Which I don't think exists, but hoping for some help...

Any means of decorating the Enum with Attributes will not work, because all the code is generated by the wsdl and is regenerated when the web reference is updated. Normally if I want to modify a class that was generated by a wsdl, I can create a partial class, but that doesn't work for an Enum. And not even sure if I could override the XmlSerialization code even if it was a class.



Some additional background:

This is actually implemented as my attempt at a dynamic Enum. The wsdl is generated from a database lookup so that I can add extra values to the database and the consuming application will have access to the allowed values without having to recompile the webservice. This way I get intellisense and constraint enforcement via the enum type, but the ability to add values without tightly coupling the webservice code and the client code. The problem is that if I add a new value, it creates the potential to break consumers that aren't updated with the new wsdl... I would much rather just ignore that value, since the consumers wouldn't know what to do with it anyway.

A SOAP Extension might be the way to fix this (I know how to add a SOAP Extension to the WebService itself, but have no idea how to add one on the client side...), but it's not ideal because I'd really like to have a generic way of handling this easily so I can have more dynamic enums in my code (they're not really dynamic, but the idea is that the values pass through the middle layer of the webservice without having to recompile that middle layer). Something like "XmlSerialization.RegisterConverter(MyDynamicEnumType, DynamicEnum.Convert)" would be ideal, where I can define a generic function to use and register.)

2

There are 2 best solutions below

0
Chris Berger On

Still hoping that someone else will have an answer, but I at least came up with something a little better than my original thought of using Regex.Replace to strip out references to enum values I didn't recognize.

I am using a partial class for the web service and overriding GetReaderForMessage as below:

namespace Program.userws  //note this namespace must match the namespace 
                          //that the webservice is declared in (in auto-generated code)
{
    partial class UserWebService
    {
        protected override XmlReader GetReaderForMessage(SoapClientMessage message, int bufferSize)
        {
            return new EnumSafeXmlReader(message.Stream);
        }
    }
}

This is the definition for EnumSafeXmlReader:

public class EnumSafeXmlReader : XmlTextReader
{
    private Assembly _callingAssembly;

    public EnumSafeXmlReader(Stream input) : base(input)
    {
        _callingAssembly = Assembly.GetCallingAssembly();
    }

    public override string ReadElementString()
    {
        string typename = this.Name;
        var val = base.ReadElementString();

        var possibleTypes = _callingAssembly.GetTypes().Where(t => t.Name == typename);
        Type enumType = possibleTypes.FirstOrDefault(t => t.IsEnum);

        if (enumType != null)
        {
            string[] allowedValues = Enum.GetNames(enumType);

            if (!allowedValues.Contains(val))
            {
                val = Activator.CreateInstance(enumType).ToString();
            }
        }

        return val;
    }
}

I also added a new value for UserRole - UserRole.Unknown, and made sure that it is the first in the list of allowed values.

<s:simpleType name="AcctUserRole">
  <s:restriction base="s:string">
    <s:enumeration value="Unknown"/>
    <s:enumeration value="Debug"/>
    <s:enumeration value="EventEditor"/>
    <s:enumeration value="InvoiceEntry"/>
  </s:restriction>
</s:simpleType>

So as long as a value of this enum is wrapped in a tag with the type name <UserRole>UnexpectedRole</UserRole>, if it is not recognized, it will be replaced with UserRole.Unknown, which my client can happily ignore. Note this could also break if there was another tag called UserRole that was not of this enum type and was expected be, say, string or int. It's fairly brittle.


This solution still leaves a lot to be desired, but it will generally work on lists of enum values...

<GetRolesForUserResult>
    <UserRole>InvoiceEntry</UserRole>
    <UserRole>UnexpectedRole</UserRole>
</GetRolesForUserResult>

This will end up resulting in a UserRole[] that contains UserRole.InvoiceEntry and UserRole.Unknown.

But if I have a field or property of type UserRole:

<User>
    <ID>5</ID>
    <Name>Zorak</Name>
    <PrimaryRole>UnexpectedRole</PrimaryRole>  <!-- causes an exception -->
</User>

this will still fail, because the Reader has no way of knowing that "PrimaryRole" needs to deserialize to type UserRole. The XmlSerializer knows this, but as far as I can tell, there is no way to override the XmlSerializer, only the XmlReader.

I suppose it's not entirely impossible to give the EnumSafeXml Reader enough information to recognize tags that will deserialize to an enum type, but it's more trouble than I'm willing to go to right now - I specifically need it to work in the "array of enum values" case, which it now does.

I did added some caching on Types so that I only have to check a Tag name once to see if it's also the name of an enum, but I removed that for clarity in this example.


I welcome any other possible solutions or recommendations for improving this solution.

7
batwad On

I'll risk the downvote and state the obvious: don't use enums in your service contracts. As you have identified they are brittle except in fixed domains.

If I were a consumer of the service, the Unknown entry would lead me to ask "what am I supposed to do when the service returns this?" to which you would reply something like "don't worry, it won't it's just there for client compatibility" to which I would reply "well if the service won't return it what's it doing in the contract?"

Return a string[] and have your client parse the array for the information it can handle. You can define a subset of the enum in the client if intellisense really is your goal, and you could have implemented this a hundred times over in the time you've spent searching for a more elaborate solution. STEP. AWAY. FROM. THE. ENUMS.