【问题标题】:Default value for flags enum in a RESTful WCF serviceRESTful WCF 服务中标志枚举的默认值
【发布时间】:2016-01-22 17:54:20
【问题描述】:

WCF 支持使用带有FlagsAttribute 标记的枚举类型作为UriTemplate 中的参数。像这样:

[DataContract]
[Flags]
public enum OptionsEnum
{
    [EnumMember]
    None = 0,
    [EnumMember]
    MyOption1 = 1,
    [EnumMember]
    MyOption2 = 2,
    [EnumMember]
    MyOption3 = 4,
    [EnumMember]
    MyOption4 = 8
}

[ServiceContract]
public interface MyServiceContract
{
    [OperationContract]
    [WebInvoke(Method = "GET", UriTemplate = "resource?options={options}")]
    void MyOperation(OptionsEnum options);
}

然后可以通过如下 URL 请求资源:

GET /resource?options=None

GET /resource?options=MyOption1

GET /resource?options=MyOption1,MyOption3

只要 URL 实际上包含 options 参数的值,所有这些都可以很好地工作。但是,如果我在请求资源时未在 URL 中指定值,如下所示:

GET /resource

我收到一条异常消息 Value cannot be null。\r\nParameter name: value 以及以下堆栈跟踪:

at System.Enum.TryParseEnum(Type enumType, String value, Boolean ignoreCase, EnumResult& parseResult)
at System.Enum.Parse(Type enumType, String value, Boolean ignoreCase)
at System.ServiceModel.Dispatcher.QueryStringConverter.ConvertStringToValue(String parameter, Type parameterType)
at System.ServiceModel.Dispatcher.UriTemplateDispatchFormatter.DeserializeRequest(Message message, Object[] parameters)
at System.ServiceModel.Dispatcher.DispatchOperationRuntime.DeserializeInputs(MessageRpc& rpc)
at System.ServiceModel.Dispatcher.DispatchOperationRuntime.InvokeBegin(MessageRpc& rpc)
at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage5(MessageRpc& rpc)
at System.ServiceModel.Dispatcher.ImmutableDispatchRuntime.ProcessMessage31(MessageRpc& rpc)
at System.ServiceModel.Dispatcher.MessageRpc.Process(Boolean isOperationContextSet)

显然,这是因为在这种情况下QueryStringConverternull 传递到Enum.Parse(...)。因此MyServiceContract的实现将不会被执行。

当然,对于options 参数的类型,我当然可以切换到string,并在服务实现中自己完成所有解析工作,但这并不是我想要的,真的。

如果 URL 不包含值(就像为 int 类型的省略参数传递 0 一样),是否有人知道将 OptionsEnum.None 传递到服务实现的干净解决方案?

我已经尝试过使用自定义的TypeConverter 实现,但即使这样似乎也不起作用。查看QueryStringConverter 的实现,它似乎总是会尝试自行转换enum 类型。

【问题讨论】:

    标签: c# web-services wcf rest enums


    【解决方案1】:

    好的,我找到了一个可重用的解决方案,并且不涉及为 flags 参数的类型切换到string。不过,我希望有更简单的东西。无论如何,万一它对别人有帮助,就在这里。

    方法比较简单:

    • 将枚举包装在引用类型中。
    • 使用TypeConverter 自定义将值从string 转换为我们的标志枚举的过程,如WCF 的QueryStringConverter 实现的那样。
    /// <summary>
    /// Wraps a flags enum value.
    /// </summary>
    /// <remarks>
    /// This class is meant to be used in conjunction with 
    /// <see cref="FlagsConverter{TFlags,TEnum}"/> and simply boxes an enum.
    /// This is necessary in order to customize WCF's default behavior for enum
    /// types (as implemented by <see cref="QueryStringConverter"/>) by using a
    /// <see cref="TypeConverter"/>.
    /// </remarks>
    /// <devdoc>
    /// We prefer this over using an 1-Tuple (<see cref="Tuple{T1} "/>) as it
    /// allows us to add constraints on the type parameter. Also, the value 
    /// wrapped by a <see cref="Tuple{T1} "/> is read-only, which we don't want
    /// here, as there is no reason to prevent [OperationContract] methods from
    /// writing the wrapped <see cref="Value"/>.
    /// </devdoc>
    /// <typeparam name="TEnum">
    /// The enum type. Must be attributed with <see cref="FlagsAttribute"/>.
    /// </typeparam>
    public abstract class Flags<TEnum>
            where TEnum : struct, IConvertible
    {
        // Use a static c'tor to add run-time checks on the type parameter that
        // cannot be checked at compile-time via constraints.
        static Flags()
        {
            if (!typeof(TEnum).IsEnum)
            {
                throw new InvalidOperationException("T is not an enum");
            }
    
            if (!typeof(TEnum).IsDefined(typeof(FlagsAttribute), false))
            {
                throw new InvalidOperationException("T is not a flags enum");
            }
        }
    
        /// <summary>
        /// The enum value.
        /// </summary>
        public TEnum Value
        {
            get;
            set;
        }
    }
    
    /// <summary>
    /// A <see cref="TypeConverter"/> implementation that converts from
    /// <c>string</c> to <see cref="Flags{TEnum}"/>.
    /// </summary>
    /// <remarks>
    /// <para>
    /// Meant to be used in conjunction with <see cref="Flags{TEnum}"/>.
    /// The purpose of this <see cref="TypeConverter"/> implementation is to
    /// convert both <c>null</c> and the empty string to <c>default(TEnum)</c>
    /// instead of throwing an exception. This way, a flags enum (wrapped in an
    /// instance of <see cref="Flags{TEnum}"/>) can be used as the type of an
    /// optional parameter in a RESTful WCF <c>OperationContract</c> method.
    /// </para>
    /// <para>
    /// If the string value (as provided by <see cref="QueryStringConverter"/>)
    /// is <c>null</c> or empty or can be successfully parsed into an enum 
    /// value of type <typeparamref name="TEnum"/>, this implementation will
    /// provide an instance of <typeparamref name="TFlags"/>. Thus, there is no
    /// need to check a <typeparamref name="TFlags"/>-typed value for 
    /// <c>null</c> within the implementation of an <c>OperationContract</c>
    /// method.
    /// </para>
    /// </remarks>
    /// <typeparam name="TFlags">
    /// A sub-class of <see cref="Flags{TEnum}"/>. Must have a default c'tor.
    /// </typeparam>
    /// <typeparam name="TEnum">
    /// The enum type. Must be attributed with <see cref="FlagsAttribute"/>.
    /// </typeparam>
    public class FlagsConverter<TFlags, TEnum> : TypeConverter
        where TFlags : Flags<TEnum>, new()
        where TEnum : struct, IConvertible
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return sourceType == typeof(string);
        }
    
        public override object ConvertFrom(ITypeDescriptorContext context,
            CultureInfo culture, object value)
        {
            if (string.IsNullOrEmpty((string)value))
            {
                // The following line is what Flags<> and FlagsConverter<,> is
                // all about: Provide the enum's default value (meaning no flag
                // is set) for null and the empty string instead of passing it
                // into Enum.Parse() (which results in an ArgumentException).
                return new TFlags { Value = default(TEnum) };
            }
    
            // Otherwise, just pass the value on the Enum.Parse(), i.e. don't 
            // add any further changes to WCF's default behavior as implemented
            // by QueryStringConverter.
            return new TFlags { Value = (TEnum)Enum.Parse(typeof(TEnum), (string)value, true) };
        }
    }
    

    这两个类现在可以用来解决这样的问题 (重用原始问题中的示例):

    [DataContract]
    [Flags]
    public enum OptionsEnum
    {
        [EnumMember]
        None = 0,
        [EnumMember]
        MyOption1 = 1,
        [EnumMember]
        MyOption2 = 2,
        [EnumMember]
        MyOption3 = 4,
        [EnumMember]
        MyOption4 = 8
    }
    
    [DataContract]
    [TypeConverter(typeof(FlagsConverter<MyOptionalFlags, OptionsEnum>))]
    public class MyOptionalFlags
        : Flags<OptionsEnum>
    {
        // We don't add anything here, as the whole purpose of this class is to
        // wrap the OptionsEnum in a class that will be instantiated by the
        // attributed FlagsConverter.
    }
    
    [ServiceContract]
    public interface MyServiceContract
    {
        [OperationContract]
        [WebInvoke(Method = "GET", UriTemplate = "resource?options={options}")]
        void MyOperation(MyOptionalFlags options);
    }
    
    public class MyServiceContractImpl
        : MyServiceContract
    {
        public void MyOperation(MyOptionalFlags options)
        {
            if (options.Value == OptionsEnum.None)
            {
                // We will now get here for requests that do not specify a 
                // value for the options parameter in the URL. Note that just 
                // like for an enum value, we don't have to check if options is
                // null here.
            }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2012-06-14
      • 2011-01-03
      • 2011-10-27
      • 2016-10-10
      • 1970-01-01
      • 2015-08-22
      • 2012-05-10
      相关资源
      最近更新 更多