2

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.)

Chris Berger
  • 557
  • 4
  • 14
  • Well, I've thought of two possible solutions. One is to override GetReaderForMessage on the WebService declaration (in a partial class) and then do Regex matching for the enum name and values, and the other is to send a list of acceptable values up to the webservice and have the webservice refuse to send down values that aren't in that list. Both of those suck, and I'm hoping someone will come up with a better answer than what I've got so far. If not, I'll post my own janky answer tomorrow. – Chris Berger Nov 07 '17 at 04:37
  • Well, I did end up overriding GetReaderForMessage, but did slightly better than Regex matching on the entire message string for enum names... Still hoping for a better way to override the deserialization of enums. – Chris Berger Nov 08 '17 at 23:03
  • The whole point of a WSDL and XSD's is to have a contract between a service and a client and you are implementing something to break this contract. Shouldn't you reconsider a different implementation? Or change the WSDL so you allow anything instead of an enum. – Wesley De Keirsmaeker Nov 10 '17 at 16:47
  • How is it breaking the contract? The WSDL will change, but compiled code can't dynamically update its wsdl, at least not with .NET's implementation of consuming web services. I can't have every change to a wsdl be a breaking change, that would be chaos. This is just a way to decouple webservice code from client code, and to decouple code from data while still allowing design-time intellisense and constraints. The latter is what enums are for. – Chris Berger Nov 10 '17 at 17:33
  • What if you try to replace Enum with just an Integer? And let the client decide what to do with it? (translate it to enum or not) – madoxdev Nov 14 '17 at 09:55

2 Answers2

0

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.

Chris Berger
  • 557
  • 4
  • 14
0

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.

batwad
  • 3,588
  • 1
  • 24
  • 38
  • This is not a useful answer. There is are good reasons to have constraints on data, and I've been searching for years for the best answer between lookup tables, enums, string values, and magic numbers. Alone, none of them quite works. I finally have a way to link together lookup tables and enums in a way that provides Intellisense, data constraints, discoverability, and extensibility, I just need XML Deserialization to not throw a fit and break the entire program when it comes to values it doesn't understand. I have figured out a way to do so, but I'm looking for something more elegant. – Chris Berger Nov 13 '17 at 02:55
  • Lookup tables are not enums. "Dynamic enums" are not enums. A service contract is not the same as an interface. The subtle inconsistencies in your design are the reason you've been banging your head against a wall. Right now you have a hammer and everything looks like a nail. Try using a screwdriver instead. – batwad Nov 14 '17 at 06:33
  • Lookup tables, enums, "stringly-typed" variables, and magic numbers all have the same end goal, which is representing choosable values. None of them accomplishes the goal perfectly on its own. Nobody seems to have an implementation that includes constraints, extensibility, portability, separation of concerns, and discoverability all at the same time. It is a difficult problem, which is why I am struggling with it - it's not because I'm "trying to hammer in a screw." The only problem I have now is overriding the deserialization code, which shouldn't be this difficult. – Chris Berger Nov 14 '17 at 15:58
  • If, rather than having a solution for overriding the deserialization code, you have a different solution that you use for achieving the goals that I want for enumerable values, then feel free to post THAT, rather than "what you want to do is stupid." – Chris Berger Nov 14 '17 at 16:00
  • I'm pretty sure I didn't call you stupid, but I'm sorry if it came across that way. Sometimes you can be so engrossed that you can't see the wood for the trees; I was trying to encourage you to step back and reassess if you're doing the right thing. Lookup tables are configuration data. Enums are code. This fundamental distinction is the root of your problem because you are conflating them. I hope to get the time to elaborate further. – batwad Nov 15 '17 at 16:55
  • If your elaboration is going to be another reason why I shouldn't want to do what I want to do, then I fear it would be a waste of your time. If you have another method that achieves the goals I want (part of which is that the values are not stored as magic numbers on either side of the data - i.e. I can get meaningful values in a query and also when checking values in code, and also that constraints can be checked at compile time, not at runtime), then I am all ears. – Chris Berger Nov 15 '17 at 17:03
  • Very few things are impossible in coding, just hard. (Or in some cases NP-Hard.) – Chris Berger Nov 16 '17 at 16:07