-1

I'm creating a WCf webservice and I wonder if it's possible to set an custom error message if deserialization of a an enum DataMember fail?

I have an enum like this

    [DataContract]
    public enum VehicleBrand
    {
        [EnumMember]
        BMW = 0,
        [EnumMember]
        Honda = 1,
        [EnumMember]
        Tesla = 2
    }

And my DataContract:

    [DataContract]
    public class MyCar
    {
        [DataMember(IsRequired = true, Order = 1)]
        [Required(ErrorMessage = "Car brand is required.")]
        [EnumDataType(typeof(VehicleBrand), ErrorMessage = "Car brand is required.")]
        public VehicleBrand CarBrand { get; set; }
    }

If for example someone calls my webservice through SoapUI and put "Mercedes" as a value for the CarBrand, he will get some error message saying that it's not possible to deserialize CarBrand... Is it possible to customize this error message to say for example that the "CarBrand should be BMW, Honda or Tesla"?

I'm using using DevTrends.WCFDataAnnotations.ValidateDataAnnotationsBehavior to validate DataContracts for WCF.

Thanks

Wadjey
  • 145
  • 13

2 Answers2

1

By default you are getting a Deserialization error when you are submitting a request that has invalid data. The fault returned contains a message like

<Message>Invalid enum value 'Banana' cannot be deserialized into type 'WcfLibrary.DataType'. Ensure that the necessary enum values are present and are marked with EnumMemberAttribute attribute if the type has DataContractAttribute attribute.</Message>

Because you defined the enum in the data contract it is also added to the wsdl as a simpletype with a restriction of all the possible entries that you've added.

<xs:simpleType name="DataType">
    <xs:restriction base="xs:string">
        <xs:enumeration value="Car"/>
        <xs:enumeration value="Bike"/>
        <xs:enumeration value="Truck"/>
    </xs:restriction>
</xs:simpleType>
<xs:element name="DataType" type="tns:DataType" nillable="true"/>

In other words you can validate the incomming message at the source against the wsdl before sending it to the server.

If you want to be in control of the fault message I think you need to change the data contract so that you accept a string instead of the enum. Then try to match the string to the enum and when that fails you return a custom fault message.

martijn
  • 485
  • 2
  • 9
1

You can use an IErrorHandler to accomplish that:

Client.cs:

  class Program
    {
        [DataContract]
        public enum MyDCServer
        {
            [EnumMember]
            Test = 0
        }
        [ServiceContract]
        public interface ITestServer
        {
            [OperationContract]
           [FaultContract(typeof(string))]
            MyDCServer EchoDC(MyDCServer input);
        }
        static Binding GetBinding()
        {
            BasicHttpBinding result = new BasicHttpBinding();
            return result;
        }
        static void Main(string[] args)
        {
            string baseAddress = "http://localhost:8000/Service";
            ChannelFactory<ITestServer> factory = new ChannelFactory<ITestServer>(GetBinding(), new EndpointAddress(baseAddress));
            ITestServer proxy = factory.CreateChannel();
            MyDCServer uu = proxy.EchoDC(MyDCServer.Test);
            Console.WriteLine(uu);
            Console.ReadKey();
        }
    }

The value transmitted by the client is Test.

