繁体   English   中英

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

[英]Default value for flags enum in a RESTful WCF service

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不能为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的实现。

当然,我可以切换到string的类型options参数和服务实现内完成所有的解析自己的东西,但是这不是我想要的,真的。

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM