|
//---------------------------------------------------------------------
// <copyright file="AtomMaterializer.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// <summary>
// Provides a class that can materialize ATOM entries into typed
// objects, while maintaining a log of changes done.
// </summary>
//---------------------------------------------------------------------
namespace System.Data.Services.Client
{
#region Namespaces.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Services.Common;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Xml;
using System.Xml.Linq;
#endregion Namespaces.
/// <summary>
/// Use this class to invoke projection methods from AtomMaterializer.
/// </summary>
internal static class AtomMaterializerInvoker
{
/// <summary>Enumerates casting each element to a type.</summary>
/// <typeparam name="T">Element type to enumerate over.</typeparam>
/// <param name="source">Element source.</param>
/// <returns>
/// An IEnumerable<T> that iterates over the specified <paramref name="source"/>.
/// </returns>
/// <remarks>
/// This method should be unnecessary with .NET 4.0 covariance support.
/// </remarks>
internal static IEnumerable<T> EnumerateAsElementType<T>(IEnumerable source)
{
return AtomMaterializer.EnumerateAsElementType<T>(source);
}
/// <summary>Creates a list to a target element type.</summary>
/// <param name="materializer">Materializer used to flow link tracking.</param>
/// <typeparam name="T">Element type to enumerate over.</typeparam>
/// <typeparam name="TTarget">Element type for list.</typeparam>
/// <param name="source">Element source.</param>
/// <returns>
/// An IEnumerable<T> that iterates over the specified <paramref name="source"/>.
/// </returns>
/// <remarks>
/// This method should be unnecessary with .NET 4.0 covariance support.
/// </remarks>
internal static List<TTarget> ListAsElementType<T, TTarget>(object materializer, IEnumerable<T> source) where T : TTarget
{
Debug.Assert(materializer.GetType() == typeof(AtomMaterializer), "materializer.GetType() == typeof(AtomMaterializer)");
return AtomMaterializer.ListAsElementType<T, TTarget>((AtomMaterializer)materializer, source);
}
/// <summary>Checks whether the entity on the specified <paramref name="path"/> is null.</summary>
/// <param name="entry">Root entry for paths.</param>
/// <param name="expectedType">Expected type for <paramref name="entry"/>.</param>
/// <param name="path">Path to pull value for.</param>
/// <returns>Whether the specified <paramref name="path"/> is null.</returns>
/// <remarks>
/// This method will not instantiate entity types on the path.
/// </remarks>
internal static bool ProjectionCheckValueForPathIsNull(
object entry,
Type expectedType,
object path)
{
Debug.Assert(entry.GetType() == typeof(AtomEntry), "entry.GetType() == typeof(AtomEntry)");
Debug.Assert(path.GetType() == typeof(ProjectionPath), "path.GetType() == typeof(ProjectionPath)");
return AtomMaterializer.ProjectionCheckValueForPathIsNull((AtomEntry)entry, expectedType, (ProjectionPath)path);
}
/// <summary>Provides support for Select invocations for projections.</summary>
/// <param name="materializer">Materializer under which projection is taking place.</param>
/// <param name="entry">Root entry for paths.</param>
/// <param name="expectedType">Expected type for <paramref name="entry"/>.</param>
/// <param name="resultType">Expected result type.</param>
/// <param name="path">Path to traverse.</param>
/// <param name="selector">Selector callback.</param>
/// <returns>An enumerable with the select results.</returns>
internal static IEnumerable ProjectionSelect(
object materializer,
object entry,
Type expectedType,
Type resultType,
object path,
Func<object, object, Type, object> selector)
{
Debug.Assert(materializer.GetType() == typeof(AtomMaterializer), "materializer.GetType() == typeof(AtomMaterializer)");
Debug.Assert(entry.GetType() == typeof(AtomEntry), "entry.GetType() == typeof(AtomEntry)");
Debug.Assert(path.GetType() == typeof(ProjectionPath), "path.GetType() == typeof(ProjectionPath)");
return AtomMaterializer.ProjectionSelect((AtomMaterializer)materializer, (AtomEntry)entry, expectedType, resultType, (ProjectionPath)path, selector);
}
/// <summary>Provides support for getting payload entries during projections.</summary>
/// <param name="entry">Entry to get sub-entry from.</param>
/// <param name="name">Name of sub-entry.</param>
/// <returns>The sub-entry (never null).</returns>
internal static AtomEntry ProjectionGetEntry(object entry, string name)
{
Debug.Assert(entry.GetType() == typeof(AtomEntry), "entry.GetType() == typeof(AtomEntry)");
return AtomMaterializer.ProjectionGetEntry((AtomEntry)entry, name);
}
/// <summary>Initializes a projection-driven entry (with a specific type and specific properties).</summary>
/// <param name="materializer">Materializer under which projection is taking place.</param>
/// <param name="entry">Root entry for paths.</param>
/// <param name="expectedType">Expected type for <paramref name="entry"/>.</param>
/// <param name="resultType">Expected result type.</param>
/// <param name="properties">Properties to materialize.</param>
/// <param name="propertyValues">Functions to get values for functions.</param>
/// <returns>The initialized entry.</returns>
internal static object ProjectionInitializeEntity(
object materializer,
object entry,
Type expectedType,
Type resultType,
string[] properties,
Func<object, object, Type, object>[] propertyValues)
{
Debug.Assert(materializer.GetType() == typeof(AtomMaterializer), "materializer.GetType() == typeof(AtomMaterializer)");
Debug.Assert(entry.GetType() == typeof(AtomEntry), "entry.GetType() == typeof(AtomEntry)");
return AtomMaterializer.ProjectionInitializeEntity((AtomMaterializer)materializer, (AtomEntry)entry, expectedType, resultType, properties, propertyValues);
}
/// <summary>Projects a simple value from the specified <paramref name="path"/>.</summary>
/// <param name="materializer">Materializer under which projection is taking place.</param>
/// <param name="entry">Root entry for paths.</param>
/// <param name="expectedType">Expected type for <paramref name="entry"/>.</param>
/// <param name="path">Path to pull value for.</param>
/// <returns>The value for the specified <paramref name="path"/>.</returns>
/// <remarks>
/// This method will not instantiate entity types, except to satisfy requests
/// for payload-driven feeds or leaf entities.
/// </remarks>
internal static object ProjectionValueForPath(object materializer, object entry, Type expectedType, object path)
{
Debug.Assert(materializer.GetType() == typeof(AtomMaterializer), "materializer.GetType() == typeof(AtomMaterializer)");
Debug.Assert(entry.GetType() == typeof(AtomEntry), "entry.GetType() == typeof(AtomEntry)");
Debug.Assert(path.GetType() == typeof(ProjectionPath), "path.GetType() == typeof(ProjectionPath)");
return AtomMaterializer.ProjectionValueForPath((AtomMaterializer)materializer, (AtomEntry)entry, expectedType, (ProjectionPath)path);
}
/// <summary>Materializes an entry with no special selection.</summary>
/// <param name="materializer">Materializer under which materialization should take place.</param>
/// <param name="entry">Entry with object to materialize.</param>
/// <param name="expectedEntryType">Expected type for the entry.</param>
/// <returns>The materialized instance.</returns>
internal static object DirectMaterializePlan(object materializer, object entry, Type expectedEntryType)
{
Debug.Assert(materializer.GetType() == typeof(AtomMaterializer), "materializer.GetType() == typeof(AtomMaterializer)");
Debug.Assert(entry.GetType() == typeof(AtomEntry), "entry.GetType() == typeof(AtomEntry)");
return AtomMaterializer.DirectMaterializePlan((AtomMaterializer)materializer, (AtomEntry)entry, expectedEntryType);
}
/// <summary>Materializes an entry without including in-lined expanded links.</summary>
/// <param name="materializer">Materializer under which materialization should take place.</param>
/// <param name="entry">Entry with object to materialize.</param>
/// <param name="expectedEntryType">Expected type for the entry.</param>
/// <returns>The materialized instance.</returns>
internal static object ShallowMaterializePlan(object materializer, object entry, Type expectedEntryType)
{
Debug.Assert(materializer.GetType() == typeof(AtomMaterializer), "materializer.GetType() == typeof(AtomMaterializer)");
Debug.Assert(entry.GetType() == typeof(AtomEntry), "entry.GetType() == typeof(AtomEntry)");
return AtomMaterializer.ShallowMaterializePlan((AtomMaterializer)materializer, (AtomEntry)entry, expectedEntryType);
}
}
/// <summary>
/// Use this class to materialize objects provided from an <see cref="AtomParser"/>.
/// </summary>
[DebuggerDisplay("AtomMaterializer {parser}")]
internal class AtomMaterializer
{
#region Private fields.
/// <summary>Associated context, used for resolving instances and types.</summary>
private readonly DataServiceContext context;
/// <summary>Expected type to materialize.</summary>
private readonly Type expectedType;
//// <summary>Whether missing properties should be ignored.</summary>
////private readonly bool ignoreMissingProperties;
/// <summary>Log into which materialization activity is recorded.</summary>
private readonly AtomMaterializerLog log;
/// <summary>Function to materialize an entry and produce a value.</summary>
private readonly ProjectionPlan materializeEntryPlan;
/// <summary>
/// Callback to be invoked when an object is materialized; receives the tag and object.
/// </summary>
private readonly Action<object, object> materializedObjectCallback;
/// <summary>Merge behavior.</summary>
private readonly MergeOption mergeOption;
/// <summary>Collection->Next Link Table for nested links</summary>
private readonly Dictionary<IEnumerable, DataServiceQueryContinuation> nextLinkTable;
/// <summary>
/// <see cref="AtomParser"/> from which content will be read.
/// </summary>
private readonly AtomParser parser;
/// <summary>Current value being materialized; possibly null.</summary>
private object currentValue;
/// <summary>Whether missing properties should be ignored.</summary>
/// <remarks>
/// This should be readonly, but we make it writeable as a
/// work-around until server projections are implemented.
/// </remarks>
private bool ignoreMissingProperties;
/// <summary>Target instance that the materializer expects to update.</summary>
private object targetInstance;
#endregion Private fields.
#region Constructors.
/// <summary>Initializes a new <see cref="AtomMaterializer"/> instance.</summary>
/// <param name="parser"><see cref="AtomParser"/> from which content will be read.</param>
/// <param name="context">Context into which instances will be materialized.</param>
/// <param name="expectedType">Expected type to materialize.</param>
/// <param name="ignoreMissingProperties">
/// Whether missing properties on the type should be ignored (i.e., the payload
/// is allowed to have properties that are not materialized and dropped instead).
/// </param>
/// <param name="mergeOption">Merge behavior.</param>
/// <param name="log">Log into which materialization activity is recorded.</param>
/// <param name="materializedObjectCallback">
/// Callback to be invoked when an object is materialized; receives the tag and object.
/// </param>
/// <param name="queryComponents">Query components describingg processing of components; possibly null.</param>
/// <param name="plan">Projection plan (if compiled in an earlier query).</param>
internal AtomMaterializer(
AtomParser parser,
DataServiceContext context,
Type expectedType,
bool ignoreMissingProperties,
MergeOption mergeOption,
AtomMaterializerLog log,
Action<object, object> materializedObjectCallback,
QueryComponents queryComponents,
ProjectionPlan plan)
{
Debug.Assert(context != null, "context != null");
Debug.Assert(parser != null, "parser != null");
Debug.Assert(log != null, "log != null");
this.context = context;
this.parser = parser;
this.expectedType = expectedType;
this.ignoreMissingProperties = ignoreMissingProperties;
this.mergeOption = mergeOption;
this.log = log;
this.materializedObjectCallback = materializedObjectCallback;
this.nextLinkTable = new Dictionary<IEnumerable, DataServiceQueryContinuation>(ReferenceEqualityComparer<IEnumerable>.Instance);
this.materializeEntryPlan = plan ?? CreatePlan(queryComponents);
}
#endregion Constructors.
#region Internal properties.
/// <summary>DataServiceContext instance this materializer is bound to.</summary>
internal DataServiceContext Context
{
get { return this.context; }
}
/// <summary>Function to materialize an entry and produce a value.</summary>
internal ProjectionPlan MaterializeEntryPlan
{
get { return this.materializeEntryPlan; }
}
/// <summary>Target instance that the materializer expects to update.</summary>
/// <remarks>
/// This property is typically null, but will be set to an existing
/// instance when POST-ing a value, so values can get merged into it.
/// </remarks>
internal object TargetInstance
{
get
{
return this.targetInstance;
}
set
{
Debug.Assert(value != null, "value != null -- otherwise we have no instance target.");
this.targetInstance = value;
}
}
/// <summary>Feed being materialized; possibly null.</summary>
internal AtomFeed CurrentFeed
{
get
{
return this.parser.CurrentFeed;
}
}
/// <summary>Entry being materialized; possibly null.</summary>
internal AtomEntry CurrentEntry
{
get
{
return this.parser.CurrentEntry;
}
}
/// <summary>Current value being materialized; possibly null.</summary>
/// <remarks>
/// This will typically be an entity if <see cref="CurrentEntry"/>
/// is assigned, but may contain a string for example if a top-level
/// primitive of type string is found.
/// </remarks>
internal object CurrentValue
{
get
{
return this.currentValue;
}
}
/// <summary>Log into which materialization activity is recorded.</summary>
internal AtomMaterializerLog Log
{
get { return this.log; }
}
/// <summary>Table storing the next links ----oicated with the current payload</summary>
internal Dictionary<IEnumerable, DataServiceQueryContinuation> NextLinkTable
{
get { return this.nextLinkTable; }
}
/// <summary>Whether we have finished processing the current data stream.</summary>
internal bool IsEndOfStream
{
get { return this.parser.DataKind == AtomDataKind.Finished; }
}
#endregion Internal properties.
#region Projection support.
/// <summary>Enumerates casting each element to a type.</summary>
/// <typeparam name="T">Element type to enumerate over.</typeparam>
/// <param name="source">Element source.</param>
/// <returns>
/// An IEnumerable<T> that iterates over the specified <paramref name="source"/>.
/// </returns>
/// <remarks>
/// This method should be unnecessary with .NET 4.0 covariance support.
/// </remarks>
internal static IEnumerable<T> EnumerateAsElementType<T>(IEnumerable source)
{
Debug.Assert(source != null, "source != null");
IEnumerable<T> typedSource = source as IEnumerable<T>;
if (typedSource != null)
{
return typedSource;
}
else
{
return EnumerateAsElementTypeInternal<T>(source);
}
}
/// <summary>Enumerates casting each element to a type.</summary>
/// <typeparam name="T">Element type to enumerate over.</typeparam>
/// <param name="source">Element source.</param>
/// <returns>
/// An IEnumerable<T> that iterates over the specified <paramref name="source"/>.
/// </returns>
/// <remarks>
/// This method should be unnecessary with .NET 4.0 covariance support.
/// </remarks>
internal static IEnumerable<T> EnumerateAsElementTypeInternal<T>(IEnumerable source)
{
Debug.Assert(source != null, "source != null");
foreach (object item in source)
{
yield return (T)item;
}
}
/// <summary>Checks whether the specified type is a DataServiceCollection type (or inherits from one).</summary>
/// <param name='type'>Type to check.</param>
/// <returns>true if the type inherits from DataServiceCollection; false otherwise.</returns>
internal static bool IsDataServiceCollection(Type type)
{
while (type != null)
{
if (type.IsGenericType && WebUtil.IsDataServiceCollectionType(type.GetGenericTypeDefinition()))
{
return true;
}
type = type.BaseType;
}
return false;
}
/// <summary>Creates a list to a target element type.</summary>
/// <param name="materializer">Materializer used to flow link tracking.</param>
/// <typeparam name="T">Element type to enumerate over.</typeparam>
/// <typeparam name="TTarget">Element type for list.</typeparam>
/// <param name="source">Element source.</param>
/// <returns>
/// An IEnumerable<T> that iterates over the specified <paramref name="source"/>.
/// </returns>
/// <remarks>
/// This method should be unnecessary with .NET 4.0 covariance support.
/// </remarks>
internal static List<TTarget> ListAsElementType<T, TTarget>(AtomMaterializer materializer, IEnumerable<T> source) where T : TTarget
{
Debug.Assert(materializer != null, "materializer != null");
Debug.Assert(source != null, "source != null");
List<TTarget> typedSource = source as List<TTarget>;
if (typedSource != null)
{
return typedSource;
}
List<TTarget> list;
IList sourceList = source as IList;
if (sourceList != null)
{
list = new List<TTarget>(sourceList.Count);
}
else
{
list = new List<TTarget>();
}
foreach (T item in source)
{
list.Add((TTarget)item);
}
// We can flow the same continuation becaues they're immutable, and
// we don't need to set the continuation property because List<T> doesn't
// have one.
DataServiceQueryContinuation continuation;
if (materializer.nextLinkTable.TryGetValue(source, out continuation))
{
materializer.nextLinkTable[list] = continuation;
}
return list;
}
/// <summary>Checks whether the entity on the specified <paramref name="path"/> is null.</summary>
/// <param name="entry">Root entry for paths.</param>
/// <param name="expectedType">Expected type for <paramref name="entry"/>.</param>
/// <param name="path">Path to pull value for.</param>
/// <returns>Whether the specified <paramref name="path"/> is null.</returns>
/// <remarks>
/// This method will not instantiate entity types on the path.
/// Note that if the target is a collection, the result is always false,
/// as the model does not allow null feeds (but instead gets an empty
/// collection, possibly with continuation tokens and such).
/// </remarks>
internal static bool ProjectionCheckValueForPathIsNull(
AtomEntry entry,
Type expectedType,
ProjectionPath path)
{
Debug.Assert(entry != null, "entry != null");
Debug.Assert(path != null, "path != null");
if (path.Count == 0 || path.Count == 1 && path[0].Member == null)
{
return entry.IsNull;
}
bool result = false;
AtomContentProperty atomProperty = null;
List<AtomContentProperty> properties = entry.DataValues;
for (int i = 0; i < path.Count; i++)
{
var segment = path[i];
if (segment.Member == null)
{
continue;
}
bool segmentIsLeaf = i == path.Count - 1;
string propertyName = segment.Member;
ClientType.ClientProperty property = ClientType.Create(expectedType).GetProperty(propertyName, false);
atomProperty = GetPropertyOrThrow(properties, propertyName);
ValidatePropertyMatch(property, atomProperty);
if (atomProperty.Feed != null)
{
Debug.Assert(segmentIsLeaf, "segmentIsLeaf -- otherwise the path generated traverses a feed, which should be disallowed");
result = false;
}
else
{
Debug.Assert(
atomProperty.Entry != null,
"atomProperty.Entry != null -- otherwise a primitive property / complex type is being rewritte with a null check; this is only supported for entities and collection");
if (segmentIsLeaf)
{
result = atomProperty.Entry.IsNull;
}
properties = atomProperty.Entry.DataValues;
entry = atomProperty.Entry;
}
expectedType = property.PropertyType;
}
return result;
}
/// <summary>Provides support for Select invocations for projections.</summary>
/// <param name="materializer">Materializer under which projection is taking place.</param>
/// <param name="entry">Root entry for paths.</param>
/// <param name="expectedType">Expected type for <paramref name="entry"/>.</param>
/// <param name="resultType">Expected result type.</param>
/// <param name="path">Path to traverse.</param>
/// <param name="selector">Selector callback.</param>
/// <returns>An enumerable with the select results.</returns>
internal static IEnumerable ProjectionSelect(
AtomMaterializer materializer,
AtomEntry entry,
Type expectedType,
Type resultType,
ProjectionPath path,
Func<object, object, Type, object> selector)
{
ClientType entryType = entry.ActualType ?? ClientType.Create(expectedType);
IEnumerable list = (IEnumerable)Util.ActivatorCreateInstance(typeof(List<>).MakeGenericType(resultType));
AtomContentProperty atomProperty = null;
ClientType.ClientProperty property = null;
for (int i = 0; i < path.Count; i++)
{
var segment = path[i];
if (segment.Member == null)
{
continue;
}
string propertyName = segment.Member;
property = entryType.GetProperty(propertyName, false);
atomProperty = GetPropertyOrThrow(entry, propertyName);
if (atomProperty.Entry != null)
{
entry = atomProperty.Entry;
entryType = ClientType.Create(property.NullablePropertyType, false);
}
}
ValidatePropertyMatch(property, atomProperty);
AtomFeed sourceFeed = atomProperty.Feed;
Debug.Assert(
sourceFeed != null,
"sourceFeed != null -- otherwise ValidatePropertyMatch should have thrown or property isn't a collection (and should be part of this plan)");
Action<object, object> addMethod = GetAddToCollectionDelegate(list.GetType());
foreach (var paramEntry in sourceFeed.Entries)
{
object projected = selector(materializer, paramEntry, property.CollectionType /* perhaps nested? */);
addMethod(list, projected);
}
ProjectionPlan plan = new ProjectionPlan();
plan.LastSegmentType = property.CollectionType;
plan.Plan = selector;
plan.ProjectedType = resultType;
materializer.FoundNextLinkForCollection(list, sourceFeed.NextLink, plan);
return list;
}
/// <summary>Provides support for getting payload entries during projections.</summary>
/// <param name="entry">Entry to get sub-entry from.</param>
/// <param name="name">Name of sub-entry.</param>
/// <returns>The sub-entry (never null).</returns>
internal static AtomEntry ProjectionGetEntry(AtomEntry entry, string name)
{
Debug.Assert(entry != null, "entry != null -- ProjectionGetEntry never returns a null entry, and top-level materialization shouldn't pass one in");
AtomContentProperty property = GetPropertyOrThrow(entry, name);
if (property.Entry == null)
{
throw new InvalidOperationException(Strings.AtomMaterializer_PropertyNotExpectedEntry(name, entry.Identity));
}
CheckEntryToAccessNotNull(property.Entry, name);
return property.Entry;
}
/// <summary>Initializes a projection-driven entry (with a specific type and specific properties).</summary>
/// <param name="materializer">Materializer under which projection is taking place.</param>
/// <param name="entry">Root entry for paths.</param>
/// <param name="expectedType">Expected type for <paramref name="entry"/>.</param>
/// <param name="resultType">Expected result type.</param>
/// <param name="properties">Properties to materialize.</param>
/// <param name="propertyValues">Functions to get values for functions.</param>
/// <returns>The initialized entry.</returns>
internal static object ProjectionInitializeEntity(
AtomMaterializer materializer,
AtomEntry entry,
Type expectedType,
Type resultType,
string[] properties,
Func<object, object, Type, object>[] propertyValues)
{
if (entry == null || entry.IsNull)
{
throw new NullReferenceException(Strings.AtomMaterializer_EntryToInitializeIsNull(resultType.FullName));
}
object result;
if (!entry.EntityHasBeenResolved)
{
AtomMaterializer.ProjectionEnsureEntryAvailableOfType(materializer, entry, resultType);
}
else if (!resultType.IsAssignableFrom(entry.ActualType.ElementType))
{
string message = Strings.AtomMaterializer_ProjectEntityTypeMismatch(
resultType.FullName,
entry.ActualType.ElementType.FullName,
entry.Identity);
throw new InvalidOperationException(message);
}
result = entry.ResolvedObject;
for (int i = 0; i < properties.Length; i++)
{
var property = entry.ActualType.GetProperty(properties[i], materializer.ignoreMissingProperties);
object value = propertyValues[i](materializer, entry, expectedType);
if (entry.ShouldUpdateFromPayload && ClientType.Create(property.NullablePropertyType, false).IsEntityType)
{
materializer.Log.SetLink(entry, property.PropertyName, value);
}
bool isEntity = property.CollectionType == null || !ClientType.CheckElementTypeIsEntity(property.CollectionType);
if (entry.ShouldUpdateFromPayload)
{
if (isEntity)
{
property.SetValue(result, value, property.PropertyName, false);
}
else
{
IEnumerable valueAsEnumerable = (IEnumerable)value;
DataServiceQueryContinuation continuation = materializer.nextLinkTable[valueAsEnumerable];
Uri nextLinkUri = continuation == null ? null : continuation.NextLinkUri;
ProjectionPlan plan = continuation == null ? null : continuation.Plan;
materializer.MergeLists(entry, property, valueAsEnumerable, nextLinkUri, plan);
}
}
else if (!isEntity)
{
materializer.FoundNextLinkForUnmodifiedCollection(property.GetValue(entry.ResolvedObject) as IEnumerable);
}
}
return result;
}
/// <summary>Projects a simple value from the specified <paramref name="path"/>.</summary>
/// <param name="materializer">Materializer under which projection is taking place.</param>
/// <param name="entry">Root entry for paths.</param>
/// <param name="expectedType">Expected type for <paramref name="entry"/>.</param>
/// <param name="path">Path to pull value for.</param>
/// <returns>The value for the specified <paramref name="path"/>.</returns>
/// <remarks>
/// This method will not instantiate entity types, except to satisfy requests
/// for payload-driven feeds or leaf entities.
/// </remarks>
internal static object ProjectionValueForPath(AtomMaterializer materializer, AtomEntry entry, Type expectedType, ProjectionPath path)
{
Debug.Assert(materializer != null, "materializer != null");
Debug.Assert(entry != null, "entry != null");
Debug.Assert(path != null, "path != null");
// An empty path indicates that we do a regular materialization.
if (path.Count == 0 || path.Count == 1 && path[0].Member == null)
{
if (!entry.EntityHasBeenResolved)
{
materializer.Materialize(entry, expectedType, /* includeLinks */ false);
}
return entry.ResolvedObject;
}
object result = null;
AtomContentProperty atomProperty = null;
List<AtomContentProperty> properties = entry.DataValues;
for (int i = 0; i < path.Count; i++)
{
var segment = path[i];
if (segment.Member == null)
{
continue;
}
bool segmentIsLeaf = i == path.Count - 1;
string propertyName = segment.Member;
// only primitive values can be mapped so we don't need to apply mappings for non-leaf segments
if (segmentIsLeaf)
{
CheckEntryToAccessNotNull(entry, propertyName);
if (!entry.EntityPropertyMappingsApplied)
{
// EPM should happen only once per entry
ClientType attributeSourceType = MaterializeAtom.GetEntryClientType(entry.TypeName, materializer.context, expectedType, false);
ApplyEntityPropertyMappings(entry, attributeSourceType);
}
}
ClientType.ClientProperty property = ClientType.Create(expectedType).GetProperty(propertyName, false);
atomProperty = GetPropertyOrThrow(properties, propertyName);
ValidatePropertyMatch(property, atomProperty);
AtomFeed feedValue = atomProperty.Feed;
if (feedValue != null)
{
Debug.Assert(segmentIsLeaf, "segmentIsLeaf -- otherwise the path generated traverses a feed, which should be disallowed");
// When we're materializing a feed as a leaf, we actually project each element.
Type collectionType = ClientType.GetImplementationType(segment.ProjectionType, typeof(ICollection<>));
if (collectionType == null)
{
collectionType = ClientType.GetImplementationType(segment.ProjectionType, typeof(IEnumerable<>));
}
Debug.Assert(
collectionType != null,
"collectionType != null -- otherwise the property should never have been recognized as a collection");
Type nestedExpectedType = collectionType.GetGenericArguments()[0];
Type feedType = segment.ProjectionType;
if (feedType.IsInterface || IsDataServiceCollection(feedType))
{
feedType = typeof(System.Collections.ObjectModel.Collection<>).MakeGenericType(nestedExpectedType);
}
IEnumerable list = (IEnumerable)Util.ActivatorCreateInstance(feedType);
MaterializeToList(materializer, list, nestedExpectedType, feedValue.Entries);
if (IsDataServiceCollection(segment.ProjectionType))
{
Type dataServiceCollectionType = WebUtil.GetDataServiceCollectionOfT(nestedExpectedType);
list = (IEnumerable)Util.ActivatorCreateInstance(
dataServiceCollectionType,
list, // items
TrackingMode.None); // tracking mode
}
ProjectionPlan plan = CreatePlanForShallowMaterialization(nestedExpectedType);
materializer.FoundNextLinkForCollection(list, feedValue.NextLink, plan);
result = list;
}
else if (atomProperty.Entry != null)
{
// If this is a leaf, then we'll do a tracking, payload-driven
// materialization. If this isn't the leaf, then we'll
// simply traverse through its properties.
if (segmentIsLeaf && !atomProperty.Entry.EntityHasBeenResolved && !atomProperty.IsNull)
{
materializer.Materialize(atomProperty.Entry, property.PropertyType, /* includeLinks */ false);
}
properties = atomProperty.Entry.DataValues;
result = atomProperty.Entry.ResolvedObject;
entry = atomProperty.Entry;
}
else
{
if (atomProperty.Properties != null)
{
if (atomProperty.MaterializedValue == null && !atomProperty.IsNull)
{
ClientType complexType = ClientType.Create(property.PropertyType);
object complexInstance = Util.ActivatorCreateInstance(property.PropertyType);
MaterializeDataValues(complexType, atomProperty.Properties, materializer.ignoreMissingProperties, materializer.context);
ApplyDataValues(complexType, atomProperty.Properties, materializer.ignoreMissingProperties, materializer.context, complexInstance);
atomProperty.MaterializedValue = complexInstance;
}
}
else
{
MaterializeDataValue(property.NullablePropertyType, atomProperty, materializer.context);
}
properties = atomProperty.Properties;
result = atomProperty.MaterializedValue;
}
expectedType = property.PropertyType;
}
return result;
}
/// <summary>
/// Ensures that an entry of <paramref name="requiredType"/> is
/// available on the specified <paramref name="entry"/>.
/// </summary>
/// <param name="materializer">Materilizer used for logging. </param>
/// <param name="entry">Entry to ensure.</param>
/// <param name="requiredType">Required type.</param>
/// <remarks>
/// As the 'Projection' suffix suggests, this method should only
/// be used during projection operations; it purposefully avoid
/// "source tree" type usage and POST reply entry resolution.
/// </remarks>
internal static void ProjectionEnsureEntryAvailableOfType(AtomMaterializer materializer, AtomEntry entry, Type requiredType)
{
Debug.Assert(materializer != null, "materializer != null");
Debug.Assert(entry != null, "entry != null");
Debug.Assert(
materializer.targetInstance == null,
"materializer.targetInstance == null -- projection shouldn't have a target instance set; that's only used for POST replies");
if (entry.EntityHasBeenResolved)
{
if (!requiredType.IsAssignableFrom(entry.ResolvedObject.GetType()))
{
throw new InvalidOperationException(
"Expecting type '" + requiredType + "' for '" + entry.Identity + "', but found " +
"a previously created instance of type '" + entry.ResolvedObject.GetType());
}
// Just check the type
return;
}
if (entry.Identity == null)
{
throw Error.InvalidOperation(Strings.Deserialize_MissingIdElement);
}
if (!materializer.TryResolveAsCreated(entry) &&
!materializer.TryResolveFromContext(entry, requiredType))
{
// The type is always required, so skip ResolveByCreating.
materializer.ResolveByCreatingWithType(entry, requiredType);
}
else
{
if (!requiredType.IsAssignableFrom(entry.ResolvedObject.GetType()))
{
throw Error.InvalidOperation(Strings.Deserialize_Current(requiredType, entry.ResolvedObject.GetType()));
}
}
}
/// <summary>Materializes an entry with no special selection.</summary>
/// <param name="materializer">Materializer under which materialization should take place.</param>
/// <param name="entry">Entry with object to materialize.</param>
/// <param name="expectedEntryType">Expected type for the entry.</param>
/// <returns>The materialized instance.</returns>
internal static object DirectMaterializePlan(AtomMaterializer materializer, AtomEntry entry, Type expectedEntryType)
{
materializer.Materialize(entry, expectedEntryType, true);
return entry.ResolvedObject;
}
/// <summary>Materializes an entry without including in-lined expanded links.</summary>
/// <param name="materializer">Materializer under which materialization should take place.</param>
/// <param name="entry">Entry with object to materialize.</param>
/// <param name="expectedEntryType">Expected type for the entry.</param>
/// <returns>The materialized instance.</returns>
internal static object ShallowMaterializePlan(AtomMaterializer materializer, AtomEntry entry, Type expectedEntryType)
{
materializer.Materialize(entry, expectedEntryType, false);
return entry.ResolvedObject;
}
/// <summary>
/// Validates the specified <paramref name="property"/> matches
/// the parsed <paramref name="atomProperty"/>.
/// </summary>
/// <param name="property">Property as understood by the type system.</param>
/// <param name="atomProperty">Property as parsed.</param>
internal static void ValidatePropertyMatch(ClientType.ClientProperty property, AtomContentProperty atomProperty)
{
Debug.Assert(property != null, "property != null");
Debug.Assert(atomProperty != null, "atomProperty != null");
if (property.IsKnownType && (atomProperty.Feed != null || atomProperty.Entry != null))
{
throw Error.InvalidOperation(Strings.Deserialize_MismatchAtomLinkLocalSimple);
}
if (atomProperty.Feed != null && property.CollectionType == null)
{
throw Error.InvalidOperation(Strings.Deserialize_MismatchAtomLinkFeedPropertyNotCollection(property.PropertyName));
}
if (atomProperty.Entry != null && property.CollectionType != null)
{
throw Error.InvalidOperation(Strings.Deserialize_MismatchAtomLinkEntryPropertyIsCollection(property.PropertyName));
}
}
#endregion Projection support.
#region Internal methods.
/// <summary>Reads the next value from the input content.</summary>
/// <returns>true if another value is available after reading; false otherwise.</returns>
/// <remarks>
/// After invocation, the currentValue field (and CurrentValue property) will
/// reflect the value materialized from the parser; possibly null if the
/// result is true (for null values); always null if the result is false.
/// </remarks>
internal bool Read()
{
this.currentValue = null;
// links from last entry should be cleared
this.nextLinkTable.Clear();
while (this.parser.Read())
{
Debug.Assert(
this.parser.DataKind != AtomDataKind.None,
"parser.DataKind != AtomDataKind.None -- otherwise parser.Read() didn't update its state");
Debug.Assert(
this.parser.DataKind != AtomDataKind.Finished,
"parser.DataKind != AtomDataKind.Finished -- otherwise parser.Read() shouldn't have returned true");
switch (this.parser.DataKind)
{
case AtomDataKind.Feed:
case AtomDataKind.FeedCount:
// Nothing to do.
break;
case AtomDataKind.Entry:
Debug.Assert(
this.parser.CurrentEntry != null,
"parser.CurrentEntry != null -- otherwise parser.DataKind shouldn't be Entry");
this.CurrentEntry.ResolvedObject = this.TargetInstance;
this.currentValue = this.materializeEntryPlan.Run(this, this.CurrentEntry, this.expectedType);
return true;
case AtomDataKind.PagingLinks:
// encountered paging links - this is the OUTER-MOST feed
// we don't need to do anything here, the CurrentFeed.NextLink should
// have already been set (either to an Uri or to null)
break;
default:
Debug.Assert(
this.parser.DataKind == AtomDataKind.Custom,
"parser.DataKind == AtomDataKind.Custom -- otherwise AtomMaterializer.Read switch is missing a case");
// V1 Compatibility Note:
// Top-level primitive types are allowed to have mixed content,
// and must be parsed with ReadCustomElementString.
//
// For complex types, we'll instead use the regular property
// reading path, with will throw on mixed content.
Type underlyingExpectedType = Nullable.GetUnderlyingType(this.expectedType) ?? this.expectedType;
ClientType targetType = ClientType.Create(underlyingExpectedType);
if (ClientConvert.IsKnownType(underlyingExpectedType))
{
// primitive type - we don't care about the namespace as per V1, but it should be in XmlConstants.DataWebNamespace ("http://schemas.microsoft.com/ado/2007/08/dataservices")
string elementText = this.parser.ReadCustomElementString();
if (elementText != null)
{
this.currentValue = ClientConvert.ChangeType(elementText, underlyingExpectedType);
}
return true;
}
else if (!targetType.IsEntityType && this.parser.IsDataWebElement)
{
// complex type - the namespace URI must be in dataweb namespace
AtomContentProperty property = this.parser.ReadCurrentPropertyValue();
if (property == null || property.IsNull)
{
this.currentValue = null;
}
else
{
this.currentValue = targetType.CreateInstance();
MaterializeDataValues(targetType, property.Properties, this.ignoreMissingProperties, this.context);
ApplyDataValues(targetType, property.Properties, this.ignoreMissingProperties, this.context, this.currentValue);
}
return true;
}
break;
}
}
Debug.Assert(this.parser.DataKind == AtomDataKind.Finished, "parser.DataKind == AtomDataKind.None");
Debug.Assert(this.parser.CurrentEntry == null, "parser.Current == null");
return false;
}
/// <summary>Helper method for constructor of DataServiceCollection.</summary>
/// <typeparam name="T">Element type for collection.</typeparam>
/// <param name="from">The enumerable which has the continuation on it.</param>
/// <param name="to">The DataServiceCollection to apply the continuation to.</param>
internal void PropagateContinuation<T>(IEnumerable<T> from, DataServiceCollection<T> to)
{
DataServiceQueryContinuation continuation;
if (this.nextLinkTable.TryGetValue(from, out continuation))
{
this.nextLinkTable.Add(to, continuation);
Util.SetNextLinkForCollection(to, continuation);
}
}
#endregion Internal methods.
#region Private methods.
/// <summary>
/// Checks that the specified <paramref name="entry"/> isn't null.
/// </summary>
/// <param name="entry">Entry to check.</param>
/// <param name="name">Name of entry being accessed.</param>
private static void CheckEntryToAccessNotNull(AtomEntry entry, string name)
{
Debug.Assert(entry != null, "entry != null");
Debug.Assert(name != null, "name != null");
if (entry.IsNull)
{
throw new NullReferenceException(Strings.AtomMaterializer_EntryToAccessIsNull(name));
}
}
/// <summary>Creates an entry materialization plan for a given projection.</summary>
/// <param name="queryComponents">Query components for plan to materialize.</param>
/// <returns>A materialization plan.</returns>
private static ProjectionPlan CreatePlan(QueryComponents queryComponents)
{
// Can we have a primitive property as well?
LambdaExpression projection = queryComponents.Projection;
ProjectionPlan result;
if (projection == null)
{
result = CreatePlanForDirectMaterialization(queryComponents.LastSegmentType);
}
else
{
result = ProjectionPlanCompiler.CompilePlan(projection, queryComponents.NormalizerRewrites);
result.LastSegmentType = queryComponents.LastSegmentType;
}
return result;
}
/// <summary>Creates an entry materialization plan that is payload-driven.</summary>
/// <param name="lastSegmentType">Segment type for the entry to materialize (typically last of URI in query).</param>
/// <returns>A payload-driven materialization plan.</returns>
private static ProjectionPlan CreatePlanForDirectMaterialization(Type lastSegmentType)
{
ProjectionPlan result = new ProjectionPlan();
result.Plan = AtomMaterializerInvoker.DirectMaterializePlan;
result.ProjectedType = lastSegmentType;
result.LastSegmentType = lastSegmentType;
return result;
}
/// <summary>Creates an entry materialization plan that is payload-driven and does not traverse expanded links.</summary>
/// <param name="lastSegmentType">Segment type for the entry to materialize (typically last of URI in query).</param>
/// <returns>A payload-driven materialization plan.</returns>
private static ProjectionPlan CreatePlanForShallowMaterialization(Type lastSegmentType)
{
ProjectionPlan result = new ProjectionPlan();
result.Plan = AtomMaterializerInvoker.ShallowMaterializePlan;
result.ProjectedType = lastSegmentType;
result.LastSegmentType = lastSegmentType;
return result;
}
/// <summary>Gets a delegate that can be invoked to add an item to a collection of the specified type.</summary>
/// <param name='listType'>Type of list to use.</param>
/// <returns>The delegate to invoke.</returns>
private static Action<object, object> GetAddToCollectionDelegate(Type listType)
{
Debug.Assert(listType != null, "listType != null");
Type listElementType;
MethodInfo addMethod = ClientType.GetAddToCollectionMethod(listType, out listElementType);
ParameterExpression list = Expression.Parameter(typeof(object), "list");
ParameterExpression item = Expression.Parameter(typeof(object), "element");
Expression body = Expression.Call(Expression.Convert(list, listType), addMethod, Expression.Convert(item, listElementType));
#if ASTORIA_LIGHT
LambdaExpression lambda = ExpressionHelpers.CreateLambda(body, list, item);
#else
LambdaExpression lambda = Expression.Lambda(body, list, item);
#endif
return (Action<object, object>)lambda.Compile();
}
/// <summary>
/// Gets or creates a collection property on the specified <paramref name="instance"/>.
/// </summary>
/// <param name="instance">Instance on which to get/create the collection.</param>
/// <param name="property">Collection property on the <paramref name="instance"/>.</param>
/// <param name="collectionType">Type to use as a collection, possibly null.</param>
/// <returns>
/// The collection corresponding to the specified <paramref name="property"/>;
/// never null.
/// </returns>
private static object GetOrCreateCollectionProperty(object instance, ClientType.ClientProperty property, Type collectionType)
{
Debug.Assert(instance != null, "instance != null");
Debug.Assert(property != null, "property != null");
Debug.Assert(property.CollectionType != null, "property.CollectionType != null -- otherwise property isn't a collection");
// NOTE: in V1, we would have instantiated nested objects before setting them.
object result;
result = property.GetValue(instance);
if (result == null)
{
if (collectionType == null)
{
collectionType = property.PropertyType;
if (collectionType.IsInterface)
{
collectionType = typeof(System.Collections.ObjectModel.Collection<>).MakeGenericType(property.CollectionType);
}
}
result = Activator.CreateInstance(collectionType);
property.SetValue(instance, result, property.PropertyName, false /* add */);
}
Debug.Assert(result != null, "result != null -- otherwise GetOrCreateCollectionProperty didn't fall back to creation");
return result;
}
/// <summary>Materializes the result of a projection into a list.</summary>
/// <param name="materializer">Materializer to use for the operation.</param>
/// <param name="list">Target list.</param>
/// <param name="nestedExpectedType">Expected type for nested object.</param>
/// <param name="entries">Entries to materialize from.</param>
/// <remarks>
/// This method supports projections and as such does shallow payload-driven
/// materialization of entities.
/// </remarks>
private static void MaterializeToList(
AtomMaterializer materializer,
IEnumerable list,
Type nestedExpectedType,
IEnumerable<AtomEntry> entries)
{
Debug.Assert(materializer != null, "materializer != null");
Debug.Assert(list != null, "list != null");
Action<object, object> addMethod = GetAddToCollectionDelegate(list.GetType());
foreach (AtomEntry feedEntry in entries)
{
if (!feedEntry.EntityHasBeenResolved)
{
materializer.Materialize(feedEntry, nestedExpectedType, /* includeLinks */ false);
}
addMethod(list, feedEntry.ResolvedObject);
}
}
/// <summary>Materializes a single value.</summary>
/// <param name="type">Type of value to set.</param>
/// <param name="atomProperty">Property holding value.</param>
/// <param name="context">Context under which value will be set.</param>
/// <returns>true if the value was set; false if it wasn't (typically because it's a complex value).</returns>
private static bool MaterializeDataValue(Type type, AtomContentProperty atomProperty, DataServiceContext context)
{
Debug.Assert(type != null, "type != null");
Debug.Assert(atomProperty != null, "atomProperty != null");
Debug.Assert(context != null, "context != null");
string propertyTypeName = atomProperty.TypeName;
string propertyValueText = atomProperty.Text;
ClientType nestedElementType = null;
Type underlyingType = Nullable.GetUnderlyingType(type) ?? type;
bool knownType = ClientConvert.IsKnownType(underlyingType);
if (!knownType)
{
nestedElementType = MaterializeAtom.GetEntryClientType(propertyTypeName, context, type, true);
Debug.Assert(nestedElementType != null, "nestedElementType != null -- otherwise ReadTypeAttribute (or someone!) should throw");
knownType = ClientConvert.IsKnownType(nestedElementType.ElementType);
}
if (knownType)
{
if (atomProperty.IsNull)
{
if (!ClientType.CanAssignNull(type))
{
throw new InvalidOperationException(Strings.AtomMaterializer_CannotAssignNull(atomProperty.Name, type.FullName));
}
atomProperty.MaterializedValue = null;
return true;
}
else
{
object value = propertyValueText;
if (propertyValueText != null)
{
value = ClientConvert.ChangeType(propertyValueText, (null != nestedElementType ? nestedElementType.ElementType : underlyingType));
}
atomProperty.MaterializedValue = value;
return true;
}
}
return false;
}
/// <summary>
/// Materializes the primitive data vlues in the given list of <paramref name="values"/>.
/// </summary>
/// <param name="actualType">Actual type for properties being materialized.</param>
/// <param name="values">List of values to materialize.</param>
/// <param name="ignoreMissingProperties">
/// Whether properties missing from the client types should be ignored.
/// </param>
/// <param name="context">Data context, used for type resolution.</param>
/// <remarks>
/// Values are materialized in-place withi each <see cref="AtomContentProperty"/>
/// instance.
/// </remarks>
private static void MaterializeDataValues(
ClientType actualType,
List<AtomContentProperty> values,
bool ignoreMissingProperties,
DataServiceContext context)
{
Debug.Assert(actualType != null, "actualType != null");
Debug.Assert(values != null, "values != null");
Debug.Assert(context != null, "context != null");
foreach (var atomProperty in values)
{
string propertyName = atomProperty.Name;
var property = actualType.GetProperty(propertyName, ignoreMissingProperties); // may throw
if (property == null)
{
continue;
}
if (atomProperty.Feed == null && atomProperty.Entry == null)
{
bool materialized = MaterializeDataValue(property.NullablePropertyType, atomProperty, context);
if (!materialized && property.CollectionType != null)
{
// client object property is collection implying nested collection of complex objects
throw Error.NotSupported(Strings.ClientType_CollectionOfNonEntities);
}
}
}
}
/// <summary>Applies a data value to the specified <paramref name="instance"/>.</summary>
/// <param name="type">Type to which a property value will be applied.</param>
/// <param name="property">Property with value to apply.</param>
/// <param name="ignoreMissingProperties">
/// Whether properties missing from the client types should be ignored.
/// </param>
/// <param name="context">Data context, used for type resolution.</param>
/// <param name="instance">Instance on which value will be applied.</param>
private static void ApplyDataValue(ClientType type, AtomContentProperty property, bool ignoreMissingProperties, DataServiceContext context, object instance)
{
Debug.Assert(type != null, "type != null");
Debug.Assert(property != null, "property != null");
Debug.Assert(context != null, "context != null");
Debug.Assert(instance != null, "instance != context");
var prop = type.GetProperty(property.Name, ignoreMissingProperties);
if (prop == null)
{
return;
}
if (property.Properties != null)
{
if (prop.IsKnownType ||
ClientConvert.IsKnownType(MaterializeAtom.GetEntryClientType(property.TypeName, context, prop.PropertyType, true).ElementType))
{
// The error message is a bit odd, but it's compatible with V1.
throw Error.InvalidOperation(Strings.Deserialize_ExpectingSimpleValue);
}
// Complex type.
bool needToSet = false;
ClientType complexType = ClientType.Create(prop.PropertyType);
object complexInstance = prop.GetValue(instance);
if (complexInstance == null)
{
complexInstance = complexType.CreateInstance();
needToSet = true;
}
MaterializeDataValues(complexType, property.Properties, ignoreMissingProperties, context);
ApplyDataValues(complexType, property.Properties, ignoreMissingProperties, context, complexInstance);
if (needToSet)
{
prop.SetValue(instance, complexInstance, property.Name, true /* allowAdd? */);
}
}
else
{
prop.SetValue(instance, property.MaterializedValue, property.Name, true /* allowAdd? */);
}
}
/// <summary>
/// Applies the values of the specified <paramref name="properties"/> to a
/// given <paramref name="instance"/>.
/// </summary>
/// <param name="type">Type to which properties will be applied.</param>
/// <param name="properties">Properties to assign to the specified <paramref name="instance"/>.</param>
/// <param name="ignoreMissingProperties">
/// Whether properties missing from the client types should be ignored.
/// </param>
/// <param name="context">Data context, used for type resolution.</param>
/// <param name="instance">Instance on which values will be applied.</param>
private static void ApplyDataValues(ClientType type, IEnumerable<AtomContentProperty> properties, bool ignoreMissingProperties, DataServiceContext context, object instance)
{
Debug.Assert(type != null, "type != null");
Debug.Assert(properties != null, "properties != null");
Debug.Assert(context != null, "properties != context");
Debug.Assert(instance != null, "instance != context");
foreach (var p in properties)
{
ApplyDataValue(type, p, ignoreMissingProperties, context, instance);
}
}
/// <summary>
/// Sets a value on the property identified by the given <paramref name="path"/>.
/// </summary>
/// <param name="values">Starting list of properties to set.</param>
/// <param name="path">List of paths, delimited by '/' characters.</param>
/// <param name="value">Value to set in text form.</param>
/// <param name="typeName">Name of type of value to set.</param>
private static void SetValueOnPath(List<AtomContentProperty> values, string path, string value, string typeName)
{
Debug.Assert(values != null, "values != null");
Debug.Assert(path != null, "path != null");
bool existing = true;
AtomContentProperty property = null;
foreach (string step in path.Split('/'))
{
if (values == null)
{
Debug.Assert(property != null, "property != null -- if values is null then this isn't the first step");
property.EnsureProperties();
values = property.Properties;
}
property = values.Where(v => v.Name == step).FirstOrDefault();
if (property == null)
{
AtomContentProperty newProperty = new AtomContentProperty();
existing = false;
newProperty.Name = step;
values.Add(newProperty);
property = newProperty;
}
else
{
// If any property along the way was already marked as null in the payload,
// we can exit early as there will be nothing to do in any
// further nested types.
if (property.IsNull)
{
//
return;
}
}
values = property.Properties;
}
Debug.Assert(property != null, "property != null -- property path should have at least one segment");
// Content wins, therefore only set values if the property didn't exist before.
if (existing == false)
{
property.TypeName = typeName;
property.Text = value;
}
}
/// <summary>
/// Applies all available Entity Property Mappings on the specified <paramref name="entry"/>.
/// </summary>
/// <param name="entry">ATOM entry on which to apply entity property mappings.</param>
/// <param name="entryType">Client type used to read EPM info from.</param>
private static void ApplyEntityPropertyMappings(AtomEntry entry, ClientType entryType)
{
Debug.Assert(entry != null, "entry != null");
Debug.Assert(entry.Tag is XElement, "entry.Tag is XElement");
Debug.Assert(entryType != null, "entryType != null -- othewise how would we know to apply property mappings (note that for projections entry.ActualType may be different that entryType)?");
Debug.Assert(!entry.EntityPropertyMappingsApplied, "!entry.EntityPropertyMappingsApplied -- EPM should happen only once per entry");
if (entryType.HasEntityPropertyMappings)
{
// NOTE: this is an XElement-based version of the code.
// There is a streaming version which has been since removed at:
// - %sdxroot%\ndp\fx\src\DataWeb\Client\System\Data\Services\Client\Epm\EpmContentDeserializer.cs
// - %sdxroot%\ndp\fx\src\DataWeb\Client\System\Data\Services\Client\Epm\EpmReaderState.cs
XElement entryElement = entry.Tag as XElement;
Debug.Assert(entryElement != null, "entryElement != null");
ApplyEntityPropertyMappings(entry, entryElement, entryType.EpmTargetTree.SyndicationRoot);
ApplyEntityPropertyMappings(entry, entryElement, entryType.EpmTargetTree.NonSyndicationRoot);
}
entry.EntityPropertyMappingsApplied = true;
}
/// <summary>
/// Applies Entity Property Mappings on the specified
/// <paramref name="entry"/> given a <paramref name="target"/> root.
/// </summary>
/// <param name="entry">ATOM entry on which to apply entity property mappings.</param>
/// <param name="entryElement">XML tree for the specified.</param>
/// <param name="target">Target for mapping.</param>
private static void ApplyEntityPropertyMappings(AtomEntry entry, XElement entryElement, EpmTargetPathSegment target)
{
Debug.Assert(target != null, "target != null");
Debug.Assert(!target.HasContent, "!target.HasContent");
//
Stack<System.Data.Services.Common.EpmTargetPathSegment> segments = new Stack<System.Data.Services.Common.EpmTargetPathSegment>();
Stack<XElement> elements = new Stack<XElement>();
segments.Push(target);
elements.Push(entryElement);
while (segments.Count > 0)
{
System.Data.Services.Common.EpmTargetPathSegment segment = segments.Pop();
XElement element = elements.Pop();
if (segment.HasContent)
{
var node = element.Nodes().Where(n => n.NodeType == XmlNodeType.Text || n.NodeType == XmlNodeType.SignificantWhitespace).FirstOrDefault();
string elementValue = (node == null) ? null : ((XText)node).Value;
Debug.Assert(segment.EpmInfo != null, "segment.EpmInfo != null -- otherwise segment.HasValue should be false");
string path = segment.EpmInfo.Attribute.SourcePath;
string typeName = (string)element.Attribute(XName.Get(XmlConstants.AtomTypeAttributeName, XmlConstants.DataWebMetadataNamespace));
//
SetValueOnPath(entry.DataValues, path, elementValue, typeName);
}
foreach (var item in segment.SubSegments)
{
if (item.IsAttribute)
{
string localName = item.SegmentName.Substring(1);
var attribute = element.Attribute(XName.Get(localName, item.SegmentNamespaceUri));
if (attribute != null)
{
// NOTE: nulls can't be expressed on the attribute; null values
// are required to always be part of the main content instead.
SetValueOnPath(entry.DataValues, item.EpmInfo.Attribute.SourcePath, attribute.Value, null);
}
}
else
{
var child = element.Element(XName.Get(item.SegmentName, item.SegmentNamespaceUri));
if (child != null)
{
segments.Push(item);
elements.Push(child);
}
}
}
Debug.Assert(segments.Count == elements.Count, "segments.Count == elements.Count -- otherwise they're out of sync");
}
}
/// <summary>Gets a property from the specified <paramref name="properties"/> list, throwing if not found.</summary>
/// <param name="properties">List to get value from.</param>
/// <param name="propertyName">Property name to look up.</param>
/// <returns>The specified property (never null).</returns>
private static AtomContentProperty GetPropertyOrThrow(List<AtomContentProperty> properties, string propertyName)
{
AtomContentProperty atomProperty = null;
if (properties != null)
{
atomProperty = properties.Where(p => p.Name == propertyName).FirstOrDefault();
}
if (atomProperty == null)
{
throw new InvalidOperationException(Strings.AtomMaterializer_PropertyMissing(propertyName));
}
Debug.Assert(atomProperty != null, "atomProperty != null");
return atomProperty;
}
/// <summary>Gets a property from the specified <paramref name="entry"/>, throwing if not found.</summary>
/// <param name="entry">Entry to get value from.</param>
/// <param name="propertyName">Property name to look up.</param>
/// <returns>The specified property (never null).</returns>
private static AtomContentProperty GetPropertyOrThrow(AtomEntry entry, string propertyName)
{
AtomContentProperty atomProperty = null;
var properties = entry.DataValues;
if (properties != null)
{
atomProperty = properties.Where(p => p.Name == propertyName).FirstOrDefault();
}
if (atomProperty == null)
{
throw new InvalidOperationException(Strings.AtomMaterializer_PropertyMissingFromEntry(propertyName, entry.Identity));
}
Debug.Assert(atomProperty != null, "atomProperty != null");
return atomProperty;
}
/// <summary>Merges a list into the property of a given <paramref name="entry"/>.</summary>
/// <param name="entry">Entry to merge into.</param>
/// <param name="property">Property on entry to merge into.</param>
/// <param name="list">List of materialized values.</param>
/// <param name="nextLink">Next link for feed from which the materialized values come from.</param>
/// <param name="plan">Projection plan for the list.</param>
/// <remarks>
/// This method will handle entries that shouldn't be updated correctly.
/// </remarks>
private void MergeLists(AtomEntry entry, ClientType.ClientProperty property, IEnumerable list, Uri nextLink, ProjectionPlan plan)
{
Debug.Assert(entry != null, "entry != null");
Debug.Assert(entry.ResolvedObject != null, "entry.ResolvedObject != null");
Debug.Assert(property != null, "property != null");
Debug.Assert(entry != null, "entry != null");
Debug.Assert(plan != null || nextLink == null, "plan != null || nextLink == null");
// Simple case: the list is of the target type, and the resolved entity
// has null; we can simply assign the collection. No merge required.
if (entry.ShouldUpdateFromPayload &&
property.NullablePropertyType == list.GetType() &&
property.GetValue(entry.ResolvedObject) == null)
{
property.SetValue(entry.ResolvedObject, list, property.PropertyName, false /* allowAdd */);
this.FoundNextLinkForCollection(list, nextLink, plan);
foreach (object item in list)
{
this.log.AddedLink(entry, property.PropertyName, item);
}
return;
}
this.ApplyItemsToCollection(entry, property, list, nextLink, plan);
}
/// <summary>Tries to resolve the object as the target one in a POST refresh.</summary>
/// <param name="entry">Entry to resolve.</param>
/// <returns>true if the entity was resolved; false otherwise.</returns>
private bool TryResolveAsTarget(AtomEntry entry)
{
if (entry.ResolvedObject == null)
{
return false;
}
// The only case when the entity hasn't been resolved but
// it has already been set is when the target instance
// was set directly to refresh a POST.
Debug.Assert(
entry.ResolvedObject == this.TargetInstance,
"entry.ResolvedObject == this.TargetInstance -- otherwise there we ResolveOrCreateInstance more than once on the same entry");
Debug.Assert(
this.mergeOption == MergeOption.OverwriteChanges || this.mergeOption == MergeOption.PreserveChanges,
"MergeOption.OverwriteChanges and MergeOption.PreserveChanges are the only expected values during SaveChanges");
entry.ActualType = ClientType.Create(entry.ResolvedObject.GetType());
this.log.FoundTargetInstance(entry);
entry.ShouldUpdateFromPayload = this.mergeOption == MergeOption.PreserveChanges ? false : true;
entry.EntityHasBeenResolved = true;
return true;
}
/// <summary>Tries to resolve the object as one from the context (only if tracking is enabled).</summary>
/// <param name="entry">Entry to resolve.</param>
/// <param name="expectedEntryType">Expected entry type for the specified <paramref name="entry"/>.</param>
/// <returns>true if the entity was resolved; false otherwise.</returns>
private bool TryResolveFromContext(AtomEntry entry, Type expectedEntryType)
{
// We should either create a new instance or grab one from the context.
bool tracking = this.mergeOption != MergeOption.NoTracking;
if (tracking)
{
EntityStates state;
entry.ResolvedObject = this.context.TryGetEntity(entry.Identity, entry.ETagText, this.mergeOption, out state);
if (entry.ResolvedObject != null)
{
if (!expectedEntryType.IsInstanceOfType(entry.ResolvedObject))
{
throw Error.InvalidOperation(Strings.Deserialize_Current(expectedEntryType, entry.ResolvedObject.GetType()));
}
entry.ActualType = ClientType.Create(entry.ResolvedObject.GetType());
entry.EntityHasBeenResolved = true;
// Note that deleted items will have their properties overwritten even
// if PreserveChanges is used as a merge option.
entry.ShouldUpdateFromPayload =
this.mergeOption == MergeOption.OverwriteChanges ||
(this.mergeOption == MergeOption.PreserveChanges && state == EntityStates.Unchanged) ||
(this.mergeOption == MergeOption.PreserveChanges && state == EntityStates.Deleted);
this.log.FoundExistingInstance(entry);
return true;
}
}
return false;
}
/// <summary>"Resolved" the entity in the <paramref name="entry"/> by instantiating it.</summary>
/// <param name="entry">Entry to resolve.</param>
/// <param name="type">Type to create.</param>
/// <remarks>
/// After invocation, entry.ResolvedObject is exactly of type <paramref name="type"/>.
/// </remarks>
private void ResolveByCreatingWithType(AtomEntry entry, Type type)
{
Debug.Assert(
entry.ResolvedObject == null,
"entry.ResolvedObject == null -- otherwise we're about to overwrite - should never be called");
entry.ActualType = ClientType.Create(type);
entry.ResolvedObject = Activator.CreateInstance(type);
entry.CreatedByMaterializer = true;
entry.ShouldUpdateFromPayload = true;
entry.EntityHasBeenResolved = true;
this.log.CreatedInstance(entry);
}
/// <summary>"Resolved" the entity in the <paramref name="entry"/> by instantiating it.</summary>
/// <param name="entry">Entry to resolve.</param>
/// <param name="expectedEntryType">Type expected by the projection.</param>
/// <remarks>
/// This method allows the expected entry to be overriden by either a callback
/// or by the type read by the parser.
/// </remarks>
private void ResolveByCreating(AtomEntry entry, Type expectedEntryType)
{
Debug.Assert(
entry.ResolvedObject == null,
"entry.ResolvedObject == null -- otherwise we're about to overwrite - should never be called");
ClientType actualType = MaterializeAtom.GetEntryClientType(entry.TypeName, this.context, expectedEntryType, true);
// We can't assert that actualType.HasKeys, as there are cases where keys won't be defined, and we still
// support deserializing them.
Debug.Assert(actualType != null, "actualType != null -- otherwise ClientType.Create returned a null value");
this.ResolveByCreatingWithType(entry, actualType.ElementType);
}
/// <summary>Tries to resolve the object from those created in this materialization session.</summary>
/// <param name="entry">Entry to resolve.</param>
/// <returns>true if the entity was resolved; false otherwise.</returns>
private bool TryResolveAsCreated(AtomEntry entry)
{
AtomEntry existingEntry;
if (!this.log.TryResolve(entry, out existingEntry))
{
return false;
}
Debug.Assert(
existingEntry.ResolvedObject != null,
"existingEntry.ResolvedObject != null -- how did it get there otherwise?");
entry.ActualType = existingEntry.ActualType;
entry.ResolvedObject = existingEntry.ResolvedObject;
entry.CreatedByMaterializer = existingEntry.CreatedByMaterializer;
entry.ShouldUpdateFromPayload = existingEntry.ShouldUpdateFromPayload;
entry.EntityHasBeenResolved = true;
return true;
}
/// <summary>Resolved or creates an instance on the specified <paramref name="entry"/>.</summary>
/// <param name="entry">Entry on which to resolve or create an instance.</param>
/// <param name="expectedEntryType">Expected type for the <paramref name="entry"/>.</param>
/// <remarks>
/// After invocation, the ResolvedObject value of the <paramref name="entry"/>
/// will be assigned, along with the ActualType value.
/// </remarks>
private void ResolveOrCreateInstance(AtomEntry entry, Type expectedEntryType)
{
Debug.Assert(entry != null, "entry != null");
Debug.Assert(expectedEntryType != null, "expectedEntryType != null");
Debug.Assert(entry.EntityHasBeenResolved == false, "entry.EntityHasBeenResolved == false");
// This will be the case when TargetInstance has been set.
if (!this.TryResolveAsTarget(entry))
{
if (entry.Identity == null)
{
throw Error.InvalidOperation(Strings.Deserialize_MissingIdElement);
}
if (!this.TryResolveAsCreated(entry))
{
if (!this.TryResolveFromContext(entry, expectedEntryType))
{
this.ResolveByCreating(entry, expectedEntryType);
}
}
}
Debug.Assert(entry.ActualType != null, "entry.ActualType != null");
Debug.Assert(entry.ResolvedObject != null, "entry.ResolvedObject != null");
Debug.Assert(entry.EntityHasBeenResolved, "entry.EntityHasBeenResolved");
return;
}
/// <summary>
/// Applies the values of a nested <paramref name="feed"/> to the collection
/// <paramref name="property"/> of the specified <paramref name="entry"/>.
/// </summary>
/// <param name="entry">Entry with collection to be modified.</param>
/// <param name="property">Collection property on the entry.</param>
/// <param name="feed">Values to apply onto the collection.</param>
/// <param name="includeLinks">Whether links that are expanded should be materialized.</param>
private void ApplyFeedToCollection(
AtomEntry entry,
ClientType.ClientProperty property,
AtomFeed feed,
bool includeLinks)
{
Debug.Assert(entry != null, "entry != null");
Debug.Assert(property != null, "property != null");
Debug.Assert(feed != null, "feed != null");
ClientType collectionType = ClientType.Create(property.CollectionType);
foreach (AtomEntry feedEntry in feed.Entries)
{
this.Materialize(feedEntry, collectionType.ElementType, includeLinks);
}
ProjectionPlan continuationPlan = includeLinks ? CreatePlanForDirectMaterialization(property.CollectionType) : CreatePlanForShallowMaterialization(property.CollectionType);
this.ApplyItemsToCollection(entry, property, feed.Entries.Select(e => e.ResolvedObject), feed.NextLink, continuationPlan);
}
/// <summary>
/// Applies the values of the <paramref name="items"/> enumeration to the
/// <paramref name="property"/> of the specified <paramref name="entry"/>.
/// </summary>
/// <param name="entry">Entry with collection to be modified.</param>
/// <param name="property">Collection property on the entry.</param>
/// <param name="items">Values to apply onto the collection.</param>
/// <param name="nextLink">Next link for collection continuation.</param>
/// <param name="continuationPlan">Projection plan for collection continuation.</param>
private void ApplyItemsToCollection(
AtomEntry entry,
ClientType.ClientProperty property,
IEnumerable items,
Uri nextLink,
ProjectionPlan continuationPlan)
{
Debug.Assert(entry != null, "entry != null");
Debug.Assert(property != null, "property != null");
Debug.Assert(items != null, "items != null");
object collection = entry.ShouldUpdateFromPayload ? GetOrCreateCollectionProperty(entry.ResolvedObject, property, null) : null;
ClientType collectionType = ClientType.Create(property.CollectionType);
foreach (object item in items)
{
if (!collectionType.ElementType.IsAssignableFrom(item.GetType()))
{
string message = Strings.AtomMaterializer_EntryIntoCollectionMismatch(
item.GetType().FullName,
collectionType.ElementType.FullName);
throw new InvalidOperationException(message);
}
if (entry.ShouldUpdateFromPayload)
{
property.SetValue(collection, item, property.PropertyName, true /* allowAdd? */);
this.log.AddedLink(entry, property.PropertyName, item);
}
}
if (entry.ShouldUpdateFromPayload)
{
this.FoundNextLinkForCollection(collection as IEnumerable, nextLink, continuationPlan);
}
else
{
this.FoundNextLinkForUnmodifiedCollection(property.GetValue(entry.ResolvedObject) as IEnumerable);
}
// Remove the extra items from the collection as necessary.
if (this.mergeOption == MergeOption.OverwriteChanges || this.mergeOption == MergeOption.PreserveChanges)
{
var itemsToRemove =
from x in this.context.GetLinks(entry.ResolvedObject, property.PropertyName)
where MergeOption.OverwriteChanges == this.mergeOption || EntityStates.Added != x.State
select x.Target;
itemsToRemove = itemsToRemove.Except(EnumerateAsElementType<object>(items));
foreach (var item in itemsToRemove)
{
if (collection != null)
{
property.RemoveValue(collection, item);
}
this.log.RemovedLink(entry, property.PropertyName, item);
}
}
}
/// <summary>Records the fact that a rel='next' link was found for the specified <paramref name="collection"/>.</summary>
/// <param name="collection">Collection to add link to.</param>
/// <param name="link">Link (possibly null).</param>
/// <param name="plan">Projection plan for the collection (null allowed only if link is null).</param>
private void FoundNextLinkForCollection(IEnumerable collection, Uri link, ProjectionPlan plan)
{
Debug.Assert(plan != null || link == null, "plan != null || link == null");
if (collection != null && !this.nextLinkTable.ContainsKey(collection))
{
DataServiceQueryContinuation continuation = DataServiceQueryContinuation.Create(link, plan);
this.nextLinkTable.Add(collection, continuation);
Util.SetNextLinkForCollection(collection, continuation);
}
}
/// <summary>Records the fact that a <paramref name="collection"/> was found but won't be modified.</summary>
/// <param name="collection">Collection to add link to.</param>
private void FoundNextLinkForUnmodifiedCollection(IEnumerable collection)
{
if (collection != null && !this.nextLinkTable.ContainsKey(collection))
{
this.nextLinkTable.Add(collection, null);
}
}
/// <summary>Materializes the specified <paramref name="entry"/>.</summary>
/// <param name="entry">Entry with object to materialize.</param>
/// <param name="expectedEntryType">Expected type for the entry.</param>
/// <param name="includeLinks">Whether links that are expanded should be materialized.</param>
/// <remarks>This is a payload-driven materialization process.</remarks>
private void Materialize(AtomEntry entry, Type expectedEntryType, bool includeLinks)
{
Debug.Assert(entry != null, "entry != null");
Debug.Assert(entry.DataValues != null, "entry.DataValues != null -- otherwise not correctly initialized");
Debug.Assert(
entry.ResolvedObject == null || entry.ResolvedObject == this.targetInstance,
"entry.ResolvedObject == null || entry.ResolvedObject == this.targetInstance -- otherwise getting called twice");
Debug.Assert(expectedEntryType != null, "expectedType != null");
// ResolvedObject will already be assigned when we have a TargetInstance, for example.
this.ResolveOrCreateInstance(entry, expectedEntryType);
Debug.Assert(entry.ResolvedObject != null, "entry.ResolvedObject != null -- otherwise ResolveOrCreateInstnace didn't do its job");
this.MaterializeResolvedEntry(entry, includeLinks);
}
/// <summary>Materializes the specified <paramref name="entry"/>.</summary>
/// <param name="entry">Entry with object to materialize.</param>
/// <param name="includeLinks">Whether links that are expanded should be materialized.</param>
/// <remarks>This is a payload-driven materialization process.</remarks>
private void MaterializeResolvedEntry(AtomEntry entry, bool includeLinks)
{
Debug.Assert(entry != null, "entry != null");
Debug.Assert(entry.ResolvedObject != null, "entry.ResolvedObject != null -- otherwise not resolved/created!");
ClientType actualType = entry.ActualType;
if (!entry.EntityPropertyMappingsApplied)
{
// EPM should happen only once per entry
ApplyEntityPropertyMappings(entry, entry.ActualType);
}
// Note that even if ShouldUpdateFromPayload is false, we will still be creating
// nested instances (but not their links), so they show up in the data context
// entries. This keeps this code compatible with V1 behavior.
MaterializeDataValues(actualType, entry.DataValues, this.ignoreMissingProperties, this.context);
foreach (var e in entry.DataValues)
{
var prop = actualType.GetProperty(e.Name, this.ignoreMissingProperties);
if (prop == null)
{
continue;
}
if (entry.ShouldUpdateFromPayload == false && e.Entry == null && e.Feed == null)
{
// Skip non-links for ShouldUpdateFromPayload.
continue;
}
if (!includeLinks && (e.Entry != null || e.Feed != null))
{
continue;
}
ValidatePropertyMatch(prop, e);
AtomFeed feedValue = e.Feed;
if (feedValue != null)
{
Debug.Assert(includeLinks, "includeLinks -- otherwise we shouldn't be materializing this entry");
this.ApplyFeedToCollection(entry, prop, feedValue, includeLinks);
}
else if (e.Entry != null)
{
if (!e.IsNull)
{
Debug.Assert(includeLinks, "includeLinks -- otherwise we shouldn't be materializing this entry");
this.Materialize(e.Entry, prop.PropertyType, includeLinks);
}
if (entry.ShouldUpdateFromPayload)
{
prop.SetValue(entry.ResolvedObject, e.Entry.ResolvedObject, e.Name, true /* allowAdd? */);
this.log.SetLink(entry, prop.PropertyName, e.Entry.ResolvedObject);
}
}
else
{
Debug.Assert(entry.ShouldUpdateFromPayload, "entry.ShouldUpdateFromPayload -- otherwise we're about to set a property we shouldn't");
ApplyDataValue(actualType, e, this.ignoreMissingProperties, this.context, entry.ResolvedObject);
}
}
Debug.Assert(entry.ResolvedObject != null, "entry.ResolvedObject != null -- otherwise we didn't do any useful work");
if (this.materializedObjectCallback != null)
{
this.materializedObjectCallback(entry.Tag, entry.ResolvedObject);
}
}
#endregion Private methods.
}
}
|