By default all the data member values in a parameter object to wcf service will be null. But how to check if actual null value was passed from the client to my service.
In otherwords if the client actually passed any values including null values to datamembers then I have to do some DB operations. So I need to distinguish between default null values and actual null values passed by client. Please advice.
I'm not sure this is what you asked but you can implement something like this in order to null check.
private bool HasNull(object webServiceInput, string[] optionalParameters = null)
{
if (ReferenceEquals(null, webServiceInput))
return false;
if (optionalParameters == null)
optionalParameters = new string[0];
var binding = BindingFlags.Instance | BindingFlags.Public;
var properties = webServiceInput.GetType().GetProperties(binding);
foreach (var property in properties)
{
if (!property.CanRead)
continue;
if (property.PropertyType.IsValueType)
continue;
if (optionalParameters.Contains(property.Name))
continue;
var value = property.GetValue(webServiceInput);
if (ReferenceEquals(null, value))
return false;
}
return true;
}
I think the only solution is to have extra data members following this pattern:
class Contract
{
[DataMember]
private string _field;
public string Field
{
get {
return _field;
}
set {
_field = value;
FieldSpecified = true;
}
}
[DataMember]
public string FieldSpecified;
}
This is the pattern that XML serialization uses.
Related
I have a class named VerseRangeReference that has the properties Chapter, FirstVerse and LastVerse.
I have decorated it with a TypeConverterAttribute [TypeConverter(typeof(VerseRangeReferenceConverter))]
I have an action on a controller like this
public Task<ViewResult> Verses(VerseRangeReference[] verses)
But the value of verses is always a single element with the value null. Here is my type converter
public class VerseRangeReferenceConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (value == null)
throw new ArgumentNullException(nameof(value));
if (value.GetType() == typeof(string))
{
string source = (string)value;
return VerseRangeReference.ParseMultiple(source);
}
return null;
}
}
The result of VerseRangeReference.ParseMultiple(source) is a valid array of instances of VerseRange.
I had to implement a custom model binder. If someone can think of a way to do this with a TypeConverter then I will accept that answer instead because model binders are more complicated.
public class VerseRangeReferenceArrayModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
string modelName = bindingContext.ModelName;
ValueProviderResult valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult != ValueProviderResult.None)
{
VerseRangeReference[] verseRangeReferences = VerseRangeReference.ParseMultiple(valueProviderResult.FirstValue);
bindingContext.Result = ModelBindingResult.Success(verseRangeReferences);
}
return Task.CompletedTask;
}
}
public class VerseRangerReferenceArrayModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(VerseRangeReference[]))
return new BinderTypeModelBinder(typeof(VerseRangeReferenceArrayModelBinder));
return null;
}
}
This must be registered.
services.AddMvc(options =>
{
options.ModelBinderProviders.Insert(0, new VerseRangerReferenceArrayModelBinderProvider());
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
You can use a type converter to bind a comma separated string to a sequence of values. However, the type converter should convert from the string to the sequence directly. This means that the type converter should be configured for something like IEnumerable<T> or T[]. To simplify matters I will continue my explanation for IEnumerable<int> but if you want to use arrays instead you should just make sure that the type converter converts to an array instead of something that implements IEnumerable<T>.
You can configure a type converter for IEnumerable<int> using TypeDescriptor.AddAttributes:
TypeDescriptor.AddAttributes(
typeof(IEnumerable<int>),
new TypeConverterAttribute(typeof(EnumerableIntTypeConverter)));
This configures EnumerableIntTypeConverter as a type converter that can convert IEnumerable<int>.
This call has to be made when the process starts and in the case of ASP.NET Core this can conveniently be done in the Startup.Configure method.
Here is the EnumerableIntTypeConverter that converts the comma separated string of numbers to a list of ints:
internal class EnumerableIntTypeConverter : TypeConverter
{
private const char Separator = ',';
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
=> sourceType == typeof(string);
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
if (!(value is string #string))
throw new NotSupportedException($"{GetType().Name} cannot convert from {(value != null ? value.GetType().FullName : "(null)")}.");
if (#string.Length == 0)
return Enumerable.Empty<int>();
var numbers = new List<int>();
var start = 0;
var end = GetEnd(#string, start);
while (true)
{
if (!int.TryParse(
#string.AsSpan(start, end - start),
NumberStyles.AllowLeadingSign,
culture,
out var number))
throw new FormatException($"{GetType().Name} cannot parse string with invalid format.");
numbers.Add(number);
if (end == #string.Length)
break;
start = end + 1;
end = GetEnd(#string, start);
}
return numbers;
}
private static int GetEnd(string #string, int start)
{
var end = #string.IndexOf(Separator, start);
return end >= 0 ? end : #string.Length;
}
}
The parsing uses System.Memory to avoid allocating a new string for each number in the list. If your framework doesn't have the int.TryParse overload that accepts a Span<char> you can use string.Substring instead.
I have a custom model binder, being used in a REST API, which looks as follows:
public class CustomQueryModelBinder : IModelBinder
{
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
if (!String.IsNullOrWhiteSpace(bindingContext.ModelName) && bindingContext.ModelType == typeof(short) && bindingContext.ValueProvider.GetValue(bindingContext.ModelName) != null)
{
short value;
var val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).FirstValue as string;
if (String.IsNullOrWhiteSpace(val))
{
return ModelBindingResult.SuccessAsync(bindingContext.ModelName, val);
}
else if (Int16.TryParse(val, out value) && value >= 0)
{
return ModelBindingResult.SuccessAsync(bindingContext.ModelName, value);
}
else
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, "The value is invalid.");
}
}
return ModelBindingResult.FailedAsync(bindingContext.ModelName);
}
}
And in cases where the custom value is not specified in the URI it should default to a valid value (greater than 0) however it is always defaulting to 0, even though the controller looks as follows:
public async Task<IActionResult> GetAsync(
[ModelBinder(BinderType = typeof(CustomQueryModelBinder))]short value = 100,
Basically value here should be getting set to 100 as its default value when it returns as null from the ModelBinder.
However this is not happening and it is constantly being returned as 0 which is resulting in System.ArgumentOutOfRangeException when trying to do a Get.
We are using RC1.
Replacing short value = 100 with short? value = 100 seems to have worked for me. Hooray for nullable types.
I have my web api and I added the web api help pages to auto-generate my documentation. It's working great for methods where my parameters are listed out, but I have a method like this:
public SessionResult PostLogin(CreateSessionCommand request)
And, on my help page, it is only listing the command parameter in the properties section. However, in the sample request section, it lists out all of the properties of my CreateSessionCommand class.
Parameters
Name | Description | Additional information
request | No documentation available. | Define this parameter in the request body.
I would like it instead to list all of the properties in my CreateSessionCommand class. Is there an easy way to do this?
So, I managed to devise a workaround for this problem, in case anyone is interested.
In HelpPageConfigurationExtensions.cs I added the following extension method:
public static void AlterApiDescription(this ApiDescription apiDescription, HttpConfiguration config)
{
var docProvider = config.Services.GetDocumentationProvider();
var addParams = new List<ApiParameterDescription>();
var removeParams = new List<ApiParameterDescription>();
foreach (var param in apiDescription.ParameterDescriptions)
{
var type = param.ParameterDescriptor.ParameterType;
//string is some special case that is not a primitive type
//also, compare by full name because the type returned does not seem to match the types generated by typeof
bool isPrimitive = type.IsPrimitive || String.Compare(type.FullName, typeof(string).FullName) == 0;
if (!isPrimitive)
{
var properties = from p in param.ParameterDescriptor.ParameterType.GetProperties()
let s = p.SetMethod
where s.IsPublic
select p;
foreach (var property in properties)
{
var documentation = docProvider.GetDocumentation(new System.Web.Http.Controllers.ReflectedHttpParameterDescriptor()
{
ActionDescriptor = param.ParameterDescriptor.ActionDescriptor,
ParameterInfo = new CustomParameterInfo(property)
});
addParams.Add(new ApiParameterDescription()
{
Documentation = documentation,
Name = property.Name,
Source = ApiParameterSource.FromBody,
ParameterDescriptor = param.ParameterDescriptor
});
}
//since this is a complex type, select it to be removed from the api description
removeParams.Add(param);
}
}
//add in our new items
foreach (var item in addParams)
{
apiDescription.ParameterDescriptions.Add(item);
}
//remove the complex types
foreach (var item in removeParams)
{
apiDescription.ParameterDescriptions.Remove(item);
}
}
And here is the Parameter info instanced class I use
internal class CustomParameterInfo : ParameterInfo
{
public CustomParameterInfo(PropertyInfo prop)
{
base.NameImpl = prop.Name;
}
}
Then, we call the extension in another method inside the extensions class
public static HelpPageApiModel GetHelpPageApiModel(this HttpConfiguration config, string apiDescriptionId)
{
object model;
string modelId = ApiModelPrefix + apiDescriptionId;
if (!config.Properties.TryGetValue(modelId, out model))
{
Collection<ApiDescription> apiDescriptions = config.Services.GetApiExplorer().ApiDescriptions;
ApiDescription apiDescription = apiDescriptions.FirstOrDefault(api => String.Equals(api.GetFriendlyId(), apiDescriptionId, StringComparison.OrdinalIgnoreCase));
if (apiDescription != null)
{
apiDescription.AlterApiDescription(config);
HelpPageSampleGenerator sampleGenerator = config.GetHelpPageSampleGenerator();
model = GenerateApiModel(apiDescription, sampleGenerator);
config.Properties.TryAdd(modelId, model);
}
}
return (HelpPageApiModel)model;
}
The comments that are used for this must be added to the controller method and not the properties of the class object. This might be because my object is part of an outside library
this should go as an addition to #Josh answer. If you want not only to list properties from the model class, but also include documentation for each property, Areas/HelpPage/XmlDocumentationProvider.cs file should be modified as follows:
public virtual string GetDocumentation(HttpParameterDescriptor parameterDescriptor)
{
ReflectedHttpParameterDescriptor reflectedParameterDescriptor = parameterDescriptor as ReflectedHttpParameterDescriptor;
if (reflectedParameterDescriptor != null)
{
if (reflectedParameterDescriptor.ParameterInfo is CustomParameterInfo)
{
const string PropertyExpression = "/doc/members/member[#name='P:{0}']";
var pi = (CustomParameterInfo) reflectedParameterDescriptor.ParameterInfo;
string selectExpression = String.Format(CultureInfo.InvariantCulture, PropertyExpression, pi.Prop.DeclaringType.FullName + "." + pi.Prop.Name);
XPathNavigator methodNode = _documentNavigator.SelectSingleNode(selectExpression);
if (methodNode != null)
{
return methodNode.Value.Trim();
}
}
else
{
XPathNavigator methodNode = GetMethodNode(reflectedParameterDescriptor.ActionDescriptor);
if (methodNode != null)
{
string parameterName = reflectedParameterDescriptor.ParameterInfo.Name;
XPathNavigator parameterNode = methodNode.SelectSingleNode(String.Format(CultureInfo.InvariantCulture, ParameterExpression, parameterName));
if (parameterNode != null)
{
return parameterNode.Value.Trim();
}
}
}
}
return null;
}
and CustomParameterInfo class should keep property info as well:
internal class CustomParameterInfo : ParameterInfo
{
public PropertyInfo Prop { get; private set; }
public CustomParameterInfo(PropertyInfo prop)
{
Prop = prop;
base.NameImpl = prop.Name;
}
}
This is currently not supported out of the box. Following bug is kind of related to that:
http://aspnetwebstack.codeplex.com/workitem/877
By default, WCF deserializes missing elements into default values like null, 0 or false. The problem with this approach is that if it's a basic type like number 0 I'm not sure whether it means the real value sent by an external system or a default value generated by WCF.
So my question is: Is it possible to find out at run-time whether the default value means "I didn't send anything".
This is crucial because we can't update and overwrite existing data in the database with the default values just because the external system didn't send a particular element this time (data corruption).
Microsoft's short answer is "It is up to the receiving endpoint to appropriately interpret a missing element."
Data member default values
http://msdn.microsoft.com/en-us/library/aa347792.aspx
Can somebody please clarify what's that supposed to mean?
Thanks
If you define your data members as properties, you can use whether the setter was called or not to decide whether some value was sent. The code below shows one data contract which knows whether it deserialized its fields.
public class Post_51ca1ead_2f0a_4912_a451_374daab0101b
{
[DataContract(Name = "Person", Namespace = "")]
public class Person
{
string name;
int age;
bool nameWasSent;
bool ageWasSent;
[DataMember]
public string Name
{
get
{
return this.name;
}
set
{
this.nameWasSent = true;
this.name = value;
}
}
[DataMember]
public int Age
{
get
{
return this.age;
}
set
{
this.ageWasSent = true;
this.age = value;
}
}
[OnDeserializing]
void OnDeserializing(StreamingContext ctx)
{
this.ageWasSent = false;
this.nameWasSent = false;
}
public override string ToString()
{
return string.Format("Person[Name={0},Age={1}]",
nameWasSent ? name : "UNSPECIFIED",
ageWasSent ? age.ToString() : "UNSPECIFIED");
}
}
public static void Test()
{
MemoryStream ms = new MemoryStream();
DataContractSerializer dcs = new DataContractSerializer(typeof(Person));
dcs.WriteObject(ms, new Person { Name = "John", Age = 30 });
Console.WriteLine(Encoding.UTF8.GetString(ms.ToArray()));
string noAge = "<Person><Name>John</Name></Person>";
ms = new MemoryStream(Encoding.UTF8.GetBytes(noAge));
object p = dcs.ReadObject(ms);
Console.WriteLine("No age: {0}", p);
string noName = "<Person><Age>45</Age></Person>";
ms = new MemoryStream(Encoding.UTF8.GetBytes(noName));
p = dcs.ReadObject(ms);
Console.WriteLine("No name: {0}", p);
}
}
I'm building a data service in WCF and I'm using a combination of reflection and open types as some of the data elements need to be created on-the-fly. Most everything is working well, but I can't get filters to work with the open type values.
The error I get is:
<message xml:lang="en-US">An error occurred while processing this request.</message>
<innererror>
<message>The method or operation is not implemented.</message>
<type>System.NotImplementedException</type>
<stacktrace> at lambda_method(Closure , GeographyProvider )
at System.Linq.Enumerable.WhereEnumerableIterator`1.MoveNext()
at System.Data.Services.DataService`1.SerializeResponseBody(RequestDescription description, IDataService dataService)
at System.Data.Services.DataService`1.HandleRequest()</stacktrace>
</innererror>
I'm using an expression visitor to rewrite the LINQ expressions and it is successfully pulling the value for the open type. At this point, I'm not sure what method or operation I need to implement is. The expression tree looks like this after the expression visitor has done it's work:
Alteryx.Web.API.DatasetProvider+<GetDatasets>d__0.Where(element =>
(element.Variant == "AGSSTD_701000")).SelectMany(element =>
ConvertChecked(element.Geographies)).Where(element =>
(element.Key == "County")).SelectMany(element =>
ConvertChecked(element.Geographies)).Where(element =>
(element.Key == "36")).SelectMany(element =>
ConvertChecked(element.Geographies)).Where(it =>
Convert(((Invoke((o, name) => GetOpenValue(o, name), it, "POPCY") >= Convert(100000)) == True)))}
I've put a break point in the GetOpenValue method and it is getting called and returning the correct value. Any thoughts on where I need to go from here?
Based on Vitek's suggestions, I added checks for Convert and the comparison methods to my expression visitor, but they aren't found. Here is what my visitor code looks like:
static readonly MethodInfo GetValueOpenPropertyMethodInfo =
typeof(OpenTypeMethods)
.GetMethod(
"GetValue",
BindingFlags.Static | BindingFlags.Public,
null,
new Type[] { typeof(object), typeof(string) },
null
);
static readonly MethodInfo OpenConvertMethodInfo =
typeof(OpenTypeMethods)
.GetMethod(
"Convert",
BindingFlags.Static | BindingFlags.Public,
null,
new Type[] { typeof(object), typeof(ResourceType) },
null
);
static readonly MethodInfo GreaterThanOrEqualMethodInfo =
typeof(OpenTypeMethods)
.GetMethod(
"GreaterThanOrEqual",
BindingFlags.Static | BindingFlags.Public,
null,
new Type[] { typeof(object), typeof(object) },
null
);
static readonly MethodInfo EqualMethodInfo =
typeof(OpenTypeMethods)
.GetMethod(
"Equal",
BindingFlags.Static | BindingFlags.Public,
null,
new Type[] { typeof(object), typeof(object) },
null
);
static readonly Expression<Func<object, string, object>> GetValueOpenReplacement =
(o, name) => GetOpenValue(o, name);
static object GetOpenValue(object o, string name)
{
return (o as OpenDataProvider).GetValue(name);
}
static readonly Expression<Func<object, object, object>> GetGreaterThanOrEqualReplacement =
(left, right) => GetOpenGreaterThanOrEqual(left, right);
static object GetOpenGreaterThanOrEqual(object left, object right)
{
string s = left.ToString();
return true;
}
static readonly Expression<Func<object, object, object>> GetEqualReplacement =
(left, right) => GetOpenEqual(left, right);
static object GetOpenEqual(object left, object right)
{
string s = left.ToString();
return true;
}
protected override Expression VisitMethodCall(
MethodCallExpression node
)
{
if (node.Method == GetValueOpenPropertyMethodInfo)
{
// Arguments[0] - the resource to get property from
// Arguments[1] - the ResourceProperty to get
// Invoke the replacement expression, passing the
// appropriate parameters.
if (node.Arguments[0].Type.BaseType == typeof(OpenDataProvider))
{
OpenDataProvider.RequestValue(((ConstantExpression)node.Arguments[1]).Value.ToString());
}
return Expression.Invoke(
Expression.Quote(GetValueOpenReplacement),
node.Arguments[0],
node.Arguments[1]
);
}
else if (node.Method == OpenConvertMethodInfo)
{
// Arguments[0] – the resource
// Arguments[1] – the ResourceType
// no need to do anything, so just
// return the argument
return this.Visit(node.Arguments[0]);
}
else if (node.Method == GreaterThanOrEqualMethodInfo)
{
// Invoke the replacement expression, passing the
// appropriate parameters.
return Expression.Invoke(
Expression.Quote(GetGreaterThanOrEqualReplacement),
node.Arguments[0],
node.Arguments[1]
);
}
else if (node.Method == EqualMethodInfo)
{
// Invoke the replacement expression, passing the
// appropriate parameters.
return Expression.Invoke(
Expression.Quote(GetEqualReplacement),
node.Arguments[0],
node.Arguments[1]
);
}
return base.VisitMethodCall(node);
}
I've put breakpoints in all of the if blocks in the VisitMethodCall method, but only the GetValueOpenProperty block is ever called.
Thanks!
Vitek kindly provided the answer to this here: http://social.msdn.microsoft.com/Forums/en-US/adodotnetdataservices/thread/bfb62cf5-48cc-4435-ae9a-76e4a13d762a
To summarize, the ExpressionVisitor needs to overide the OpenTypeMethods in the VisitBinary method.
Thanks for the help, Vitek!