Server.cs:

 public class Post
    {
        [DataContract]
        public enum MyDCServer
        {
            [EnumMember]
            BMW = 0,
            [EnumMember]
            Honda = 1,
            [EnumMember]
            Tesla = 2
        }
        [ServiceContract]
        public interface ITestServer
        {
            [OperationContract]
           [FaultContract(typeof(string), Action = Service.FaultAction)]
            MyDCServer EchoDC(MyDCServer input);
        }
     
        public class Service : ITestServer
        {
            public const string FaultAction = "http://my.fault/serializationError";
            public MyDCServer EchoDC(MyDCServer input)
            {
                Console.WriteLine(input);

                return input;
            }
        }
        public class MyErrorHandler : IErrorHandler
        {
            public bool HandleError(Exception error)
            {
                return error is FaultException && (error.InnerException as SerializationException != null);
            }

            public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
            {
                if (error is FaultException)
                {
                    SerializationException serException = error.InnerException as SerializationException;
                    if (serException != null)
                    {
                        string detail = String.Format("{0}: {1}", serException.GetType().FullName, serException.Message);
                        FaultException<string> faultException = new FaultException<string>(detail, new FaultReason("CarBrand should be BMW, Honda or Tesla"));
                        MessageFault messageFault = faultException.CreateMessageFault();
                        fault = Message.CreateMessage(version, messageFault, Service.FaultAction);
                    }
                }
            }
        }
        public class MyServiceBehavior : IServiceBehavior
        {
            public void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
            {
            }
            public void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
            {
                foreach (ChannelDispatcher disp in serviceHostBase.ChannelDispatchers)
                {
                    disp.ErrorHandlers.Add(new MyErrorHandler());
                }
            }
            public void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
            {
                foreach (ServiceEndpoint endpoint in serviceDescription.Endpoints)
                {
                    if (endpoint.Contract.ContractType == typeof(IMetadataExchange)) continue;
                    foreach (OperationDescription operation in endpoint.Contract.Operations)
                    {
                        FaultDescription expectedFault = operation.Faults.Find(Service.FaultAction);
                        if (expectedFault == null || expectedFault.DetailType != typeof(string))
                        {
                            throw new InvalidOperationException("Operation must have FaultContract(typeof(string))");
                        }
                    }
                }
            }
        }
        static Binding GetBinding()
        {
            BasicHttpBinding result = new BasicHttpBinding();
            return result;
        }
        static void Main(string[] args)
        {
            string baseAddress = "http://localhost:8000/Service";
            ServiceHost host = new ServiceHost(typeof(Service), new Uri(baseAddress));
            host.AddServiceEndpoint(typeof(ITestServer), GetBinding(), "");
            host.Description.Behaviors.Add(new MyServiceBehavior());
            host.Open();
            Console.WriteLine("Host opened");
            Console.ReadLine();
            host.Close();
        }
    }

And there is no Test in the enumeration type of the server.

When the client calls, it will get the following exception information:

enter image description here

The code above is referenced from this link;

UPDATE:

We can use serException.TargetSite.Name to determine whether an enumeration type serialization exception has occurred:

if (serException != null&& serException.TargetSite.Name== "ReadEnumValue")
                        {
                            string detail = String.Format("{0}: {1}", serException.GetType().FullName, serException.Message);
                            FaultException<string> faultException = new FaultException<string>(detail, new FaultReason("CarBrand should be BMW, Honda or Tesla"));
                            MessageFault messageFault = faultException.CreateMessageFault();
                            fault = Message.CreateMessage(version, messageFault, Service.FaultAction);
                        }

For how to apply MyServiceBehavior in the configuration file, you can refer to this link.

You can also use Attribute to apply MyServiceBehavior to the service:

public class MyServiceBehavior : Attribute,IServiceBehavior
            {...

enter image description here

Ding Peng
  • 3,702
  • 1
  • 5
  • 8
  • This custom fault handler will be used for all deserialization errors regardless if it is related to the enum. You need to find a way to identify is the error is related to the enum and only then send the custom fault. – martijn Nov 27 '20 at 08:49
  • @ding-peng Thanks for your help, do have any idea how to limit this custom fault for enums? Also do you know how to add _MyServiceBehavior_ directly to the web.config (without the `Main(string[] args)`) ? – Wadjey Nov 27 '20 at 19:52
  • 1
    This is still valid for all the enum's in the message and can cause confusion. you need to make the fault more generic and find the possible values by getting the enum type. the comment box is to small to give an example. But I'm still convinced that this is something that should be handled on the client side. The data contract and/or the wsdl is giving you the correct values so this can be checked before invoking the server. – martijn Nov 30 '20 at 07:58
  • @DingPeng Thanks for updating your reply, I'm able to test your code and it's showing the expected message, but is it possible to make it more generic to be able to show a specific message for each enum? for example if I have an enum VehicleBrand and an enum ClothBrand, I want to be able to show a different fault message for each of these enums. Do you know if it's possible to do that? – Wadjey Dec 01 '20 at 22:16
  • I've added a suggestion to the solution @DingPeng provided. But it needs to be approved before it becomes visible. – martijn Dec 04 '20 at 10:35