RESTful WCF服务中标志枚举的默认值

时间:2016-01-22 17:54:20

标签: c# web-services wcf rest enums

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);
}

然后可以通过以下网址请求资源:

GET /resource?options=None

GET /resource?options=MyOption1

GET /resource?options=MyOption1,MyOption3

只要URL实际包含options参数的值,所有这一切都能很好地工作。但是,如果我在不指定URL中的值的情况下请求资源,请执行以下操作:

GET /resource

我收到一条消息值不能为空的例外。\ 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)

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

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

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

我已经尝试过使用自定义TypeConverter实现,但即便如此也不行。查看QueryStringConverter的实现情况,似乎总是会尝试自行转换enum类型。

1 个答案:

答案 0 :(得分:3)

好的,我找到了一个可重用的解决方案,并且不涉及为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.
        }
    }
}