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
类型。
答案 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.
}
}
}