Code:
/ Dotnetfx_Vista_SP2 / Dotnetfx_Vista_SP2 / 8.0.50727.4016 / DEVDIV / depot / DevDiv / releases / Orcas / QFE / ndp / fx / src / DataWeb / Client / System / Data / Services / Client / DataServiceContext.cs / 2 / DataServiceContext.cs
//----------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//
// context
//
//---------------------------------------------------------------------
namespace System.Data.Services.Client
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
#if !ASTORIA_LIGHT // Data.Services http stack
using System.Net;
#else
using System.Data.Services.Http;
#endif
using System.Text;
using System.Xml;
using System.Xml.Linq;
///
/// context
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506", Justification = "Central class of the API, likely to have many cross-references")]
public class DataServiceContext
{
/// represents identity for a resource without one
private const Uri NoIdentity = null;
/// represents entityset for a resource without one
private const string NoEntitySet = null;
/// represents empty etag
private const string NoETag = null;
/// base uri prepended to relative uri
private readonly System.Uri baseUri;
/// base uri with guranteed trailing slash
private readonly System.Uri baseUriWithSlash;
#if !ASTORIA_LIGHT // Credentials not available
/// Authentication interface for retrieving credentials for Web client authentication.
private System.Net.ICredentials credentials;
#endif
/// Override the namespace used for the data parts of the ATOM entries
private string dataNamespace;
/// resolve type from a typename
private Func resolveName;
/// resolve typename from a type
private Func resolveType;
#if !ASTORIA_LIGHT // Timeout not available
/// time-out value in seconds, 0 for default
private int timeout;
#endif
/// whether to use post-tunneling for PUT/DELETE
private bool postTunneling;
/// Options when deserializing properties to the target type.
private bool ignoreMissingProperties;
/// Used to specify a value synchronization strategy.
private MergeOption mergeOption;
/// Default options to be used while doing savechanges.
private SaveChangesOptions saveChangesDefaultOptions;
/// Override the namespace used for the scheme in the category for ATOM entries.
private Uri typeScheme;
#region Resource state management
/// change order
private uint nextChange;
/// Set of tracked resources by ResourceBox.Resource
private Dictionary objectToResource = new Dictionary();
/// Set of tracked resources by ResourceBox.Identity
private Dictionary identityToResource;
/// Set of tracked bindings by ResourceBox.Identity
private Dictionary bindings = new Dictionary(RelatedEnd.EquivalenceComparer);
#endregion
#region ctor
///
/// Instantiates a new context with the specified Uri.
/// The library expects the Uri to point to the root of a data service,
/// but does not issue a request to validate it does indeed identify the root of a service.
/// If the Uri does not identify the root of the service, the behavior of the client library is undefined.
///
///
/// An absolute, well formed http or https URI without a query or fragment which identifies the root of a data service.
/// A Uri provided with a trailing slash is equivalent to one without such a trailing character
///
/// if the is not an absolute, well formed http or https URI without a query or fragment
/// when the is null
///
/// With Silverlight, the can be a relative Uri
/// that will be combined with System.Windows.Browser.HtmlPage.Document.DocumentUri.
///
public DataServiceContext(Uri serviceRoot)
{
Util.CheckArgumentNull(serviceRoot, "serviceRoot");
#if ASTORIA_LIGHT
if (!serviceRoot.IsAbsoluteUri)
{
serviceRoot = new Uri(System.Windows.Browser.HtmlPage.Document.DocumentUri, serviceRoot);
}
#endif
if (!serviceRoot.IsAbsoluteUri ||
!Uri.IsWellFormedUriString(serviceRoot.OriginalString, UriKind.Absolute) ||
!String.IsNullOrEmpty(serviceRoot.Query) ||
!string.IsNullOrEmpty(serviceRoot.Fragment) ||
((serviceRoot.Scheme != "http") && (serviceRoot.Scheme != "https")))
{
throw Error.Argument(Strings.Context_BaseUri, "serviceRoot");
}
this.baseUri = serviceRoot;
this.baseUriWithSlash = serviceRoot;
if (!serviceRoot.OriginalString.EndsWith("/", StringComparison.Ordinal))
{
this.baseUriWithSlash = Util.CreateUri(serviceRoot.OriginalString + "/", UriKind.Absolute);
}
this.mergeOption = MergeOption.AppendOnly;
this.DataNamespace = XmlConstants.DataWebNamespace;
this.UsePostTunneling = false;
this.typeScheme = new Uri(XmlConstants.DataWebSchemeNamespace);
}
#endregion
#if !ASTORIA_LIGHT // Data.Services http stack
///
/// This event is fired before a request it sent to the server, giving
/// the handler the opportunity to inspect, adjust and/or replace the
/// WebRequest object used to perform the request.
///
///
/// When calling BeginSaveChanges and not using SaveChangesOptions.Batch,
/// this event may be raised from a different thread.
///
public event EventHandler SendingRequest;
#endif
///
/// This event fires once an entry has been read into a .NET object
/// but before the serializer returns to the caller, giving handlers
/// an opporunity to read further information from the incoming ATOM
/// entry and updating the object
///
///
/// This event should only be raised from the thread that was used to
/// invoke Execute, EndExecute, SaveChanges, EndSaveChanges.
///
public event EventHandler ReadingEntity;
///
/// This event fires once an ATOM entry is ready to be written to
/// the network for a request, giving handlers an opportunity to
/// customize the entry with information from the corresponding
/// .NET object or the environment.
///
///
/// When calling BeginSaveChanges and not using SaveChangesOptions.Batch,
/// this event may be raised from a different thread.
///
public event EventHandler WritingEntity;
#region BaseUri, Credentials, MergeOption, Timeout, Links, Entities
///
/// Absolute Uri identifying the root of the target data service.
/// A Uri provided with a trailing slash is equivalent to one without such a trailing character.
///
///
/// Example: http://server/host/myservice.svc
///
public Uri BaseUri
{
get { return this.baseUri; }
}
#if !ASTORIA_LIGHT // Credentials not available
///
/// Gets and sets the authentication information used by each query created using the context object.
///
public System.Net.ICredentials Credentials
{
get { return this.credentials; }
set { this.credentials = value; }
}
#endif
///
/// Used to specify a synchronization strategy when sending/receiving entities to/from a data service.
/// This value is read by the deserialization component of the client prior to materializing objects.
/// As such, it is recommended to set this property to the appropriate materialization strategy
/// before executing any queries/updates to the data service.
///
///
/// The default value is .AppendOnly.
///
public MergeOption MergeOption
{
get { return this.mergeOption; }
set { this.mergeOption = Util.CheckEnumerationValue(value, "MergeOption"); }
}
///
/// Are properties missing from target type ignored?
///
///
/// This also affects responses during SaveChanges.
///
public bool IgnoreMissingProperties
{
get { return this.ignoreMissingProperties; }
set { this.ignoreMissingProperties = value; }
}
/// Override the namespace used for the data parts of the ATOM entries
public string DataNamespace
{
get
{
return this.dataNamespace;
}
set
{
Util.CheckArgumentNull(value, "value");
this.dataNamespace = value;
}
}
///
/// Enables one to override the default type resolution strategy used by the client library.
/// Set this property to a delegate which identifies a function that resolves
/// a type within the client application to a namespace-qualified type name.
/// This enables the client to perform custom mapping between the type name
/// provided in a response from the server and a type on the client.
///
///
/// This method enables one to override the entity name that is serialized
/// to the target representation (ATOM,JSON, etc) for the specified type.
///
public Func ResolveName
{
get { return this.resolveName; }
set { this.resolveName = value; }
}
///
/// Enables one to override the default type resolution strategy used by the client library.
/// Set this property to a delegate which identifies a function that resolves a
/// namespace-qualified type name to type within the client application.
/// This enables the client to perform custom mapping between the type name
/// provided in a response from the server and a type on the client.
///
///
/// Overriding type resolution enables inserting a custom type name to type mapping strategy.
/// It does not enable one to affect how a response is materialized into the identified type.
///
public Func ResolveType
{
get { return this.resolveType; }
set { this.resolveType = value; }
}
#if !ASTORIA_LIGHT // Timeout not available
///
/// Get and sets the timeout span in seconds to use for the underlying HTTP request to the data service.
///
///
/// A value of 0 will use the default timeout of the underlying HTTP request.
/// This value must be set before executing any query or update operations against
/// the target data service for it to have effect on the on the request.
/// The value may be changed between requests to a data service and the new value
/// will be picked up by the next data service request.
///
public int Timeout
{
get
{
return this.timeout;
}
set
{
if (value < 0)
{
throw Error.ArgumentOutOfRange("Timeout");
}
this.timeout = value;
}
}
#endif
/// Gets or sets the URI used to indicate what type scheme is used by the service.
public Uri TypeScheme
{
get
{
return this.typeScheme;
}
set
{
Util.CheckArgumentNull(value, "value");
this.typeScheme = value;
}
}
/// whether to use post-tunneling for PUT/DELETE
public bool UsePostTunneling
{
get { return this.postTunneling; }
set { this.postTunneling = value; }
}
///
/// Returns a collection of all the links (ie. associations) currently being tracked by the context.
/// If no links are being tracked, a collection with 0 elements is returned.
///
public ReadOnlyCollection Links
{
get
{
return (from link in this.bindings.Values
orderby link.ChangeOrder
select new LinkDescriptor(link.SourceResource, link.SourceProperty, link.TargetResouce, link.State))
.ToList().AsReadOnly();
}
}
///
/// Returns a collection of all the resources currently being tracked by the context.
/// If no resources are being tracked, a collection with 0 elements is returned.
///
public ReadOnlyCollection Entities
{
get
{
return (from entity in this.objectToResource.Values
orderby entity.ChangeOrder
select new EntityDescriptor(entity.Resource, entity.ETag, entity.State))
.ToList().AsReadOnly();
}
}
///
/// Default SaveChangesOptions that needs to be used when doing SaveChanges.
///
public SaveChangesOptions SaveChangesDefaultOptions
{
get
{
return this.saveChangesDefaultOptions;
}
set
{
ValidateSaveChangesOptions(value);
this.saveChangesDefaultOptions = value;
}
}
#endregion
/// base uri with guranteed trailing slash
internal Uri BaseUriWithSlash
{
get { return this.baseUriWithSlash; }
}
/// Indicates if there are subscribers for the ReadingEntity event
internal bool HasReadingEntityHandlers
{
[DebuggerStepThrough]
get { return this.ReadingEntity != null; }
}
#region CreateQuery
///
/// create a query based on (BaseUri + relativeUri)
///
/// type of object to materialize
/// entitySetName
/// composible, enumerable query object
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "required for this feature")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads", Justification = "required for this feature")]
public DataServiceQuery CreateQuery(string entitySetName)
{
Util.CheckArgumentNotEmpty(entitySetName, "entitySetName");
this.ValidateEntitySetName(ref entitySetName);
ResourceSetExpression rse = new ResourceSetExpression(typeof(IOrderedQueryable), null, Expression.Constant(entitySetName), typeof(T), null, null);
return new DataServiceQuery.DataServiceOrderedQuery(rse, new DataServiceQueryProvider(this));
}
#endregion
#region GetMetadataUri
///
/// Given the base URI, resolves the location of the metadata endpoint for the service by using an HTTP OPTIONS request or falling back to convention ($metadata)
///
/// Uri to retrieve metadata from
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "required for this feature")]
public Uri GetMetadataUri()
{
//
Uri metadataUri = Util.CreateUri(this.baseUriWithSlash.OriginalString + XmlConstants.UriMetadataSegment, UriKind.Absolute);
return metadataUri;
}
#endregion
#region LoadProperty
///
/// Begin getting response to load a collection or reference property.
///
/// actually doesn't modify the property until EndLoadProperty is called.
/// entity
/// name of collection or reference property to load
/// The AsyncCallback delegate.
/// The state object for this request.
/// An IAsyncResult that references the asynchronous request for a response.
public IAsyncResult BeginLoadProperty(object entity, string propertyName, AsyncCallback callback, object state)
{
LoadPropertyAsyncResult result = this.CreateLoadPropertyRequest(entity, propertyName, callback, state);
result.BeginExecute(null);
return result;
}
///
/// Load a collection or reference property from a async result.
///
/// async result generated by BeginLoadProperty
/// QueryOperationResponse instance containing information about the response.
public QueryOperationResponse EndLoadProperty(IAsyncResult asyncResult)
{
LoadPropertyAsyncResult response = QueryAsyncResult.EndExecute(this, "LoadProperty", asyncResult);
return response.LoadProperty();
}
#if !ASTORIA_LIGHT // Synchronous methods not available
///
/// Load a collection or reference property.
///
///
/// An entity in detached or added state will throw an InvalidOperationException
/// since there is nothing it can load from the server.
///
/// An entity in unchanged or modified state will load its collection or
/// reference elements as unchanged with unchanged bindings.
///
/// An entity in deleted state will loads its collection or reference elements
/// in the unchanged state with bindings in the deleted state.
///
/// entity
/// name of collection or reference property to load
/// QueryOperationResponse instance containing information about the response.
public QueryOperationResponse LoadProperty(object entity, string propertyName)
{
LoadPropertyAsyncResult result = this.CreateLoadPropertyRequest(entity, propertyName, null, null);
result.Execute(null);
return result.LoadProperty();
}
#endif
#endregion
#region ExecuteBatch, BeginExecuteBatch, EndExecuteBatch
///
/// Batch multiple queries into a single async request.
///
/// User callback when results from batch are available.
/// user state in IAsyncResult
/// queries to batch
/// async result object
public IAsyncResult BeginExecuteBatch(AsyncCallback callback, object state, params DataServiceRequest[] queries)
{
Util.CheckArgumentNotEmpty(queries, "queries");
SaveAsyncResult result = new SaveAsyncResult(this, "ExecuteBatch", queries, SaveChangesOptions.Batch, callback, state, true);
result.BatchBeginRequest(false /*replaceOnUpdate*/);
return result;
}
///
/// Call when results from batch are desired.
///
/// async result object returned from BeginExecuteBatch
/// batch response from which query results can be enumerated.
public DataServiceResponse EndExecuteBatch(IAsyncResult asyncResult)
{
SaveAsyncResult result = BaseAsyncResult.EndExecute(this, "ExecuteBatch", asyncResult);
return result.EndRequest();
}
#if !ASTORIA_LIGHT // Synchronous methods not available
///
/// Batch multiple queries into a single request.
///
/// queries to batch
/// batch response from which query results can be enumerated.
public DataServiceResponse ExecuteBatch(params DataServiceRequest[] queries)
{
Util.CheckArgumentNotEmpty(queries, "queries");
SaveAsyncResult result = new SaveAsyncResult(this, "ExecuteBatch", queries, SaveChangesOptions.Batch, null, null, false);
result.BatchRequest(false /*replaceOnUpdate*/);
return result.EndRequest();
}
#endif
#endregion
#region Execute(Uri), BeginExecute(Uri), EndExecute(Uri)
/// begin the execution of the request uri
/// element type of the result
/// request to execute
/// User callback when results from execution are available.
/// user state in IAsyncResult
/// async result object
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Type is used to infer result")]
public IAsyncResult BeginExecute(Uri requestUri, AsyncCallback callback, object state)
{
requestUri = Util.CreateUri(this.baseUriWithSlash, requestUri);
return (new DataServiceRequest(requestUri)).BeginExecute(this, this, callback, state);
}
///
/// Call when results from batch are desired.
///
/// element type of the result
/// async result object returned from BeginExecuteBatch
/// batch response from which query results can be enumerated.
/// asyncResult is null
/// asyncResult did not originate from this instance or End was previously called
/// problem in request or materializing results of query into objects
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Type is used to infer result")]
public IEnumerable EndExecute(IAsyncResult asyncResult)
{
QueryAsyncResult response = QueryAsyncResult.EndExecute(this, "Execute", asyncResult);
IEnumerable results = response.ServiceRequest.Materialize(this, response.ContentType, response.GetResponseStream).Cast();
return (IEnumerable)response.GetResponse(results, typeof(TElement));
}
#if !ASTORIA_LIGHT // Synchronous methods not available
///
/// Execute the requestUri
///
/// element type of the result
/// request uri to execute
/// batch response from which query results can be enumerated.
/// null requestUri
/// !BaseUri.IsBaseOf(requestUri)
/// problem materializing results of query into objects
/// failure to get response for requestUri
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Type is used to infer result")]
public IEnumerable Execute(Uri requestUri)
{
requestUri = Util.CreateUri(this.baseUriWithSlash, requestUri);
DataServiceRequest request = DataServiceRequest.GetInstance(typeof(TElement), requestUri);
return request.Execute(this, requestUri);
}
#endif
#endregion
#region SaveChanges, BeginSaveChanges, EndSaveChanges
///
/// submit changes to the server in a single change set
///
/// callback
/// state
/// async result
public IAsyncResult BeginSaveChanges(AsyncCallback callback, object state)
{
return this.BeginSaveChanges(this.SaveChangesDefaultOptions, callback, state);
}
///
/// begin submitting changes to the server
///
/// options on how to save changes
/// The AsyncCallback delegate.
/// The state object for this request.
/// An IAsyncResult that references the asynchronous request for a response.
public IAsyncResult BeginSaveChanges(SaveChangesOptions options, AsyncCallback callback, object state)
{
ValidateSaveChangesOptions(options);
SaveAsyncResult result = new SaveAsyncResult(this, "SaveChanges", null, options, callback, state, true);
bool replaceOnUpdate = IsFlagSet(options, SaveChangesOptions.ReplaceOnUpdate);
if (IsFlagSet(options, SaveChangesOptions.Batch))
{
result.BatchBeginRequest(replaceOnUpdate);
}
else
{
result.BeginNextChange(replaceOnUpdate); // may invoke callback before returning
}
return result;
}
///
/// done submitting changes to the server
///
/// The pending request for a response.
/// changeset response
public DataServiceResponse EndSaveChanges(IAsyncResult asyncResult)
{
SaveAsyncResult result = BaseAsyncResult.EndExecute(this, "SaveChanges", asyncResult);
return result.EndRequest();
}
#if !ASTORIA_LIGHT // Synchronous methods not available
///
/// submit changes to the server in a single change set
///
/// changeset response
public DataServiceResponse SaveChanges()
{
return this.SaveChanges(this.SaveChangesDefaultOptions);
}
///
/// submit changes to the server
///
/// options on how to save changes
/// changeset response
///
/// MergeOption.NoTracking is tricky but supported because to insert a relationship we need the identity
/// of both ends and if one end was an inserted object then its identity is attached, but may not match its value
///
/// This initial implementation does not do batching.
/// Things are sent to the server in the following order
/// 1) delete relationships
/// 2) delete objects
/// 3) update objects
/// 4) insert objects
/// 5) insert relationship
///
public DataServiceResponse SaveChanges(SaveChangesOptions options)
{
DataServiceResponse errors = null;
ValidateSaveChangesOptions(options);
SaveAsyncResult result = new SaveAsyncResult(this, "SaveChanges", null, options, null, null, false);
bool replaceOnUpdate = IsFlagSet(options, SaveChangesOptions.ReplaceOnUpdate);
if (IsFlagSet(options, SaveChangesOptions.Batch))
{
result.BatchRequest(replaceOnUpdate);
}
else
{
result.BeginNextChange(replaceOnUpdate);
}
errors = result.EndRequest();
Debug.Assert(null != errors, "null errors");
return errors;
}
#endif
#endregion
#region Add, Attach, Delete, Detach, Update, TryGetEntity, TryGetUri
///
/// Notifies the context that a new link exists between the and objects
/// and that the link is represented via the source. which is a collection.
/// The context adds this link to the set of newly created links to be sent to
/// the data service on the next call to SaveChanges().
///
///
/// Links are one way relationships. If a back pointer exists (ie. two way association),
/// this method should be called a second time to notify the context object of the second link.
///
/// Source object participating in the link.
/// The name of the property on the source object which represents a link from the source to the target object.
/// The target object involved in the link which is bound to the source object also specified in this call.
/// If , or are null.
/// if link already exists
/// if source or target are detached
/// if source or target are in deleted state
/// if sourceProperty is not a collection
public void AddLink(object source, string sourceProperty, object target)
{
this.EnsureRelatable(source, sourceProperty, target, EntityStates.Added);
RelatedEnd relation = new RelatedEnd(source, sourceProperty, target);
if (this.bindings.ContainsKey(relation))
{
throw Error.InvalidOperation(Strings.Context_RelationAlreadyContained);
}
relation.State = EntityStates.Added;
this.bindings.Add(relation, relation);
this.objectToResource[source].RelatedLinkCount++;
this.IncrementChange(relation);
}
///
/// Notifies the context to start tracking the specified link between source and the specified target entity.
///
/// Source object participating in the link.
/// The name of the property on the source object which represents a link from the source to the target object.
/// The target object involved in the link which is bound to the source object also specified in this call.
/// If , or are null.
/// if binding already exists
/// if source or target are in added state
/// if source or target are in deleted state
public void AttachLink(object source, string sourceProperty, object target)
{
this.AttachLink(source, sourceProperty, target, MergeOption.NoTracking);
}
///
/// Removes the specified link from the list of links being tracked by the context.
/// Any link being tracked by the context, regardless of its current state, can be detached.
///
/// Source object participating in the link.
/// The name of the property on the source object which represents a link from the source to the target object.
/// The target object involved in the link which is bound to the source object also specified in this call.
/// If or are null.
/// if sourceProperty is empty
/// true if binding was previously being tracked, false if not
public bool DetachLink(object source, string sourceProperty, object target)
{
Util.CheckArgumentNull(source, "source");
Util.CheckArgumentNotEmpty(sourceProperty, "sourceProperty");
RelatedEnd existing;
RelatedEnd relation = new RelatedEnd(source, sourceProperty, target);
if (!this.bindings.TryGetValue(relation, out existing))
{
return false;
}
this.DetachExistingLink(existing);
return true;
}
///
/// Notifies the context that a link exists between the and object
/// and that the link is represented via the source. which is a collection.
/// The context adds this link to the set of deleted links to be sent to
/// the data service on the next call to SaveChanges().
/// If the specified link exists in the "Added" state, then the link is detached (see DetachLink method) instead.
///
/// Source object participating in the link.
/// The name of the property on the source object which represents a link from the source to the target object.
/// The target object involved in the link which is bound to the source object also specified in this call.
/// If , or are null.
/// if source or target are detached
/// if source or target are in added state
/// if sourceProperty is not a collection
public void DeleteLink(object source, string sourceProperty, object target)
{
bool delay = this.EnsureRelatable(source, sourceProperty, target, EntityStates.Deleted);
RelatedEnd existing = null;
RelatedEnd relation = new RelatedEnd(source, sourceProperty, target);
if (this.bindings.TryGetValue(relation, out existing) && (EntityStates.Added == existing.State))
{ // Added -> Detached
this.DetachExistingLink(existing);
}
else
{
if (delay)
{ // can't have non-added relationship when source or target is in added state
throw Error.InvalidOperation(Strings.Context_NoRelationWithInsertEnd);
}
if (null == existing)
{ // detached -> deleted
this.bindings.Add(relation, relation);
this.objectToResource[source].RelatedLinkCount++;
existing = relation;
}
if (EntityStates.Deleted != existing.State)
{
existing.State = EntityStates.Deleted;
// It is the users responsibility to delete the link
// before deleting the entity when required.
this.IncrementChange(existing);
}
}
}
///
/// Notifies the context that a modified link exists between the and objects
/// and that the link is represented via the source. which is a reference.
/// The context adds this link to the set of modified created links to be sent to
/// the data service on the next call to SaveChanges().
///
///
/// Links are one way relationships. If a back pointer exists (ie. two way association),
/// this method should be called a second time to notify the context object of the second link.
///
/// Source object participating in the link.
/// The name of the property on the source object which represents a link from the source to the target object.
/// The target object involved in the link which is bound to the source object also specified in this call.
/// If , or are null.
/// if link already exists
/// if source or target are detached
/// if source or target are in deleted state
/// if sourceProperty is not a reference property
public void SetLink(object source, string sourceProperty, object target)
{
this.EnsureRelatable(source, sourceProperty, target, EntityStates.Modified);
RelatedEnd relation = this.DetachReferenceLink(source, sourceProperty, target, MergeOption.NoTracking);
if (null == relation)
{
relation = new RelatedEnd(source, sourceProperty, target);
this.bindings.Add(relation, relation);
}
Debug.Assert(
0 == relation.State ||
IncludeLinkState(relation.State),
"set link entity state");
if (EntityStates.Modified != relation.State)
{
relation.State = EntityStates.Modified;
this.objectToResource[source].RelatedLinkCount++;
this.IncrementChange(relation);
}
}
#endregion
#region AddObject, AttachTo, DeleteObject, Detach, TryGetEntity, TryGetUri
///
/// Add entity into the context in the Added state for tracking.
/// It does not follow the object graph and add related objects.
///
/// EntitySet for the object to be added.
/// entity graph to add
/// if entitySetName is null
/// if entitySetName is empty
/// if entity is null
/// if entity does not have a key property
/// if entity is already being tracked by the context
///
/// Any leading or trailing forward slashes will automatically be trimmed from entitySetName.
///
public void AddObject(string entitySetName, object entity)
{
this.ValidateEntitySetName(ref entitySetName);
ValidateEntityWithKey(entity);
Uri editLink = Util.CreateUri(entitySetName, UriKind.Relative);
ResourceBox resource = new ResourceBox(NoIdentity, editLink, entity);
resource.State = EntityStates.Added;
try
{
this.objectToResource.Add(entity, resource);
}
catch (ArgumentException)
{
throw Error.InvalidOperation(Strings.Context_EntityAlreadyContained);
}
this.IncrementChange(resource);
}
///
/// Attach entity into the context in the Unchanged state for tracking.
/// It does not follow the object graph and attach related objects.
///
/// EntitySet for the object to be attached.
/// entity graph to attach
/// if entitySetName is null
/// if entitySetName is empty
/// if entity is null
/// if entity does not have a key property
/// if entity is already being tracked by the context
public void AttachTo(string entitySetName, object entity)
{
this.AttachTo(entitySetName, entity, NoETag);
}
///
/// Attach entity into the context in the Unchanged state for tracking.
/// It does not follow the object graph and attach related objects.
///
/// EntitySet for the object to be attached.
/// entity graph to attach
/// etag
/// if entitySetName is null
/// if entitySetName is empty
/// if entity is null
/// if entity does not have a key property
/// if entity is already being tracked by the context
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", MessageId = "etag", Justification = "represents ETag in request")]
public void AttachTo(string entitySetName, object entity, string etag)
{
this.ValidateEntitySetName(ref entitySetName);
ValidateEntityWithKey(entity);
Uri editLink = GenerateEditLinkUri(this.baseUriWithSlash, entitySetName, entity);
// we fake the identity by using the generated edit link
// ReferenceIdentity is a test hook to help verify we dont' use identity instead of editLink
Uri identity = Util.ReferenceIdentity(editLink);
this.AttachTo(identity, editLink, etag, entity, true);
}
///
/// Mark an existing object being tracked by the context for deletion.
///
/// entity to be mark deleted
/// if entity is null
/// if entity is not being tracked by the context
///
/// Existings objects in the Added state become detached.
///
public void DeleteObject(object entity)
{
Util.CheckArgumentNull(entity, "entity");
ResourceBox resource = null;
if (!this.objectToResource.TryGetValue(entity, out resource))
{ // detached object
throw Error.InvalidOperation(Strings.Context_EntityNotContained);
}
EntityStates state = resource.State;
if (EntityStates.Added == state)
{ // added -> detach
if ((null != resource.Identity) &&
!this.identityToResource.Remove(resource.Identity))
{ // added objects can have identity via NoTracking
Debug.Assert(false, "didn't remove identity");
}
this.DetachRelated(resource);
resource.State = EntityStates.Detached;
bool flag = this.objectToResource.Remove(entity);
Debug.Assert(flag, "should have removed existing entity");
}
else if (EntityStates.Deleted != state)
{
Debug.Assert(
IncludeLinkState(state),
"bad state transition to deleted");
// Leave related links alone which means we can have a link in the Added
// or Modified state referencing a source/target entity in the Deleted state.
resource.State = EntityStates.Deleted;
this.IncrementChange(resource);
}
}
///
/// Detach entity from the context.
///
/// entity to detach.
/// true if object was detached
/// if entity is null
public bool Detach(object entity)
{
Util.CheckArgumentNull(entity, "entity");
ResourceBox resource = null;
if (this.objectToResource.TryGetValue(entity, out resource))
{
return this.DetachResource(resource);
}
return false;
}
///
/// Mark an existing object for update in the context.
///
/// entity to be mark for update
/// if entity is null
/// if entity is detached
public void UpdateObject(object entity)
{
Util.CheckArgumentNull(entity, "entity");
ResourceBox resource = null;
if (!this.objectToResource.TryGetValue(entity, out resource))
{
throw Error.Argument(Strings.Context_EntityNotContained, "entity");
}
if (EntityStates.Unchanged == resource.State)
{
resource.State = EntityStates.Modified;
this.IncrementChange(resource);
}
}
///
/// Find tracked entity by its identity.
///
/// entities in added state are not likely to have a identity
/// entity type
/// identity
/// entity being tracked by context
/// true if entity was found
/// identity is null
public bool TryGetEntity(Uri identity, out TEntity entity) where TEntity : class
{
entity = null;
Util.CheckArgumentNull(identity, "relativeUri");
// ReferenceIdentity is a test hook to help verify we dont' use identity instead of editLink
identity = Util.ReferenceIdentity(identity);
EntityStates state;
entity = (TEntity)this.TryGetEntity(identity, null, MergeOption.AppendOnly, out state);
return (null != entity);
}
///
/// Identity uri for tracked entity.
/// Though the identity might use a dereferencable scheme, you MUST NOT assume it can be dereferenced.
///
/// Entities in added state are not likely to have an identity.
/// entity being tracked by context
/// identity
/// true if entity is being tracked and has a identity
/// entity is null
public bool TryGetUri(object entity, out Uri identity)
{
identity = null;
Util.CheckArgumentNull(entity, "entity");
ResourceBox resource = null;
if (this.objectToResource.TryGetValue(entity, out resource) &&
(null != resource.Identity))
{
// DereferenceIdentity is a test hook to help verify we dont' use identity instead of editLink
identity = Util.DereferenceIdentity(resource.Identity);
}
return (null != identity);
}
///
/// Handle response by looking at status and possibly throwing an exception.
///
/// response status code
/// Version string on the response header; possibly null.
/// delegate to get response stream
/// throw or return on failure
/// exception on failure
internal static Exception HandleResponse(
HttpStatusCode statusCode,
string responseVersion,
Func getResponseStream,
bool throwOnFailure)
{
InvalidOperationException failure = null;
if (!CanHandleResponseVersion(responseVersion))
{
string description = Strings.Context_VersionNotSupported(
responseVersion,
XmlConstants.DataServiceClientVersionCurrentMajor,
XmlConstants.DataServiceClientVersionCurrentMinor);
failure = Error.InvalidOperation(description);
}
if (failure == null && !WebUtil.SuccessStatusCode(statusCode))
{
failure = GetResponseText(getResponseStream, statusCode);
}
if (failure != null && throwOnFailure)
{
throw failure;
}
return failure;
}
/// response materialization has an identity to attach to the inserted object
/// identity of entity
/// edit link of entity
/// inserted object
/// etag of attached object
internal void AttachIdentity(Uri identity, Uri editLink, object entity, string etag)
{ // insert->unchanged
Debug.Assert(null != identity && identity.IsAbsoluteUri, "must have identity");
this.EnsureIdentityToResource();
ResourceBox resource = this.objectToResource[entity];
Debug.Assert(EntityStates.Added == resource.State, "didn't find expected entity in added state");
if ((null != resource.Identity) &&
!this.identityToResource.Remove(resource.Identity))
{
Debug.Assert(false, "didn't remove added identity");
}
resource.ETag = etag;
resource.Identity = identity; // always attach the identity
resource.EditLink = editLink;
resource.State = EntityStates.Unchanged;
this.identityToResource.Add(identity, resource);
}
/// use location from header to generate initial edit and identity
/// entity in added state
/// location from post header
internal void AttachLocation(object entity, string location)
{
Debug.Assert(null != entity, "null != entity");
Uri editLink = new Uri(location, UriKind.Absolute);
Uri identity = Util.ReferenceIdentity(editLink);
this.EnsureIdentityToResource();
ResourceBox resource = this.objectToResource[entity];
Debug.Assert(EntityStates.Added == resource.State, "didn't find expected entity in added state");
if ((null != resource.Identity) &&
!this.identityToResource.Remove(resource.Identity))
{
Debug.Assert(false, "didn't remove added identity");
}
resource.Identity = identity; // always attach the identity
resource.EditLink = editLink;
this.identityToResource.Add(identity, resource);
}
///
/// Track a binding.
///
/// Source resource.
/// Property on the source resource that relates to the target resource.
/// Target resource.
/// merge operation
internal void AttachLink(object source, string sourceProperty, object target, MergeOption linkMerge)
{
this.EnsureRelatable(source, sourceProperty, target, EntityStates.Unchanged);
RelatedEnd existing = null;
RelatedEnd relation = new RelatedEnd(source, sourceProperty, target);
if (this.bindings.TryGetValue(relation, out existing))
{
switch (linkMerge)
{
case MergeOption.AppendOnly:
break;
case MergeOption.OverwriteChanges:
relation = existing;
break;
case MergeOption.PreserveChanges:
if ((EntityStates.Added == existing.State) ||
(EntityStates.Unchanged == existing.State) ||
(EntityStates.Modified == existing.State && null != existing.TargetResouce))
{
relation = existing;
}
break;
case MergeOption.NoTracking: // public API point should throw if link exists
throw Error.InvalidOperation(Strings.Context_RelationAlreadyContained);
}
}
else
{
bool collectionProperty = (null != ClientType.Create(source.GetType()).GetProperty(sourceProperty, false).CollectionType);
if (collectionProperty || (null == (existing = this.DetachReferenceLink(source, sourceProperty, target, linkMerge))))
{
this.bindings.Add(relation, relation);
this.objectToResource[source].RelatedLinkCount++;
this.IncrementChange(relation);
}
else if (!((MergeOption.AppendOnly == linkMerge) ||
(MergeOption.PreserveChanges == linkMerge && EntityStates.Modified == existing.State)))
{
// AppendOnly doesn't change state or target
// OverWriteChanges changes target and state
// PreserveChanges changes target if unchanged, leaves modified target and state alone
relation = existing;
}
}
relation.State = EntityStates.Unchanged;
}
///
/// Attach entity into the context in the Unchanged state.
///
/// Identity for the object to be attached.
/// EntitySet for the object to be attached.
/// etag for the entity
/// entity graph to attach
/// fail for public api else change existing relationship to unchanged
/// if entitySetName is empty
/// if entitySetName is null
/// if entity is null
/// if entity is already being tracked by the context
internal void AttachTo(Uri identity, Uri editLink, string etag, object entity, bool fail)
{
Debug.Assert((null != identity && identity.IsAbsoluteUri), "must have identity");
Debug.Assert(null != editLink, "must have editLink");
Debug.Assert(null != entity && ClientType.Create(entity.GetType()).HasKeys, "entity must have keys to attach");
this.EnsureIdentityToResource();
Debug.Assert(identity.IsAbsoluteUri, "Uri is not absolute");
ResourceBox resource;
this.objectToResource.TryGetValue(entity, out resource);
ResourceBox existing;
this.identityToResource.TryGetValue(identity, out existing);
if (fail && (null != resource))
{
throw Error.InvalidOperation(Strings.Context_EntityAlreadyContained);
}
else if (resource != existing)
{
throw Error.InvalidOperation(Strings.Context_DifferentEntityAlreadyContained);
}
else if (null == resource)
{
resource = new ResourceBox(identity, editLink, entity);
this.IncrementChange(resource);
this.objectToResource.Add(entity, resource);
this.identityToResource.Add(identity, resource);
}
resource.State = EntityStates.Unchanged;
resource.ETag = etag;
}
#endregion
///
/// create the request object
///
/// requestUri
/// updating
/// Whether the request/response should request/assume ATOM or any MIME type
/// content type for the request
/// a request ready to get a response
internal HttpWebRequest CreateRequest(Uri requestUri, string method, bool allowAnyType, string contentType)
{
Debug.Assert(null != requestUri, "request uri is null");
Debug.Assert(requestUri.IsAbsoluteUri, "request uri is not absolute uri");
Debug.Assert(UriUtil.UriInvariantInsensitiveIsBaseOf(this.baseUriWithSlash, requestUri), "context is not base of request uri");
Debug.Assert(
Object.ReferenceEquals(XmlConstants.HttpMethodDelete, method) ||
Object.ReferenceEquals(XmlConstants.HttpMethodGet, method) ||
Object.ReferenceEquals(XmlConstants.HttpMethodPost, method) ||
Object.ReferenceEquals(XmlConstants.HttpMethodPut, method) ||
Object.ReferenceEquals(XmlConstants.HttpMethodMerge, method),
"unexpected http method string reference");
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(requestUri);
#if !ASTORIA_LIGHT // Credentials not available
if (null != this.Credentials)
{
request.Credentials = this.Credentials;
}
#endif
#if !ASTORIA_LIGHT // Timeout not available
if (0 != this.timeout)
{
request.Timeout = (int)Math.Min(Int32.MaxValue, new TimeSpan(0, 0, this.timeout).TotalMilliseconds);
}
#endif
#if !ASTORIA_LIGHT // KeepAlive not available
request.KeepAlive = true;
#endif
#if !ASTORIA_LIGHT // UserAgent not available
request.UserAgent = "Microsoft ADO.NET Data Services";
#endif
if (this.UsePostTunneling &&
(!Object.ReferenceEquals(XmlConstants.HttpMethodPost, method)) &&
(!Object.ReferenceEquals(XmlConstants.HttpMethodGet, method)))
{
request.Headers[XmlConstants.HttpXMethod] = method;
request.Method = XmlConstants.HttpMethodPost;
}
else
{
request.Method = method;
}
#if !ASTORIA_LIGHT // Data.Services http stack
// Fires whenever a new HttpWebRequest has been created
// The event fires early - before the client library sets many of its required property values.
// This ensures the client library has the last say on the value of mandated properties
// such as the HTTP verb being used for the request.
if (this.SendingRequest != null)
{
SendingRequestEventArgs args = new SendingRequestEventArgs(request);
this.SendingRequest(this, args);
if (!Object.ReferenceEquals(args.Request, request))
{
request = (HttpWebRequest)args.Request;
}
}
#endif
request.Accept = allowAnyType ?
XmlConstants.MimeAny :
(XmlConstants.MimeApplicationAtom + "," + XmlConstants.MimeApplicationXml);
request.Headers[HttpRequestHeader.AcceptCharset] = XmlConstants.Utf8Encoding;
// Always sending the version along allows the server to fail before processing.
request.Headers[XmlConstants.HttpDataServiceVersion] = XmlConstants.DataServiceClientVersionCurrent;
request.Headers[XmlConstants.HttpMaxDataServiceVersion] = XmlConstants.DataServiceClientVersionCurrent;
#if !ASTORIA_LIGHT // AllowWriteStreamBuffering not available
bool allowStreamBuffering = false;
#endif
bool removeXMethod = true;
if (!Object.ReferenceEquals(XmlConstants.HttpMethodGet, method))
{
Debug.Assert(!String.IsNullOrEmpty(contentType), "Content-Type must be specified for non get operation");
request.ContentType = contentType;
if (Object.ReferenceEquals(XmlConstants.HttpMethodDelete, method))
{
request.ContentLength = 0;
}
#if !ASTORIA_LIGHT // AllowWriteStreamBuffering not available
// else
{ // always set to workaround NullReferenceException in HttpWebRequest.GetResponse when ContentLength = 0
allowStreamBuffering = true;
}
#endif
if (this.UsePostTunneling && (!Object.ReferenceEquals(XmlConstants.HttpMethodPost, method)))
{
request.Headers[XmlConstants.HttpXMethod] = method;
method = XmlConstants.HttpMethodPost;
removeXMethod = false;
}
}
else
{
Debug.Assert(contentType == null, "Content-Type for get methods should be null");
}
#if !ASTORIA_LIGHT // AllowWriteStreamBuffering not available
// When AllowWriteStreamBuffering is true, the data is buffered in memory so it is ready to be resent
// in the event of redirections or authentication requests.
request.AllowWriteStreamBuffering = allowStreamBuffering;
#endif
ICollection headers;
#if !ASTORIA_LIGHT // alternate Headers.AllKeys
headers = request.Headers.AllKeys;
#else
headers = request.Headers.Headers;
#endif
if (headers.Contains(XmlConstants.HttpRequestIfMatch))
{
#if !ASTORIA_LIGHT // alternate IfMatch header doesn't work
request.Headers.Remove(HttpRequestHeader.IfMatch);
#endif
}
if (removeXMethod && headers.Contains(XmlConstants.HttpXMethod))
{
#if !ASTORIA_LIGHT // alternate HttpXMethod header doesn't work
request.Headers.Remove(XmlConstants.HttpXMethod);
#endif
}
request.Method = method;
return request;
}
///
/// get an enumerable materializes the objects the response
///
/// http response
/// base elementType being materialized
/// delegate to create the materializer
/// an enumerable
internal MaterializeAtom GetMaterializer(QueryAsyncResult response, Type elementType, Func create)
{
if (HttpStatusCode.NoContent == response.StatusCode)
{ // object was deleted
return null;
}
if (HttpStatusCode.Created == response.StatusCode &&
response.ContentLength == 0)
{
// created but no response back
return null;
}
if (null != response)
{
return (MaterializeAtom)this.GetMaterializer(elementType, response.ContentType, response.GetResponseStream, create);
}
return null;
}
///
/// get an enumerable materializes the objects the response
///
/// elementType
/// contentType
/// method to get http response stream
/// method to create a materializer
/// an enumerable
internal object GetMaterializer(
Type elementType,
string contentType,
Func response,
Func create)
{
Debug.Assert(null != create, "null create");
string mime = null;
Encoding encoding = null;
if (!String.IsNullOrEmpty(contentType))
{
HttpProcessUtility.ReadContentType(contentType, out mime, out encoding);
}
if (String.Equals(mime, XmlConstants.MimeApplicationAtom, StringComparison.OrdinalIgnoreCase) ||
String.Equals(mime, XmlConstants.MimeApplicationXml, StringComparison.OrdinalIgnoreCase))
{
System.IO.Stream rstream = response();
if (null != rstream)
{
XmlReader reader = XmlUtil.CreateXmlReader(rstream, encoding);
return create(this, reader, elementType);
}
return null;
}
throw Error.InvalidOperation(Strings.Deserialize_UnknownMimeTypeSpecified(mime));
}
///
/// Find tracked entity by its resourceUri and update its etag.
///
/// resource id
/// updated etag
/// merge option
/// state of entity
/// entity if found else null
internal object TryGetEntity(Uri resourceUri, string etag, MergeOption merger, out EntityStates state)
{
Debug.Assert(null != resourceUri, "null uri");
state = EntityStates.Detached;
ResourceBox resource = null;
if ((null != this.identityToResource) &&
this.identityToResource.TryGetValue(resourceUri, out resource))
{
state = resource.State;
if ((null != etag) && (MergeOption.AppendOnly != merger))
{ // don't update the etag if AppendOnly
resource.ETag = etag;
}
Debug.Assert(null != resource.Resource, "null entity");
return resource.Resource;
}
return null;
}
///
/// get the resource box for an entity
///
/// entity
/// resource box
internal ResourceBox GetEntity(object source)
{
return this.objectToResource[source];
}
///
/// get the related links ignoring target entity
///
/// source entity
/// source entity's property
/// enumerable of related ends
internal IEnumerable GetLinks(object source, string sourceProperty)
{
return this.bindings.Values.Where(o => (o.SourceResource == source) && (o.SourceProperty == sourceProperty));
}
///
/// user hook to resolve name into a type
///
/// name to resolve
/// base type associated with name
/// null to skip node
/// if ResolveType function returns a type not assignable to the userType
internal Type ResolveTypeFromName(string wireName, Type userType)
{
Debug.Assert(null != userType, "null != baseType");
if (String.IsNullOrEmpty(wireName))
{
return userType;
}
Type payloadType;
if (!ClientConvert.ToNamedType(wireName, out payloadType))
{
payloadType = null;
Func resolve = this.ResolveType;
if (null != resolve)
{
// if the ResolveType property is set, call the provided type resultion method
payloadType = resolve(wireName);
}
if (null == payloadType)
{
// if the type resolution method returns null or the ResolveType property was not set
#if !ASTORIA_LIGHT
payloadType = ClientType.ResolveFromName(wireName, userType);
#else
payloadType = ClientType.ResolveFromName(wireName, userType, this.GetType());
#endif
}
if ((null != payloadType) && (!userType.IsAssignableFrom(payloadType)))
{
// throw an exception if the type from the resolver is not assignable to the expected type
throw Error.InvalidOperation(Strings.Deserialize_Current(userType, payloadType));
}
}
return payloadType ?? userType;
}
///
/// The reverse of ResolveType
///
/// client type
/// type for the server
internal string ResolveNameFromType(Type type)
{
Debug.Assert(null != type, "null type");
Func resolve = this.ResolveName;
return ((null != resolve) ? resolve(type) : (String)null);
}
///
/// Fires the ReadingEntity event
///
/// Entity being (de)serialized
/// XML data of the ATOM entry
internal void FireReadingEntityEvent(object entity, XElement data)
{
Debug.Assert(entity != null, "entity != null");
Debug.Assert(data != null, "data != null");
ReadingWritingEntityEventArgs args = new ReadingWritingEntityEventArgs(entity, data);
this.ReadingEntity(this, args);
}
#region Ensure
/// Filter to verify states
/// x
/// true if added/updated/deleted
private static bool HasModifiedResourceState(Entry x)
{
Debug.Assert(
(EntityStates.Added == x.State) ||
(EntityStates.Modified == x.State) ||
(EntityStates.Unchanged == x.State) ||
(EntityStates.Deleted == x.State),
"entity state is not valid");
return (EntityStates.Unchanged != x.State);
}
/// modified or unchanged
/// state to test
/// true if modified or unchanged
private static bool IncludeLinkState(EntityStates x)
{
return ((EntityStates.Modified == x) || (EntityStates.Unchanged == x));
}
#endregion
/// Checks whether an ADO.NET Data Service version string can be handled.
/// Version string on the response header; possibly null.
/// true if the version can be handled; false otherwise.
private static bool CanHandleResponseVersion(string responseVersion)
{
if (!String.IsNullOrEmpty(responseVersion))
{
KeyValuePair version;
if (!HttpProcessUtility.TryReadVersion(responseVersion, out version))
{
return false;
}
// For the time being, we only handle 1.0 responses.
if (version.Key.Major != XmlConstants.DataServiceClientVersionCurrentMajor ||
version.Key.Minor != XmlConstants.DataServiceClientVersionCurrentMinor)
{
return false;
}
}
return true;
}
/// generate a Uri based on key properties of the entity
/// baseUri
/// entitySetName
/// entity
/// absolute uri
private static Uri GenerateEditLinkUri(Uri baseUriWithSlash, string entitySetName, object entity)
{
Debug.Assert(null != baseUriWithSlash && baseUriWithSlash.IsAbsoluteUri && baseUriWithSlash.OriginalString.EndsWith("/", StringComparison.Ordinal), "baseUriWithSlash");
Debug.Assert(!String.IsNullOrEmpty(entitySetName) && !entitySetName.StartsWith("/", StringComparison.Ordinal), "entitySetName");
Debug.Assert(null != entity, "entity");
StringBuilder builder = new StringBuilder();
builder.Append(baseUriWithSlash.AbsoluteUri);
builder.Append(entitySetName);
builder.Append("(");
string prefix = String.Empty;
ClientType clientType = ClientType.Create(entity.GetType());
Debug.Assert(clientType.HasKeys, "requires keys");
ClientType.ClientProperty[] keys = clientType.Properties.Where(ClientType.ClientProperty.GetKeyProperty).ToArray();
foreach (ClientType.ClientProperty property in keys)
{
#if ASTORIA_OPEN_OBJECT
Debug.Assert(!property.OpenObjectProperty, "key property values can't be OpenProperties");
#endif
builder.Append(prefix);
if (1 < keys.Length)
{
builder.Append(property.PropertyName).Append("=");
}
object value = property.GetValue(entity);
if (null == value)
{
throw Error.InvalidOperation(Strings.Serializer_NullKeysAreNotSupported(property.PropertyName));
}
string converted;
if (!ClientConvert.TryKeyPrimitiveToString(value, out converted))
{
throw Error.InvalidOperation(Strings.Deserialize_Current(typeof(string), value.GetType()));
}
builder.Append(converted);
prefix = ",";
}
builder.Append(")");
return Util.CreateUri(builder.ToString(), UriKind.Absolute);
}
/// Get http method string from entity resource state
/// resource state
/// whether we need to update MERGE or PUT method for update.
/// http method string delete, put or post
private static string GetEntityHttpMethod(EntityStates state, bool replaceOnUpdate)
{
switch (state)
{
case EntityStates.Deleted:
return XmlConstants.HttpMethodDelete;
case EntityStates.Modified:
if (replaceOnUpdate)
{
return XmlConstants.HttpMethodPut;
}
else
{
return XmlConstants.HttpMethodMerge;
}
case EntityStates.Added:
return XmlConstants.HttpMethodPost;
default:
throw Error.InternalError(InternalError.UnvalidatedEntityState);
}
}
/// Get http method string from link resource state
/// resource
/// http method string put or post
private static string GetLinkHttpMethod(RelatedEnd link)
{
bool collection = (null != ClientType.Create(link.SourceResource.GetType()).GetProperty(link.SourceProperty, false).CollectionType);
if (!collection)
{
Debug.Assert(EntityStates.Modified == link.State, "not Modified state");
if (null == link.TargetResouce)
{ // REMOVE/DELETE a reference
return XmlConstants.HttpMethodDelete;
}
else
{ // UPDATE/PUT a reference
return XmlConstants.HttpMethodPut;
}
}
else if (EntityStates.Deleted == link.State)
{ // you call DELETE on $links
return XmlConstants.HttpMethodDelete;
}
else
{ // you INSERT/POST into a collection
Debug.Assert(EntityStates.Added == link.State, "not Added state");
return XmlConstants.HttpMethodPost;
}
}
///
/// get the response text into a string
///
/// method to get response stream
/// status code
/// text
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031", Justification = "Cache exception so user can examine it later")]
private static DataServiceClientException GetResponseText(Func getResponseStream, HttpStatusCode statusCode)
{
string message = null;
using (System.IO.Stream stream = getResponseStream())
{
if ((null != stream) && stream.CanRead)
{
message = new StreamReader(stream).ReadToEnd();
}
}
if (String.IsNullOrEmpty(message))
{
message = statusCode.ToString();
}
return new DataServiceClientException(message, (int)statusCode);
}
/// Handle changeset response.
/// headers of changeset response
private static void HandleResponsePost(RelatedEnd entry)
{
if (!((EntityStates.Added == entry.State) || (EntityStates.Modified == entry.State && null != entry.TargetResouce)))
{
Error.ThrowBatchUnexpectedContent(InternalError.LinkNotAddedState);
}
entry.State = EntityStates.Unchanged;
}
/// Handle changeset response.
/// updated entity or link
/// updated etag
private static void HandleResponsePut(Entry entry, string etag)
{
if (entry.IsResource)
{
if (EntityStates.Modified != entry.State)
{
Error.ThrowBatchUnexpectedContent(InternalError.EntryNotModified);
}
entry.State = EntityStates.Unchanged;
((ResourceBox)entry).ETag = etag;
}
else
{
RelatedEnd link = (RelatedEnd)entry;
if ((EntityStates.Added == entry.State) || (EntityStates.Modified == entry.State))
{
link.State = EntityStates.Unchanged;
}
else if (EntityStates.Detached != entry.State)
{ // this link may have been previously detached by a detaching entity
Error.ThrowBatchUnexpectedContent(InternalError.LinkBadState);
}
}
}
///
/// write out an individual property value which can be a primitive or link
///
/// writer
/// namespaceName in which we need to write the property element.
/// property which contains name, type, is key (if false and null value, will throw)
/// property value
private static void WriteContentProperty(XmlWriter writer, string namespaceName, ClientType.ClientProperty property, object propertyValue)
{
writer.WriteStartElement(property.PropertyName, namespaceName);
string typename = ClientConvert.GetEdmType(property.PropertyType);
if (null != typename)
{
writer.WriteAttributeString(XmlConstants.AtomTypeAttributeName, XmlConstants.DataWebMetadataNamespace, typename);
}
if (null == propertyValue)
{ //
writer.WriteAttributeString(XmlConstants.AtomNullAttributeName, XmlConstants.DataWebMetadataNamespace, XmlConstants.XmlTrueLiteral);
if (property.KeyProperty)
{
throw Error.InvalidOperation(Strings.Serializer_NullKeysAreNotSupported(property.PropertyName));
}
}
else
{
string convertedValue = ClientConvert.ToString(propertyValue);
if (0 == convertedValue.Length)
{ //
writer.WriteAttributeString(XmlConstants.AtomNullAttributeName, XmlConstants.DataWebMetadataNamespace, XmlConstants.XmlFalseLiteral);
}
else
{ // value
if (Char.IsWhiteSpace(convertedValue[0]) ||
Char.IsWhiteSpace(convertedValue[convertedValue.Length - 1]))
{ // xml:space="preserve"
writer.WriteAttributeString(XmlConstants.XmlSpaceAttributeName, XmlConstants.XmlNamespacesNamespace, XmlConstants.XmlSpacePreserveValue);
}
writer.WriteValue(convertedValue);
}
}
writer.WriteEndElement();
}
/// validate
/// entity to validate
/// if entity was null
/// if entity does not have a key property
private static void ValidateEntityWithKey(object entity)
{
Util.CheckArgumentNull(entity, "entity");
if (!ClientType.Create(entity.GetType()).HasKeys)
{
throw Error.Argument(Strings.Content_EntityWithoutKey, "entity");
}
}
///
/// Validate the SaveChanges Option
///
/// options as specified by the user.
private static void ValidateSaveChangesOptions(SaveChangesOptions options)
{
const SaveChangesOptions All =
SaveChangesOptions.ContinueOnError |
SaveChangesOptions.Batch |
SaveChangesOptions.ReplaceOnUpdate;
// Make sure no higher order bits are set.
if ((options | All) != All)
{
throw Error.ArgumentOutOfRange("options");
}
// Both batch and continueOnError can't be set together
if (IsFlagSet(options, SaveChangesOptions.Batch | SaveChangesOptions.ContinueOnError))
{
throw Error.ArgumentOutOfRange("options");
}
}
///
/// checks whether the given flag is set on the options
///
/// options as specified by the user.
/// whether the given flag is set on the options
/// true if the given flag is set, otherwise false.
private static bool IsFlagSet(SaveChangesOptions options, SaveChangesOptions flag)
{
return ((options & flag) == flag);
}
///
/// Write the batch headers along with the first http header for the batch operation.
///
/// Stream writer which writes to the underlying stream.
/// HTTP method name for the operation.
/// uri for the operation.
private static void WriteOperationRequestHeaders(StreamWriter writer, string methodName, string uri)
{
writer.WriteLine("{0}: {1}", XmlConstants.HttpContentType, XmlConstants.MimeApplicationHttp);
writer.WriteLine("{0}: {1}", XmlConstants.HttpContentTransferEncoding, XmlConstants.BatchRequestContentTransferEncoding);
writer.WriteLine();
writer.WriteLine("{0} {1} {2}", methodName, uri, XmlConstants.HttpVersionInBatching);
}
///
/// Write the batch headers along with the first http header for the batch operation.
///
/// Stream writer which writes to the underlying stream.
/// status code for the response.
private static void WriteOperationResponseHeaders(StreamWriter writer, int statusCode)
{
writer.WriteLine("{0}: {1}", XmlConstants.HttpContentType, XmlConstants.MimeApplicationHttp);
writer.WriteLine("{0}: {1}", XmlConstants.HttpContentTransferEncoding, XmlConstants.BatchRequestContentTransferEncoding);
writer.WriteLine();
writer.WriteLine("{0} {1} {2}", XmlConstants.HttpVersionInBatching, statusCode, (HttpStatusCode)statusCode);
}
///
/// Check to see if the resource to be inserted is a media entry, and if so
/// setup a POST request for the media content first and turn the rest of
/// the operation into a PUT to update the rest of the properties.
///
/// The resource to check/process
/// A web request setup to do POST the media resource
private HttpWebRequest CheckAndProcessMediaEntry(ResourceBox box)
{
//
ClientType type = ClientType.Create(box.Resource.GetType());
if (type.MediaDataMember == null)
{
// this is not a media link entry, process normally
return null;
}
HttpWebRequest mediaRequest = this.CreateRequest(box.GetResourceUri(this.baseUriWithSlash), XmlConstants.HttpMethodPost, true, XmlConstants.MimeApplicationAtom);
if (type.MediaDataMember.MimeTypeProperty == null)
{
mediaRequest.ContentType = XmlConstants.MimeApplicationOctetStream;
}
else
{
string mimeType = type.MediaDataMember.MimeTypeProperty.GetValue(box.Resource).ToString();
if (string.IsNullOrEmpty(mimeType))
{
throw Error.InvalidOperation(
Strings.Context_NoContentTypeForMediaLink(
type.ElementTypeName,
type.MediaDataMember.MimeTypeProperty.PropertyName));
}
mediaRequest.ContentType = mimeType;
}
object value = type.MediaDataMember.GetValue(box.Resource);
if (value == null)
{
mediaRequest.ContentLength = 0;
}
else
{
byte[] buffer = value as byte[];
if (buffer == null)
{
string mime;
Encoding encoding;
HttpProcessUtility.ReadContentType(mediaRequest.ContentType, out mime, out encoding);
if (encoding == null)
{
encoding = Encoding.UTF8;
mediaRequest.ContentType += XmlConstants.MimeTypeUtf8Encoding;
}
buffer = encoding.GetBytes(ClientConvert.ToString(value));
}
mediaRequest.ContentLength = buffer.Length;
using (Stream s = mediaRequest.GetRequestStream())
{
s.Write(buffer, 0, buffer.Length);
}
}
// Convert the insert into an update for the media link entry we just created
// (note that the identity still needs to be fixed up on the resbox once
// the response comes with the 'location' header; that happens during processing
// of the response in SavedResource())
box.State = EntityStates.Modified;
return mediaRequest;
}
/// the work to detach a resource
/// resource to detach
/// true if detached
private bool DetachResource(ResourceBox resource)
{
this.DetachRelated(resource);
resource.ChangeOrder = UInt32.MaxValue;
resource.State = EntityStates.Detached;
bool flag = this.objectToResource.Remove(resource.Resource);
Debug.Assert(flag, "should have removed existing entity");
if (null != resource.Identity)
{
flag = this.identityToResource.Remove(resource.Identity);
Debug.Assert(flag, "should have removed existing identity");
}
return true;
}
///
/// write out binding payload using POST with http method override for PUT
///
/// binding
/// for non-batching its a request object ready to get a response from else null when batching
private HttpWebRequest CreateRequest(RelatedEnd binding)
{
Debug.Assert(null != binding, "null binding");
if (binding.ContentGeneratedForSave)
{
return null;
}
ResourceBox sourceResource = this.objectToResource[binding.SourceResource];
ResourceBox targetResource = (null != binding.TargetResouce) ? this.objectToResource[binding.TargetResouce] : null;
// these failures should only with SaveChangesOptions.ContinueOnError
if (null == sourceResource.Identity)
{
Debug.Assert(!binding.ContentGeneratedForSave, "already saved link");
binding.ContentGeneratedForSave = true;
Debug.Assert(EntityStates.Added == sourceResource.State, "expected added state");
throw Error.InvalidOperation(Strings.Context_LinkResourceInsertFailure, sourceResource.SaveError);
}
else if ((null != targetResource) && (null == targetResource.Identity))
{
Debug.Assert(!binding.ContentGeneratedForSave, "already saved link");
binding.ContentGeneratedForSave = true;
Debug.Assert(EntityStates.Added == targetResource.State, "expected added state");
throw Error.InvalidOperation(Strings.Context_LinkResourceInsertFailure, targetResource.SaveError);
}
Debug.Assert(null != sourceResource.Identity, "missing sourceResource.Identity");
return this.CreateRequest(this.CreateRequestUri(sourceResource, binding), GetLinkHttpMethod(binding), false, XmlConstants.MimeApplicationXml);
}
/// create the uri for a link
/// edit link of source
/// link
/// appropriate uri for link state
private Uri CreateRequestUri(ResourceBox sourceResource, RelatedEnd binding)
{
Uri requestUri = Util.CreateUri(sourceResource.GetResourceUri(this.baseUriWithSlash), this.CreateRequestRelativeUri(binding));
return requestUri;
}
///
/// create the uri for the link relative to its source entity
///
/// link
/// uri
private Uri CreateRequestRelativeUri(RelatedEnd binding)
{
Uri relative;
bool collection = (null != ClientType.Create(binding.SourceResource.GetType()).GetProperty(binding.SourceProperty, false).CollectionType);
if (collection && (EntityStates.Added != binding.State))
{ // you DELETE(PUT NULL) from a collection
Debug.Assert(null != binding.TargetResouce, "null target in collection");
ResourceBox targetResource = this.objectToResource[binding.TargetResouce];
// For collections, we need to generate the uri with the property name followed by the keys.
// GenerateEditLinkUri generates an absolute uri
// First parameters is the base service uri
// Second parameter is the segment name (in this case, navigation property name)
// Third parameter is the resource whose key values need to be appended after the segment
// For e.g. If the navigation property name is "Purchases" and the resource type is Order with key '1', then this method will generate 'baseuri/Purchases(1)'
Uri navigationPropertyUri = this.BaseUriWithSlash.MakeRelativeUri(DataServiceContext.GenerateEditLinkUri(this.BaseUriWithSlash, binding.SourceProperty, targetResource.Resource));
// Get the relative uri and appends links segment at the start.
relative = Util.CreateUri(XmlConstants.UriLinkSegment + "/" + navigationPropertyUri.OriginalString, UriKind.Relative);
}
else
{ // UPDATE(PUT ID) a reference && INSERT(POST ID) into a collection
relative = Util.CreateUri(XmlConstants.UriLinkSegment + "/" + binding.SourceProperty, UriKind.Relative);
}
Debug.Assert(!relative.IsAbsoluteUri, "should be relative uri");
return relative;
}
///
/// write content to batch text stream
///
/// link
/// batch text stream
private void CreateRequestBatch(RelatedEnd binding, StreamWriter text)
{
Uri relative = this.CreateRequestRelativeUri(binding);
ResourceBox sourceResource = this.objectToResource[binding.SourceResource];
string requestString;
if (null != sourceResource.Identity)
{
requestString = this.CreateRequestUri(sourceResource, binding).AbsoluteUri;
}
else
{
requestString = "$" + sourceResource.ChangeOrder.ToString(CultureInfo.InvariantCulture) + "/" + relative.OriginalString;
}
WriteOperationRequestHeaders(text, GetLinkHttpMethod(binding), requestString);
text.WriteLine("{0}: {1}", XmlConstants.HttpDataServiceVersion, XmlConstants.DataServiceClientVersionCurrent);
text.WriteLine("{0}: {1}", XmlConstants.HttpContentID, binding.ChangeOrder);
// if (EntityStates.Deleted || (EntityState.Modifed && null == TargetResource))
// then the server will fail the batch section if content type exists
if ((EntityStates.Added == binding.State) || (EntityStates.Modified == binding.State && (null != binding.TargetResouce)))
{
text.WriteLine("{0}: {1}", XmlConstants.HttpContentType, XmlConstants.MimeApplicationXml);
}
}
///
/// create content memory stream for link
///
/// link
/// should newline be written
/// memory stream
private MemoryStream CreateRequestData(RelatedEnd binding, bool newline)
{
Debug.Assert(
(binding.State == EntityStates.Added) ||
(binding.State == EntityStates.Modified && null != binding.TargetResouce),
"This method must be called only when a binding is added or put");
MemoryStream stream = new MemoryStream();
XmlWriter writer = XmlUtil.CreateXmlWriterAndWriteProcessingInstruction(stream, HttpProcessUtility.EncodingUtf8NoPreamble);
ResourceBox targetResource = this.objectToResource[binding.TargetResouce];
#region
writer.WriteStartElement(XmlConstants.UriElementName, XmlConstants.DataWebMetadataNamespace);
string id;
if (null != targetResource.Identity)
{
id = Util.DereferenceIdentity(targetResource.Identity).AbsoluteUri;
}
else
{
id = "$" + targetResource.ChangeOrder.ToString(CultureInfo.InvariantCulture);
}
writer.WriteValue(id);
writer.WriteEndElement(); //
#endregion
writer.Flush();
if (newline)
{
// end the xml content stream with a newline
stream.WriteByte((byte)'\r');
stream.WriteByte((byte)'\n');
}
// strip the preamble.
stream.Position = 0;
return stream;
}
///
/// Create HttpWebRequest from a resource
///
/// resource
/// resource state
/// whether we need to update MERGE or PUT method for update.
/// web request
private HttpWebRequest CreateRequest(ResourceBox box, EntityStates state, bool replaceOnUpdate)
{
Debug.Assert(null != box && ((EntityStates.Added == state) || (EntityStates.Modified == state) || (EntityStates.Deleted == state)), "unexpected entity ResourceState");
string httpMethod = GetEntityHttpMethod(state, replaceOnUpdate);
Uri requestUri = box.GetResourceUri(this.baseUriWithSlash);
HttpWebRequest request = this.CreateRequest(requestUri, httpMethod, false, XmlConstants.MimeApplicationAtom);
if ((null != box.ETag) && ((EntityStates.Deleted == state) || (EntityStates.Modified == state)))
{
#if !ASTORIA_LIGHT // different way to write Request headers
request.Headers.Set(HttpRequestHeader.IfMatch, box.ETag);
#else
request.Headers[XmlConstants.HttpRequestIfMatch] = box.ETag;
#endif
}
return request;
}
///
/// generate batch request for entity
///
/// entity
/// batch stream to write to
/// whether we need to update MERGE or PUT method for update.
private void CreateRequestBatch(ResourceBox box, StreamWriter text, bool replaceOnUpdate)
{
Debug.Assert(null != box, "null box");
Debug.Assert(null != text, "null text");
Uri requestUri = box.GetResourceUri(this.baseUriWithSlash);
Debug.Assert(null != requestUri, "request uri is null");
Debug.Assert(requestUri.IsAbsoluteUri, "request uri is not absolute uri");
Debug.Assert(UriUtil.UriInvariantInsensitiveIsBaseOf(this.baseUriWithSlash, requestUri), "context is not base of request uri");
WriteOperationRequestHeaders(text, GetEntityHttpMethod(box.State, replaceOnUpdate), requestUri.AbsoluteUri);
text.WriteLine("{0}: {1}", XmlConstants.HttpContentID, box.ChangeOrder);
if (EntityStates.Deleted != box.State)
{
text.WriteLine("{0}: {1};{2}", XmlConstants.HttpContentType, XmlConstants.MimeApplicationAtom, XmlConstants.MimeTypeEntry);
}
if ((null != box.ETag) && (EntityStates.Deleted == box.State || EntityStates.Modified == box.State))
{
text.WriteLine("{0}: {1}", XmlConstants.HttpRequestIfMatch, box.ETag);
}
}
///
/// create memory stream with entity data
///
/// entity
/// should newline be written
/// memory stream containing data
private MemoryStream CreateRequestData(ResourceBox box, bool newline)
{
Debug.Assert(null != box, "null box");
MemoryStream stream = null;
switch (box.State)
{
case EntityStates.Deleted:
break;
case EntityStates.Modified:
case EntityStates.Added:
stream = new MemoryStream();
break;
default:
Error.ThrowInternalError(InternalError.UnvalidatedEntityState);
break;
}
if (null != stream)
{
XmlWriter writer;
XDocument node = null;
if (this.WritingEntity != null)
{
// if we have to fire the WritingEntity event, buffer the content
// in an XElement so we can present the handler with the data
node = new XDocument();
writer = node.CreateWriter();
}
else
{
writer = XmlUtil.CreateXmlWriterAndWriteProcessingInstruction(stream, HttpProcessUtility.EncodingUtf8NoPreamble);
}
ClientType type = ClientType.Create(box.Resource.GetType());
string typeName = this.ResolveNameFromType(type.ElementType);
#region
writer.WriteStartElement(XmlConstants.AtomEntryElementName, XmlConstants.AtomNamespace);
writer.WriteAttributeString(XmlConstants.DataWebNamespacePrefix, XmlConstants.XmlNamespacesNamespace, this.DataNamespace);
writer.WriteAttributeString(XmlConstants.DataWebMetadataNamespacePrefix, XmlConstants.XmlNamespacesNamespace, XmlConstants.DataWebMetadataNamespace);
//
if (!String.IsNullOrEmpty(typeName))
{
writer.WriteStartElement(XmlConstants.AtomCategoryElementName, XmlConstants.AtomNamespace);
writer.WriteAttributeString(XmlConstants.AtomCategorySchemeAttributeName, this.typeScheme.OriginalString);
writer.WriteAttributeString(XmlConstants.AtomCategoryTermAttributeName, typeName);
writer.WriteEndElement();
}
//
// 2008-05-05T21:44:55Z
//
writer.WriteElementString(XmlConstants.AtomTitleElementName, XmlConstants.AtomNamespace, String.Empty);
writer.WriteElementString(XmlConstants.AtomUpdatedElementName, XmlConstants.AtomNamespace, XmlConvert.ToString(DateTime.UtcNow, XmlDateTimeSerializationMode.RoundtripKind));
writer.WriteStartElement(XmlConstants.AtomAuthorElementName, XmlConstants.AtomNamespace);
writer.WriteElementString(XmlConstants.AtomNameElementName, XmlConstants.AtomNamespace, String.Empty);
writer.WriteEndElement();
if (EntityStates.Modified == box.State)
{
// http://host/service/entityset(key)
writer.WriteElementString(XmlConstants.AtomIdElementName, Util.DereferenceIdentity(box.Identity).AbsoluteUri);
}
else
{
writer.WriteElementString(XmlConstants.AtomIdElementName, XmlConstants.AtomNamespace, String.Empty);
}
#region
if (EntityStates.Added == box.State)
{
this.CreateRequestDataLinks(box, writer);
}
#endregion
#region or
if (type.MediaDataMember == null)
{
writer.WriteStartElement(XmlConstants.AtomContentElementName, XmlConstants.AtomNamespace);
writer.WriteAttributeString(XmlConstants.AtomTypeAttributeName, XmlConstants.MimeApplicationXml); // empty namespace
}
writer.WriteStartElement(XmlConstants.AtomPropertiesElementName, XmlConstants.DataWebMetadataNamespace);
this.WriteContentProperties(writer, type, box.Resource);
writer.WriteEndElement(); //
if (type.MediaDataMember == null)
{
writer.WriteEndElement(); //
}
writer.WriteEndElement(); //
writer.Flush();
writer.Close();
#endregion
#endregion
if (this.WritingEntity != null)
{
ReadingWritingEntityEventArgs args = new ReadingWritingEntityEventArgs(box.Resource, node.Root);
this.WritingEntity(this, args);
// copy the buffered XDocument to the memory stream. no easy way of avoiding
// the copy given that we need to know the length before scanning the stream
node.Save(new StreamWriter(stream)); // defaults to UTF8 w/o preamble & Save will Flush
}
if (newline)
{
// end the xml content stream with a newline
stream.WriteByte((byte)'\r');
stream.WriteByte((byte)'\n');
}
stream.Position = 0;
}
return stream;
}
///
/// add the related links for new entites to non-new entites
///
/// entity in added state
/// writer to add links to
private void CreateRequestDataLinks(ResourceBox box, XmlWriter writer)
{
Debug.Assert(EntityStates.Added == box.State, "entity not added state");
ClientType clientType = null;
foreach (RelatedEnd end in this.RelatedLinks(box))
{
Debug.Assert(!end.ContentGeneratedForSave, "already saved link");
end.ContentGeneratedForSave = true;
if (null == clientType)
{
clientType = ClientType.Create(box.Resource.GetType());
}
string typeAttributeValue;
if (null != clientType.GetProperty(end.SourceProperty, false).CollectionType)
{
typeAttributeValue = XmlConstants.MimeApplicationAtom + ";" + XmlConstants.MimeTypeFeed;
}
else
{
typeAttributeValue = XmlConstants.MimeApplicationAtom + ";" + XmlConstants.MimeTypeEntry;
}
Debug.Assert(null != end.TargetResouce, "null is DELETE");
Uri targetIdentity = Util.DereferenceIdentity(this.objectToResource[end.TargetResouce].Identity);
writer.WriteStartElement(XmlConstants.AtomLinkElementName, XmlConstants.AtomNamespace);
writer.WriteAttributeString(XmlConstants.AtomHRefAttributeName, targetIdentity.ToString());
writer.WriteAttributeString(XmlConstants.AtomLinkRelationAttributeName, XmlConstants.DataWebRelatedNamespace + end.SourceProperty);
writer.WriteAttributeString(XmlConstants.AtomTypeAttributeName, typeAttributeValue);
writer.WriteEndElement();
}
}
/// Handle response to deleted entity.
/// deleted entity
private void HandleResponseDelete(Entry entry)
{
if (EntityStates.Deleted != entry.State)
{
Error.ThrowBatchUnexpectedContent(InternalError.EntityNotDeleted);
}
if (entry.IsResource)
{
ResourceBox resource = (ResourceBox)entry;
this.DetachResource(resource);
}
else
{
this.DetachExistingLink((RelatedEnd)entry);
}
}
/// Handle changeset response.
/// headers of changeset response
/// changeset response stream
/// editLink of the newly created item (non-null if materialize is null)
/// ETag header value from the server response (or null if no etag or if there is an actual response)
private void HandleResponsePost(ResourceBox entry, MaterializeAtom materializer, Uri editLink, string etag)
{
Debug.Assert((materializer != null) || (editLink != null), "must have either materializer or editLink");
if (EntityStates.Added != entry.State)
{
Error.ThrowBatchUnexpectedContent(InternalError.EntityNotAddedState);
}
ResourceBox box = (ResourceBox)entry;
if (materializer == null)
{
Uri identity = Util.ReferenceIdentity(editLink);
this.AttachIdentity(identity, editLink, entry.Resource, etag);
}
else
{
materializer.SetInsertingObject(box.Resource);
foreach (object x in materializer)
{
Debug.Assert(null != box.Identity, "updated inserted should always gain an identity");
Debug.Assert(x == box.Resource, "x == box.Resource, should have same object generated by response");
Debug.Assert(EntityStates.Unchanged == box.State, "should have moved out of insert");
Debug.Assert((null != this.identityToResource) && this.identityToResource.ContainsKey(box.Identity), "should have identity tracked");
}
}
foreach (RelatedEnd end in this.RelatedLinks(box))
{
Debug.Assert(0 != end.SaveResultWasProcessed, "link should have been saved with the enty");
if (IncludeLinkState(end.SaveResultWasProcessed))
{
HandleResponsePost(end);
}
}
}
/// flag results as being processed
/// result entry being processed
/// count of related links that were also processed
private int SaveResultProcessed(Entry entry)
{
Debug.Assert(0 == entry.SaveResultWasProcessed, "this entity/link already had a result");
entry.SaveResultWasProcessed = entry.State;
int count = 0;
if (entry.IsResource && (EntityStates.Added == entry.State))
{
foreach (RelatedEnd end in this.RelatedLinks((ResourceBox)entry))
{
Debug.Assert(end.ContentGeneratedForSave, "link should have been saved with the enty");
if (end.ContentGeneratedForSave)
{
Debug.Assert(0 == end.SaveResultWasProcessed, "this link already had a result");
end.SaveResultWasProcessed = end.State;
count++;
}
}
}
return count;
}
///
/// enumerate the related Modified/Unchanged links for an added item
///
/// entity
/// related links
///
/// During a non-batch SaveChanges, an Added entity can become an Unchanged entity
/// and should be included in the set of related links for the second Added entity.
///
private IEnumerable RelatedLinks(ResourceBox box)
{
int related = box.RelatedLinkCount;
if (0 < related)
{
foreach (RelatedEnd end in this.bindings.Values)
{
if (end.SourceResource == box.Resource)
{
if (null != end.TargetResouce)
{ // null TargetResource is equivalent to Deleted
ResourceBox target = this.objectToResource[end.TargetResouce];
// assumption: the source entity started in the Added state
// note: SaveChanges operates with two passes
// a) first send the request and then attach identity and append the result into a batch response (Example: BeginSaveChanges)
// b) process the batch response (shared code with SaveChanges(Batch)) (Example: EndSaveChanges)
// note: SaveResultWasProcessed is set when to the pre-save state when the save result is sucessfully processed
// scenario #1 when target entity started in modified or unchanged state
// 1) the link target entity was modified and now implicitly assumed to be unchanged (this is true in second pass)
// 2) or link target entity has not been saved is in the modified or unchanged state (this is true in first pass)
// scenario #2 when target entity started in added state
// 1) target entity has an identity (true in first pass for non-batch)
// 2) target entity is processed before source to qualify (1) better during the second pass
// 3) the link target has not been saved and is in the added state
// 4) or the link target has been saved and was in the added state
if (IncludeLinkState(target.SaveResultWasProcessed) || ((0 == target.SaveResultWasProcessed) && IncludeLinkState(target.State)) ||
((null != target.Identity) && (target.ChangeOrder < box.ChangeOrder) &&
((0 == target.SaveResultWasProcessed && EntityStates.Added == target.State) ||
(EntityStates.Added == target.SaveResultWasProcessed))))
{
Debug.Assert(box.ChangeOrder < end.ChangeOrder, "saving is out of order");
yield return end;
}
}
if (0 == --related)
{
break;
}
}
}
}
Debug.Assert(0 == related, "related count mismatch");
}
///
/// detach related bindings
///
/// detached entity
private void DetachRelated(ResourceBox entity)
{
foreach (RelatedEnd end in this.bindings.Values.Where(entity.IsRelatedEntity).ToList())
{
this.DetachExistingLink(end);
}
}
///
/// create the load property request
///
/// entity
/// name of collection or reference property to load
/// The AsyncCallback delegate.
/// user state
/// a aync result that you can get a response from
private LoadPropertyAsyncResult CreateLoadPropertyRequest(object entity, string propertyName, AsyncCallback callback, object state)
{
ResourceBox box = this.EnsureContained(entity, "entity");
Util.CheckArgumentNotEmpty(propertyName, "propertyName");
ClientType type = ClientType.Create(entity.GetType());
Debug.Assert(type.HasKeys, "must have keys to be contained");
if (EntityStates.Added == box.State)
{
throw Error.InvalidOperation(Strings.Context_NoLoadWithInsertEnd);
}
ClientType.ClientProperty property = type.GetProperty(propertyName, false);
Debug.Assert(null != property, "should have thrown if propertyName didn't exist");
Uri relativeUri;
bool allowAnyType = false;
if (type.MediaDataMember != null && propertyName == type.MediaDataMember.PropertyName)
{
// special case for requesting the "media" value of an ATOM media link entry
relativeUri = Util.CreateUri(XmlConstants.UriValueSegment, UriKind.Relative);
allowAnyType = true; // $value can be of any MIME type
}
else
{
relativeUri = Util.CreateUri(propertyName + (null != property.CollectionType ? "()" : String.Empty), UriKind.Relative);
}
Uri requestUri = Util.CreateUri(box.GetResourceUri(this.baseUriWithSlash), relativeUri);
HttpWebRequest request = this.CreateRequest(requestUri, XmlConstants.HttpMethodGet, allowAnyType, null);
DataServiceRequest dataServiceRequest = DataServiceRequest.GetInstance(property.PropertyType, requestUri);
return new LoadPropertyAsyncResult(entity, propertyName, this, request, callback, state, dataServiceRequest);
}
///
/// write the content section of the atom payload
///
/// writer
/// resource type
/// resource value
private void WriteContentProperties(XmlWriter writer, ClientType type, object resource)
{
#region value
foreach (ClientType.ClientProperty property in type.Properties)
{
// don't write mime data member or the mime type member for it
if (property == type.MediaDataMember ||
(type.MediaDataMember != null &&
type.MediaDataMember.MimeTypeProperty == property))
{
continue;
}
object propertyValue = property.GetValue(resource);
if (property.IsKnownType)
{
WriteContentProperty(writer, this.DataNamespace, property, propertyValue);
}
#if ASTORIA_OPEN_OBJECT
else if (property.OpenObjectProperty)
{
foreach (KeyValuePair pair in (IDictionary)propertyValue)
{
if ((null == pair.Value) || ClientConvert.IsKnownType(pair.Value.GetType()))
{
WriteContentProperty(writer, pair.Key, pair.Value, false);
}
}
}
#endif
else if (null == property.CollectionType)
{
ClientType nested = ClientType.Create(property.PropertyType);
if (!nested.HasKeys)
{
#region complex type
writer.WriteStartElement(property.PropertyName, this.DataNamespace);
string typeName = this.ResolveNameFromType(nested.ElementType);
if (!String.IsNullOrEmpty(typeName))
{
writer.WriteAttributeString(XmlConstants.AtomTypeAttributeName, XmlConstants.DataWebMetadataNamespace, typeName);
}
this.WriteContentProperties(writer, nested, propertyValue);
writer.WriteEndElement();
#endregion
}
}
}
#endregion
}
///
/// detach existing link
///
/// link to detach
private void DetachExistingLink(RelatedEnd existing)
{
if (this.bindings.Remove(existing))
{ // this link may have been previously detached by a detaching entity
existing.State = EntityStates.Detached;
this.objectToResource[existing.SourceResource].RelatedLinkCount--;
}
}
///
/// find and detach link for reference property
///
/// source entity
/// source entity property name for target entity
/// target entity
/// link merge option
/// true if found and not removed
private RelatedEnd DetachReferenceLink(object source, string sourceProperty, object target, MergeOption linkMerge)
{
RelatedEnd existing = this.GetLinks(source, sourceProperty).FirstOrDefault();
if (null != existing)
{
if ((target == existing.TargetResouce) ||
(MergeOption.AppendOnly == linkMerge) ||
(MergeOption.PreserveChanges == linkMerge && EntityStates.Modified == existing.State))
{
return existing;
}
this.DetachExistingLink(existing);
Debug.Assert(!this.bindings.Values.Any(o => (o.SourceResource == source) && (o.SourceProperty == sourceProperty)), "only expecting one");
}
return null;
}
///
/// verify the resource being tracked by context
///
/// resource
/// parameter name to include in ArgumentException
/// The given resource.
/// if resource is not contained
private ResourceBox EnsureContained(object resource, string parameterName)
{
Util.CheckArgumentNull(resource, parameterName);
ResourceBox box = null;
if (!this.objectToResource.TryGetValue(resource, out box))
{
throw Error.InvalidOperation(Strings.Context_EntityNotContained);
}
return box;
}
///
/// verify the source and target are relatable
///
/// source Resource
/// source Property
/// target Resource
/// destination state of relationship to evaluate for
/// true if DeletedState and one of the ends is in the added state
/// if source or target are null
/// if source or target are not contained
/// if source property is null
/// if source property empty
/// Can only relate ends with keys.
/// If target doesn't match property type.
/// If adding relationship where one of the ends is in the deleted state.
/// If attaching relationship where one of the ends is in the added or deleted state.
private bool EnsureRelatable(object source, string sourceProperty, object target, EntityStates state)
{
ResourceBox sourceResource = this.EnsureContained(source, "source");
ResourceBox targetResource = null;
if ((null != target) || ((EntityStates.Modified != state) && (EntityStates.Unchanged != state)))
{
targetResource = this.EnsureContained(target, "target");
}
Util.CheckArgumentNotEmpty(sourceProperty, "sourceProperty");
ClientType type = ClientType.Create(source.GetType());
Debug.Assert(type.HasKeys, "should be enforced by just adding an object");
// will throw InvalidOperationException if property doesn't exist
ClientType.ClientProperty property = type.GetProperty(sourceProperty, false);
if (property.IsKnownType)
{
throw Error.InvalidOperation(Strings.Context_RelationNotRefOrCollection);
}
if ((EntityStates.Unchanged == state) && (null == target) && (null != property.CollectionType))
{
targetResource = this.EnsureContained(target, "target");
}
if (((EntityStates.Added == state) || (EntityStates.Deleted == state)) && (null == property.CollectionType))
{
throw Error.InvalidOperation(Strings.Context_AddLinkCollectionOnly);
}
else if ((EntityStates.Modified == state) && (null != property.CollectionType))
{
throw Error.InvalidOperation(Strings.Context_SetLinkReferenceOnly);
}
// if (property.IsCollection) then property.PropertyType is the collection elementType
// either way you can only have a relation ship between keyed objects
type = ClientType.Create(property.CollectionType ?? property.PropertyType);
Debug.Assert(type.HasKeys, "should be enforced by just adding an object");
if ((null != target) && !type.ElementType.IsInstanceOfType(target))
{
// target is not of the correct type
throw Error.Argument(Strings.Context_RelationNotRefOrCollection, "target");
}
if ((EntityStates.Added == state) || (EntityStates.Unchanged == state))
{
if ((sourceResource.State == EntityStates.Deleted) ||
((targetResource != null) && (targetResource.State == EntityStates.Deleted)))
{
// can't add/attach new relationship when source or target in deleted state
throw Error.InvalidOperation(Strings.Context_NoRelationWithDeleteEnd);
}
}
if ((EntityStates.Deleted == state) || (EntityStates.Unchanged == state))
{
if ((sourceResource.State == EntityStates.Added) ||
((targetResource != null) && (targetResource.State == EntityStates.Added)))
{
// can't have non-added relationship when source or target is in added state
if (EntityStates.Deleted == state)
{
return true;
}
throw Error.InvalidOperation(Strings.Context_NoRelationWithInsertEnd);
}
}
return false;
}
/// validate and trim leading and trailing forward slashes
/// resource name to validate
/// if entitySetName was null
/// if entitySetName was empty or contained only forward slash
private void ValidateEntitySetName(ref string entitySetName)
{
Util.CheckArgumentNotEmpty(entitySetName, "entitySetName");
entitySetName = entitySetName.Trim(Util.ForwardSlash);
Util.CheckArgumentNotEmpty(entitySetName, "entitySetName");
Uri tmp = Util.CreateUri(entitySetName, UriKind.RelativeOrAbsolute);
if (tmp.IsAbsoluteUri ||
!String.IsNullOrEmpty(Util.CreateUri(this.baseUriWithSlash, tmp)
.GetComponents(UriComponents.Query | UriComponents.Fragment, UriFormat.SafeUnescaped)))
{
throw Error.Argument(Strings.Context_EntitySetName, "entitySetName");
}
}
/// create this.identityToResource when necessary
private void EnsureIdentityToResource()
{
if (null == this.identityToResource)
{
System.Threading.Interlocked.CompareExchange(ref this.identityToResource, new Dictionary(), null);
}
}
///
/// increment the resource change for sorting during submit changes
///
/// the resource to update the change order
private void IncrementChange(RelatedEnd box)
{
box.ChangeOrder = ++this.nextChange;
}
///
/// increment the resource change for sorting during submit changes
///
/// the resource to update the change order
private void IncrementChange(ResourceBox box)
{
box.ChangeOrder = ++this.nextChange;
}
///
/// Entity and LinkDescriptor base class that contains change order and state
///
internal abstract class Entry
{
/// change order
private uint changeOrder = UInt32.MaxValue;
/// was content generated for the entity
private bool saveContentGenerated;
/// was this entity save result processed
/// 0 - no processed, otherwise reflects the previous state
private EntityStates saveResultProcessed;
/// state
private EntityStates state;
/// last save exception per entry
private Exception saveError;
/// changeOrder
internal uint ChangeOrder
{
get { return this.changeOrder; }
set { this.changeOrder = value; }
}
/// true if resource, false if link
internal abstract bool IsResource
{
get;
}
/// was content generated for the entity
internal bool ContentGeneratedForSave
{
get { return this.saveContentGenerated; }
set { this.saveContentGenerated = value; }
}
/// was this entity save result processed
internal EntityStates SaveResultWasProcessed
{
get { return this.saveResultProcessed; }
set { this.saveResultProcessed = value; }
}
/// last save exception per entry
internal Exception SaveError
{
get { return this.saveError; }
set { this.saveError = value; }
}
/// state
internal EntityStates State
{
get { return this.state; }
set { this.state = value; }
}
}
///
/// An untyped container for a resource and its identity
///
[DebuggerDisplay("State = {state}, Uri = {editLink}, Element = {resource.GetType().ToString()}")]
internal class ResourceBox : Entry
{
/// uri to identitfy the entity
/// <atom:id>identity</id>
private Uri identity;
/// uri to edit the entity
/// <atom:link rel="edit" href="editLink" />
private Uri editLink;
// /// uri to query the entity
// /// <atom:link rel="self" href="queryLink" />
// private Uri queryLink;
/// entity ETag (concurrency token)
private string etag;
/// entity
private object resource;
/// count of links for which this entity is the source
private int relatedLinkCount;
/// constructor
/// resource Uri
/// resource EntitySet
/// non-null resource
internal ResourceBox(Uri identity, Uri editLink, object resource)
{
Debug.Assert(null == identity || identity.IsAbsoluteUri, "bad identity");
Debug.Assert(null != editLink, "null editLink");
this.identity = identity;
this.editLink = editLink;
this.resource = resource;
}
/// this is a entity
internal override bool IsResource
{
get { return true; }
}
/// uri to edit entity
internal Uri EditLink
{
get { return this.editLink; }
set { this.editLink = value; }
}
/// etag
internal string ETag
{
get { return this.etag; }
set { this.etag = value; }
}
/// entity uri identity
internal Uri Identity
{
get { return this.identity; }
set { this.identity = Util.CheckArgumentNull(value, "Identity"); }
}
/// count of links for which this entity is the source
internal int RelatedLinkCount
{
get { return this.relatedLinkCount; }
set { this.relatedLinkCount = value; }
}
/// entity
internal object Resource
{
get { return this.resource; }
}
/// uri to edit the entity
/// baseUriWithSlash
/// absolute uri which can be used to edit the entity
internal Uri GetResourceUri(Uri baseUriWithSlash)
{
Uri result = Util.CreateUri(baseUriWithSlash, this.EditLink);
return result;
}
/// is the entity the same as the source or target entity
/// related end
/// true if same as source or target entity
internal bool IsRelatedEntity(RelatedEnd related)
{
return ((this.resource == related.SourceResource) || (this.resource == related.TargetResouce));
}
}
///
/// An untyped container for a resource and its related end
///
[DebuggerDisplay("State = {state}")]
internal sealed class RelatedEnd : Entry
{
/// IEqualityComparer to compare equivalence between to related ends
internal static readonly IEqualityComparer EquivalenceComparer = new EqualityComparer();
/// source entity
private readonly object source;
/// name of property on source entity that references the target entity
private readonly string sourceProperty;
/// target entity
private readonly object target;
// /// Property on the target resource that relates back to the source resource
// internal readonly string ChildProperty;
/// constructor
/// parentResource
/// sourceProperty
/// childResource
internal RelatedEnd(object source, string property, object target)
{
Debug.Assert(null != source, "null source");
Debug.Assert(!String.IsNullOrEmpty(property), "null target");
this.source = source;
this.sourceProperty = property;
this.target = target;
}
/// this is a link
internal override bool IsResource
{
get { return false; }
}
/// target resource
internal object TargetResouce
{
get { return this.target; }
}
/// source resource property name
internal string SourceProperty
{
get { return this.sourceProperty; }
}
/// source resource
internal object SourceResource
{
get { return this.source; }
}
///
/// Are the two related ends equivalent?
///
/// x
/// y
/// true if the related ends are equivalent
public static bool Equals(RelatedEnd x, RelatedEnd y)
{
return ((x.SourceResource == y.SourceResource) &&
(x.TargetResouce == y.TargetResouce) &&
(x.SourceProperty == y.SourceProperty));
}
/// test for equivalence
private sealed class EqualityComparer : IEqualityComparer
{
/// test for equivalence
/// x
/// y
/// true if equivalent
bool IEqualityComparer.Equals(RelatedEnd x, RelatedEnd y)
{
return RelatedEnd.Equals(x, y);
}
/// hash code based on the contained resources
/// x
/// hash code
int IEqualityComparer.GetHashCode(RelatedEnd x)
{
return (x.SourceResource.GetHashCode() ^
((null != x.TargetResouce) ? x.TargetResouce.GetHashCode() : 0) ^
x.SourceProperty.GetHashCode());
}
}
}
/// wrapper around loading a property from a response
private class LoadPropertyAsyncResult : QueryAsyncResult
{
/// entity whose property is being loaded
private readonly object entity;
/// name of the property on the entity that is being loaded
private readonly string propertyName;
/// constructor
/// entity
/// name of collection or reference property to load
/// Originating context
/// Originating WebRequest
/// user callback
/// user state
/// request object.
internal LoadPropertyAsyncResult(object entity, string propertyName, DataServiceContext context, HttpWebRequest request, AsyncCallback callback, object state, DataServiceRequest dataServiceRequest)
: base(context, "LoadProperty", dataServiceRequest, request, callback, state)
{
this.entity = entity;
this.propertyName = propertyName;
}
///
/// loading a property from a response
///
/// QueryOperationResponse instance containing information about the response.
internal QueryOperationResponse LoadProperty()
{
IEnumerable results = null;
DataServiceContext context = (DataServiceContext)this.Source;
ClientType type = ClientType.Create(this.entity.GetType());
Debug.Assert(type.HasKeys, "must have keys to be contained");
ResourceBox box = context.EnsureContained(this.entity, "entity");
if (EntityStates.Added == box.State)
{
throw Error.InvalidOperation(Strings.Context_NoLoadWithInsertEnd);
}
ClientType.ClientProperty property = type.GetProperty(this.propertyName, false);
Type elementType = property.CollectionType ?? property.NullablePropertyType;
try
{
if (type.MediaDataMember == property)
{
results = this.ReadPropertyFromRawData(property);
}
else
{
results = this.ReadPropertyFromAtom(box, property);
}
return this.GetResponse(results, elementType);
}
catch (InvalidOperationException ex)
{
QueryOperationResponse response = this.GetResponse(results, elementType);
if (response != null)
{
response.Error = ex;
throw new DataServiceQueryException(Strings.DataServiceException_GeneralError, ex, response);
}
throw;
}
}
///
/// Load property data from an ATOM response
///
/// Box pointing to the entity to load this to
/// The property being loaded
/// property values as IEnumerable.
private IEnumerable ReadPropertyFromAtom(ResourceBox box, ClientType.ClientProperty property)
{
DataServiceContext context = (DataServiceContext)this.Source;
bool deletedState = (EntityStates.Deleted == box.State);
Type nestedType;
#if ASTORIA_OPEN_OBJECT
if (property.OpenObjectProperty)
{
nestedType = typeof(OpenObject);
}
else
#endif
{
nestedType = property.CollectionType ?? property.NullablePropertyType;
}
ClientType clientType = ClientType.Create(nestedType);
// when setting a reference, use the entity
// when adding an item to a collection, use the collection object referenced by the entity
bool setNestedValue = false;
object collection = this.entity;
if (null != property.CollectionType)
{ // get the collection that we actually add nested
collection = property.GetValue(this.entity);
if (null == collection)
{
setNestedValue = true;
collection = Activator.CreateInstance(typeof(List<>).MakeGenericType(nestedType));
}
}
Func create = delegate(DataServiceContext ctx, XmlReader reader, Type elmentType)
{
return new MaterializeAtom(ctx, reader, elmentType, ctx.MergeOption);
};
// store the results so that they can be there in the response body.
Type elementType = property.CollectionType ?? property.NullablePropertyType;
IList results = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType));
// elementType.ElementType has Nullable stripped away, use nestedType for materializer
using (MaterializeAtom materializer = context.GetMaterializer(this, nestedType, create))
{
if (null != materializer)
{
int count = 0;
#if ASTORIA_OPEN_OBJECT
object openProperties = null;
#endif
foreach (object child in materializer)
{
results.Add(child);
count++;
#if ASTORIA_OPEN_OBJECT
property.SetValue(collection, child, this.propertyName, ref openProperties, true);
#else
property.SetValue(collection, child, this.propertyName, true);
#endif
// via LoadProperty, you can have a property with and null value
if ((null != child) && (MergeOption.NoTracking != materializer.MergeOptionValue) && clientType.HasKeys)
{
if (deletedState)
{
context.DeleteLink(this.entity, this.propertyName, child);
}
else
{ // put link into unchanged state
context.AttachLink(this.entity, this.propertyName, child, materializer.MergeOptionValue);
}
}
}
}
// we don't do this because we are loading, not refreshing
// if ((0 == count) && (MergeOption.OverwriteChanges == this.mergeOption))
// { property.Clear(entity); }
}
if (setNestedValue)
{
#if ASTORIA_OPEN_OBJECT
object openProperties = null;
property.SetValue(this.entity, collection, this.propertyName, ref openProperties, false);
#else
property.SetValue(this.entity, collection, this.propertyName, false);
#endif
}
return results;
}
///
/// Load property data form a raw response
///
/// The property being loaded
/// property values as IEnumerable.
private IEnumerable ReadPropertyFromRawData(ClientType.ClientProperty property)
{
// if this is the data property for a media entry, what comes back
// is the raw value (no markup)
#if ASTORIA_OPEN_OBJECT
object openProps = null;
#endif
string mimeType = null;
Encoding encoding = null;
Type elementType = property.CollectionType ?? property.NullablePropertyType;
IList results = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType));
HttpProcessUtility.ReadContentType(this.ContentType, out mimeType, out encoding);
using (Stream responseStream = this.GetResponseStream())
{
// special case byte[], and for everything else let std conversion kick-in
if (property.PropertyType == typeof(byte[]))
{
int total = checked((int)this.ContentLength);
byte[] buffer = new byte[total];
int read = 0;
while (read < total)
{
int r = responseStream.Read(buffer, read, total - read);
if (r <= 0)
{
throw Error.InvalidOperation(Strings.Context_UnexpectedZeroRawRead);
}
read += r;
}
results.Add(buffer);
#if ASTORIA_OPEN_OBJECT
property.SetValue(this.entity, buffer, this.propertyName, ref openProps, false);
#else
property.SetValue(this.entity, buffer, this.propertyName, false);
#endif
}
else
{
StreamReader reader = new StreamReader(responseStream, encoding);
object convertedValue = property.PropertyType == typeof(string) ?
reader.ReadToEnd() :
ClientConvert.ChangeType(reader.ReadToEnd(), property.PropertyType);
results.Add(convertedValue);
#if ASTORIA_OPEN_OBJECT
property.SetValue(this.entity, convertedValue, this.propertyName, ref openProps, false);
#else
property.SetValue(this.entity, convertedValue, this.propertyName, false);
#endif
}
}
#if ASTORIA_OPEN_OBJECT
Debug.Assert(openProps == null, "These should not be set in this path");
#endif
if (property.MimeTypeProperty != null)
{
// an implication of this 3rd-arg-null is that mime type properties cannot be open props
#if ASTORIA_OPEN_OBJECT
property.MimeTypeProperty.SetValue(this.entity, mimeType, null, ref openProps, false);
Debug.Assert(openProps == null, "These should not be set in this path");
#else
property.MimeTypeProperty.SetValue(this.entity, mimeType, null, false);
#endif
}
return results;
}
}
///
/// implementation of IAsyncResult for SaveChanges
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Pending")]
private class SaveAsyncResult : BaseAsyncResult
{
/// where to pull the changes from
private readonly DataServiceContext Context;
/// sorted list of entries by change order
private readonly List ChangedEntries;
/// array of queries being executed
private readonly DataServiceRequest[] Queries;
/// operations
private readonly List Responses;
/// boundary used when generating batch boundary
private readonly string batchBoundary;
/// option in use for SaveChanges
private readonly SaveChangesOptions options;
/// if true then async, else [....]
private readonly bool executeAsync;
/// debugging trick to track number of completed requests
private int changesCompleted;
/// wrapped request
private PerRequest request;
/// batch web response
private HttpWebResponse batchResponse;
/// response stream for the batch
private Stream httpWebResponseStream;
/// service response
private DataServiceResponse service;
/// The ResourceBox or RelatedEnd currently in flight
private int entryIndex = -1;
///
/// True if the current in-flight request is a media link entry POST
/// that needs to be followed by a PUT for the rest of the properties
///
private bool procesingMediaLinkEntry;
/// response stream
private BatchStream responseBatchStream;
/// temporary buffer when cache results from CUD op in non-batching save changes
private byte[] buildBatchBuffer;
/// temporary writer when cache results from CUD op in non-batching save changes
private StreamWriter buildBatchWriter;
/// count of data actually copied
private long copiedContentLength;
/// what is the changset boundary
private string changesetBoundary;
/// is a change set being cached
private bool changesetStarted;
#region constructors
///
/// constructor for async operations
///
/// context
/// method
/// queries
/// options
/// user callback
/// user state object
/// async or [....]
internal SaveAsyncResult(DataServiceContext context, string method, DataServiceRequest[] queries, SaveChangesOptions options, AsyncCallback callback, object state, bool async)
: base(context, method, callback, state)
{
this.executeAsync = async;
this.Context = context;
this.Queries = queries;
this.options = options;
this.Responses = new List();
if (null == queries)
{
#region changed entries
this.ChangedEntries = context.objectToResource.Values.Cast()
.Union(context.bindings.Values.Cast())
.Where(HasModifiedResourceState)
.OrderBy(o => o.ChangeOrder)
.ToList();
foreach (Entry e in this.ChangedEntries)
{
e.ContentGeneratedForSave = false;
e.SaveResultWasProcessed = 0;
e.SaveError = null;
if (!e.IsResource)
{
object target = ((RelatedEnd)e).TargetResouce;
if (null != target)
{
Entry f = context.objectToResource[target];
if (EntityStates.Unchanged == f.State)
{
f.ContentGeneratedForSave = false;
f.SaveResultWasProcessed = 0;
f.SaveError = null;
}
}
}
}
#endregion
}
else
{
this.ChangedEntries = new List();
}
if (IsFlagSet(options, SaveChangesOptions.Batch))
{
this.batchBoundary = XmlConstants.HttpMultipartBoundaryBatch + "_" + Guid.NewGuid().ToString();
}
else
{
this.batchBoundary = XmlConstants.HttpMultipartBoundaryBatchResponse + "_" + Guid.NewGuid().ToString();
this.DataServiceResponse = new DataServiceResponse(null, -1, this.Responses, false /*batchResponse*/);
}
}
#endregion constructor
#region end
/// generate the batch request of all changes to save
internal DataServiceResponse DataServiceResponse
{
get
{
Debug.Assert(null != this.service, "null service");
return this.service;
}
set
{
this.service = value;
}
}
/// process the batch
/// data service response
internal DataServiceResponse EndRequest()
{
if ((null != this.responseBatchStream) || (null != this.httpWebResponseStream))
{
this.HandleBatchResponse();
}
return this.DataServiceResponse;
}
#endregion
#region start a batch
/// initial the async batch save changeset
/// whether we need to update MERGE or PUT method for update.
internal void BatchBeginRequest(bool replaceOnUpdate)
{
PerRequest pereq = null;
try
{
MemoryStream memory = this.GenerateBatchRequest(replaceOnUpdate);
if (null != memory)
{
HttpWebRequest httpWebRequest = this.CreateBatchRequest(memory);
this.request = pereq = new PerRequest();
pereq.Request = httpWebRequest;
pereq.RequestStreamContent = memory;
this.httpWebResponseStream = new MemoryStream();
int step = ++pereq.RequestStep;
IAsyncResult asyncResult = httpWebRequest.BeginGetRequestStream(this.AsyncEndGetRequestStream, pereq);
bool reallyCompletedSynchronously = asyncResult.CompletedSynchronously && (step < pereq.RequestStep);
pereq.RequestCompletedSynchronously &= reallyCompletedSynchronously;
}
else
{
Debug.Assert(this.CompletedSynchronously, "completedSynchronously");
Debug.Assert(this.IsCompleted, "completed");
}
}
catch (Exception e)
{
this.HandleFailure(pereq, e);
throw; // to user on BeginSaveChangeSet, will still invoke Callback
}
finally
{
this.HandleCompleted(pereq); // will invoke user callback
}
Debug.Assert((this.CompletedSynchronously && this.IsCompleted) || !this.CompletedSynchronously, "[....] without complete");
}
#if !ASTORIA_LIGHT // Synchronous methods not available
///
/// Synchronous batch request
///
/// whether we need to update MERGE or PUT method for update.
internal void BatchRequest(bool replaceOnUpdate)
{
MemoryStream memory = this.GenerateBatchRequest(replaceOnUpdate);
if ((null != memory) && (0 < memory.Length))
{
HttpWebRequest httpWebRequest = this.CreateBatchRequest(memory);
using (System.IO.Stream requestStream = httpWebRequest.GetRequestStream())
{
byte[] buffer = memory.GetBuffer();
int bufferOffset = checked((int)memory.Position);
int bufferLength = checked((int)memory.Length) - bufferOffset;
// the following is useful in the debugging Immediate Window
// string x = System.Text.Encoding.UTF8.GetString(buffer, bufferOffset, bufferLength);
requestStream.Write(buffer, bufferOffset, bufferLength);
}
HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse();
this.batchResponse = httpWebResponse;
if (null != httpWebResponse)
{
this.httpWebResponseStream = httpWebResponse.GetResponseStream();
}
}
}
#endif
#endregion
#region start a non-batch requests
///
/// This starts the next change
///
/// whether we need to update MERGE or PUT method for update.
internal void BeginNextChange(bool replaceOnUpdate)
{
Debug.Assert(!this.IsCompleted, "why being called if already completed?");
// SaveCallback can't chain synchronously completed responses, caller will loop the to next change
PerRequest pereq = null;
do
{
HttpWebRequest httpWebRequest = null;
HttpWebResponse response = null;
try
{
if (null != this.request)
{
this.IsCompleted = true;
Error.ThrowInternalError(InternalError.InvalidBeginNextChange);
}
httpWebRequest = this.CreateNextRequest(replaceOnUpdate);
if ((null != httpWebRequest) || (this.entryIndex < this.ChangedEntries.Count))
{
if (this.ChangedEntries[this.entryIndex].ContentGeneratedForSave)
{
Debug.Assert(this.ChangedEntries[this.entryIndex] is RelatedEnd, "only expected RelatedEnd to presave");
Debug.Assert(
this.ChangedEntries[this.entryIndex].State == EntityStates.Added ||
this.ChangedEntries[this.entryIndex].State == EntityStates.Modified,
"only expected added to presave");
continue;
}
MemoryStream memoryStream = null;
if (this.executeAsync)
{
#region async
this.request = pereq = new PerRequest();
pereq.Request = httpWebRequest;
IAsyncResult asyncResult;
int step = ++pereq.RequestStep;
if (this.procesingMediaLinkEntry || (null == (memoryStream = this.CreateChangeData(this.entryIndex, false))))
{
asyncResult = httpWebRequest.BeginGetResponse(this.AsyncEndGetResponse, pereq);
}
else
{
httpWebRequest.ContentLength = memoryStream.Length - memoryStream.Position;
pereq.RequestStreamContent = memoryStream;
asyncResult = httpWebRequest.BeginGetRequestStream(this.AsyncEndGetRequestStream, pereq);
}
bool reallyCompletedSynchronously = asyncResult.CompletedSynchronously && (step < pereq.RequestStep);
pereq.RequestCompletedSynchronously &= reallyCompletedSynchronously;
this.CompletedSynchronously &= reallyCompletedSynchronously;
#endregion
}
#if !ASTORIA_LIGHT // Synchronous methods not available
else
{
#region [....]
memoryStream = this.CreateChangeData(this.entryIndex, false);
if (null != memoryStream)
{
byte[] buffer = memoryStream.GetBuffer();
int bufferOffset = checked((int)memoryStream.Position);
int bufferLength = checked((int)memoryStream.Length) - bufferOffset;
httpWebRequest.ContentLength = bufferLength;
using (Stream stream = httpWebRequest.GetRequestStream())
{
// the following is useful in the debugging Immediate Window
// string x = System.Text.Encoding.UTF8.GetString(memoryStream.GetBuffer(), bufferOffset, (int)memoryStream.Length);
stream.Write(buffer, bufferOffset, bufferLength);
}
}
response = (HttpWebResponse)httpWebRequest.GetResponse();
if (!this.procesingMediaLinkEntry)
{
this.changesCompleted++;
}
this.HandleOperationResponse(httpWebRequest, response);
this.HandleOperationResponseData(response);
this.HandleOperationEnd();
this.request = null;
#endregion
}
#endif
}
else
{
this.IsCompleted = true;
if (this.CompletedSynchronously)
{
this.HandleCompleted(pereq);
}
}
}
catch (InvalidOperationException e)
{
WebUtil.GetHttpWebResponse(e, ref response);
this.HandleOperationException(e, httpWebRequest, response);
this.HandleCompleted(pereq);
}
finally
{
if (null != response)
{
response.Close();
}
}
// either everything completed synchronously until a change is saved and its state changed
// and we don't return to this loop until then or something was asynchronous
// and we won't continue in this loop, instead letting the inner most loop start the next request
}
while (((null == pereq) || (pereq.RequestCompleted && pereq.RequestCompletedSynchronously)) && !this.IsCompleted);
Debug.Assert(this.executeAsync || this.CompletedSynchronously, "[....] !CompletedSynchronously");
Debug.Assert((this.CompletedSynchronously && this.IsCompleted) || !this.CompletedSynchronously, "[....] without complete");
Debug.Assert(this.entryIndex < this.ChangedEntries.Count || this.ChangedEntries.All(o => o.ContentGeneratedForSave), "didn't generated content for all entities/links");
}
/// cleanup work to do once the batch / savechanges is complete
protected override void CompletedRequest()
{
this.buildBatchBuffer = null;
if (null != this.buildBatchWriter)
{
Debug.Assert(!IsFlagSet(this.options, SaveChangesOptions.Batch), "should be non-batch");
this.HandleOperationEnd();
this.buildBatchWriter.WriteLine("--{0}--", this.batchBoundary);
this.buildBatchWriter.Flush();
Debug.Assert(Object.ReferenceEquals(this.httpWebResponseStream, this.buildBatchWriter.BaseStream), "expected different stream");
this.httpWebResponseStream.Position = 0;
this.buildBatchWriter = null;
// the following is useful in the debugging Immediate Window
// string x = System.Text.Encoding.UTF8.GetString(memoryStream.GetBuffer(), 0, (int)memoryStream.Length);
this.responseBatchStream = new BatchStream(this.httpWebResponseStream, this.batchBoundary, HttpProcessUtility.EncodingUtf8NoPreamble, false);
}
}
/// build the *Descriptor object in the ChangeList
/// entry to build from
/// EntityDescriptor or LinkDescriptor
private static Descriptor BuildReturn(Entry entry)
{
if (entry.IsResource)
{
ResourceBox box = (ResourceBox)entry;
EntityDescriptor obj = new EntityDescriptor(box.Resource, box.ETag, box.State);
return obj;
}
else
{
RelatedEnd end = (RelatedEnd)entry;
LinkDescriptor obj = new LinkDescriptor(end.SourceResource, end.SourceProperty, end.TargetResouce, end.State);
return obj;
}
}
/// verify non-null and not completed
/// the request in progress
/// error code if null or completed
/// the next step to validate CompletedSyncronously
private static int CompleteCheck(PerRequest value, InternalError errorcode)
{
if ((null == value) || value.RequestCompleted)
{
Error.ThrowInternalError(errorcode);
}
return ++value.RequestStep;
}
/// verify they have the same reference
/// the actual thing
/// the expected thing
/// error code if they are not
private static void EqualRefCheck(PerRequest actual, PerRequest expected, InternalError errorcode)
{
if (!Object.ReferenceEquals(actual, expected))
{
Error.ThrowInternalError(errorcode);
}
}
/// Set the AsyncWait and invoke the user callback.
/// the request object
private void HandleCompleted(PerRequest pereq)
{
if (null != pereq)
{
this.CompletedSynchronously &= pereq.RequestCompletedSynchronously;
if (pereq.RequestCompleted)
{
System.Threading.Interlocked.CompareExchange(ref this.request, null, pereq);
if (IsFlagSet(this.options, SaveChangesOptions.Batch))
{ // all competing thread must complete this before user calback is invoked
System.Threading.Interlocked.CompareExchange(ref this.batchResponse, pereq.HttpWebResponse, null);
pereq.HttpWebResponse = null;
}
pereq.Dispose();
}
}
this.HandleCompleted();
}
/// Cache the exception that happened on the background thread for the caller of EndSaveChanges.
/// the request object
/// exception object from background thread
/// true if the exception should be rethrown
private bool HandleFailure(PerRequest pereq, Exception e)
{
if (null != pereq)
{
pereq.RequestCompleted = true;
}
return this.HandleFailure(e);
}
///
/// Create HttpWebRequest from the next availabe resource
///
/// whether we need to update MERGE or PUT method for update.
/// web request
private HttpWebRequest CreateNextRequest(bool replaceOnUpdate)
{
if (!this.procesingMediaLinkEntry)
{
this.entryIndex++;
}
else
{
// if we were creating a media entry before, then the "next change"
// is to do the second step of the creation, a PUT to update
// metadata
this.procesingMediaLinkEntry = false;
}
if (unchecked((uint)this.entryIndex < (uint)this.ChangedEntries.Count))
{
Entry entry = this.ChangedEntries[this.entryIndex];
if (entry.IsResource)
{
ResourceBox box = (ResourceBox)entry;
HttpWebRequest req;
if ((EntityStates.Added == entry.State) && (null != (req = this.Context.CheckAndProcessMediaEntry(box))))
{
this.procesingMediaLinkEntry = true;
}
else
{
Debug.Assert(!this.procesingMediaLinkEntry || entry.State == EntityStates.Modified, "!this.procesingMediaLinkEntry || entry.State == EntityStates.Modified");
req = this.Context.CreateRequest(box, entry.State, replaceOnUpdate);
}
return req;
}
return this.Context.CreateRequest((RelatedEnd)entry);
}
return null;
}
///
/// create memory stream for entry (entity or link)
///
/// index into changed entries
/// include newline in output
/// memory stream of data for entry
private MemoryStream CreateChangeData(int index, bool newline)
{
Entry entry = this.ChangedEntries[index];
Debug.Assert(!entry.ContentGeneratedForSave, "already saved entity/link");
entry.ContentGeneratedForSave = true;
if (entry.IsResource)
{
ResourceBox box = (ResourceBox)entry;
if (!this.procesingMediaLinkEntry)
{
Debug.Assert(!this.procesingMediaLinkEntry || entry.State == EntityStates.Modified, "!this.procesingMediaLinkEntry || entry.State == EntityStates.Modified");
return this.Context.CreateRequestData(box, newline);
}
}
else
{
RelatedEnd link = (RelatedEnd)entry;
if ((EntityStates.Added == link.State) ||
((EntityStates.Modified == link.State) && (null != link.TargetResouce)))
{
return this.Context.CreateRequestData(link, newline);
}
}
return null;
}
#endregion
#region generate batch response from non-batch
/// basic separator between response
private void HandleOperationStart()
{
this.HandleOperationEnd();
if (null == this.httpWebResponseStream)
{
this.httpWebResponseStream = new MemoryStream();
}
if (null == this.buildBatchWriter)
{
this.buildBatchWriter = new StreamWriter(this.httpWebResponseStream); // defaults to UTF8 w/o preamble
}
if (null == this.changesetBoundary)
{
this.changesetBoundary = XmlConstants.HttpMultipartBoundaryChangesetResponse + "_" + Guid.NewGuid().ToString();
}
this.changesetStarted = true;
this.buildBatchWriter.WriteLine("--{0}", this.batchBoundary);
this.buildBatchWriter.WriteLine("{0}: {1}; boundary={2}", XmlConstants.HttpContentType, XmlConstants.MimeMultiPartMixed, this.changesetBoundary);
this.buildBatchWriter.WriteLine();
this.buildBatchWriter.WriteLine("--{0}", this.changesetBoundary);
}
/// write the trailing --changesetboundary--
private void HandleOperationEnd()
{
if (this.changesetStarted)
{
Debug.Assert(null != this.buildBatchWriter, "buildBatchWriter");
Debug.Assert(null != this.changesetBoundary, "changesetBoundary");
this.buildBatchWriter.WriteLine();
this.buildBatchWriter.WriteLine("--{0}--", this.changesetBoundary);
this.changesetStarted = false;
}
}
/// operation with exception
/// exception object
/// request object
/// response object
private void HandleOperationException(Exception e, HttpWebRequest httpWebRequest, HttpWebResponse response)
{
if (null != response)
{
this.HandleOperationResponse(httpWebRequest, response);
this.HandleOperationResponseData(response);
this.HandleOperationEnd();
}
else
{
this.HandleOperationStart();
WriteOperationResponseHeaders(this.buildBatchWriter, 500);
this.buildBatchWriter.WriteLine("{0}: {1}", XmlConstants.HttpContentType, XmlConstants.MimeTextPlain);
this.buildBatchWriter.WriteLine("{0}: {1}", XmlConstants.HttpContentID, this.ChangedEntries[this.entryIndex].ChangeOrder);
this.buildBatchWriter.WriteLine();
this.buildBatchWriter.WriteLine(e.ToString());
this.HandleOperationEnd();
}
this.request = null;
if (!IsFlagSet(this.options, SaveChangesOptions.ContinueOnError))
{
this.IsCompleted = true;
// if it was a media link entry don't even try to do a PUT if the POST didn't succeed
this.procesingMediaLinkEntry = false;
}
}
/// operation with HttpWebResponse
/// request object
/// response object
private void HandleOperationResponse(HttpWebRequest httpWebRequest, HttpWebResponse response)
{
this.HandleOperationStart();
string location = null;
if (this.ChangedEntries[this.entryIndex].IsResource &&
this.ChangedEntries[this.entryIndex].State == EntityStates.Added)
{
location = response.Headers[XmlConstants.HttpResponseLocation];
if (WebUtil.SuccessStatusCode(response.StatusCode))
{
if (null != location)
{
this.Context.AttachLocation(((ResourceBox)this.ChangedEntries[this.entryIndex]).Resource, location);
}
else
{
throw Error.NotSupported(Strings.Deserialize_NoLocationHeader);
}
}
}
if ((null == location) && (null != httpWebRequest))
{
location = httpWebRequest.RequestUri.OriginalString;
}
WriteOperationResponseHeaders(this.buildBatchWriter, (int)response.StatusCode);
foreach (string name in response.Headers.AllKeys)
{
if (XmlConstants.HttpContentLength != name)
{
this.buildBatchWriter.WriteLine("{0}: {1}", name, response.Headers[name]);
}
}
this.buildBatchWriter.WriteLine("{0}: {1}", XmlConstants.HttpContentID, this.ChangedEntries[this.entryIndex].ChangeOrder);
this.buildBatchWriter.WriteLine();
}
///
/// copy the response data
///
/// response object
private void HandleOperationResponseData(HttpWebResponse response)
{
using (Stream stream = response.GetResponseStream())
{
if (null != stream)
{
this.buildBatchWriter.Flush();
if (0 == WebUtil.CopyStream(stream, this.buildBatchWriter.BaseStream, ref this.buildBatchBuffer))
{
this.HandleOperationResponseNoData();
}
}
}
}
/// only call when no data was written to added "Content-Length: 0"
private void HandleOperationResponseNoData()
{
#if DEBUG
MemoryStream memory = this.buildBatchWriter.BaseStream as MemoryStream;
Debug.Assert(null != memory, "expected MemoryStream");
Debug.Assert(
(char)memory.GetBuffer()[(int)memory.Length - 2] == '\r' &&
(char)memory.GetBuffer()[(int)memory.Length - 1] == '\n',
"didn't end with newline");
#endif
this.buildBatchWriter.BaseStream.Position -= 2;
this.buildBatchWriter.WriteLine("{0}: {1}", XmlConstants.HttpContentLength, 0);
this.buildBatchWriter.WriteLine();
}
#endregion
///
/// create the web request for a batch
///
/// memory stream for length
/// httpweb request
private HttpWebRequest CreateBatchRequest(MemoryStream memory)
{
Uri requestUri = Util.CreateUri(this.Context.baseUriWithSlash, Util.CreateUri("$batch", UriKind.Relative));
string contentType = XmlConstants.MimeMultiPartMixed + "; " + XmlConstants.HttpMultipartBoundary + "=" + this.batchBoundary;
HttpWebRequest httpWebRequest = this.Context.CreateRequest(requestUri, XmlConstants.HttpMethodPost, false, contentType);
httpWebRequest.ContentLength = memory.Length - memory.Position;
return httpWebRequest;
}
/// generate the batch request of all changes to save
/// whether we need to update MERGE or PUT method for update.
/// buffer containing data for request stream
private MemoryStream GenerateBatchRequest(bool replaceOnUpdate)
{
this.changesetBoundary = null;
if (null != this.Queries)
{
}
else if (0 == this.ChangedEntries.Count)
{
this.DataServiceResponse = new DataServiceResponse(null, (int)WebExceptionStatus.Success, this.Responses, true /*batchResponse*/);
this.IsCompleted = true;
return null;
}
else
{
this.changesetBoundary = XmlConstants.HttpMultipartBoundaryChangeSet + "_" + Guid.NewGuid().ToString();
}
MemoryStream memory = new MemoryStream();
StreamWriter text = new StreamWriter(memory); // defaults to UTF8 w/o preamble
if (null != this.Queries)
{
for (int i = 0; i < this.Queries.Length; ++i)
{
Uri requestUri = Util.CreateUri(this.Context.baseUriWithSlash, this.Queries[i].RequestUri);
Debug.Assert(null != requestUri, "request uri is null");
Debug.Assert(requestUri.IsAbsoluteUri, "request uri is not absolute uri");
Debug.Assert(UriUtil.UriInvariantInsensitiveIsBaseOf(this.Context.baseUriWithSlash, requestUri), "context is not base of request uri");
text.WriteLine("--{0}", this.batchBoundary);
WriteOperationRequestHeaders(text, XmlConstants.HttpMethodGet, requestUri.AbsoluteUri);
text.WriteLine();
}
}
else if (0 < this.ChangedEntries.Count)
{
text.WriteLine("--{0}", this.batchBoundary);
text.WriteLine("{0}: {1}; boundary={2}", XmlConstants.HttpContentType, XmlConstants.MimeMultiPartMixed, this.changesetBoundary);
text.WriteLine();
for (int i = 0; i < this.ChangedEntries.Count; ++i)
{
#region validate changeset boundary starts on newline
#if DEBUG
{
text.Flush();
Debug.Assert(
(char)memory.GetBuffer()[(int)memory.Length - 2] == '\r' &&
(char)memory.GetBuffer()[(int)memory.Length - 1] == '\n',
"boundary didn't start with newline");
}
#endif
#endregion
Entry entry = this.ChangedEntries[i];
if (entry.ContentGeneratedForSave)
{
continue;
}
text.WriteLine("--{0}", this.changesetBoundary);
MemoryStream stream = this.CreateChangeData(i, true);
if (entry.IsResource)
{
ResourceBox box = (ResourceBox)entry;
// media link entry creation is not supported in batch mode
if (box.State == EntityStates.Added &&
ClientType.Create(box.Resource.GetType()).MediaDataMember != null)
{
throw Error.NotSupported(Strings.Context_BatchNotSupportedForMediaLink);
}
this.Context.CreateRequestBatch(box, text, replaceOnUpdate);
}
else
{
this.Context.CreateRequestBatch((RelatedEnd)entry, text);
}
byte[] buffer = null;
int bufferOffset = 0, bufferLength = 0;
if (null != stream)
{
buffer = stream.GetBuffer();
bufferOffset = checked((int)stream.Position);
bufferLength = checked((int)stream.Length) - bufferOffset;
}
if (0 < bufferLength)
{
text.WriteLine("{0}: {1}", XmlConstants.HttpContentLength, bufferLength);
}
text.WriteLine(); // NewLine separates header from message
if (0 < bufferLength)
{
text.Flush();
text.BaseStream.Write(buffer, bufferOffset, bufferLength);
}
}
#region validate changeset boundary ended with newline
#if DEBUG
{
text.Flush();
Debug.Assert(
(char)memory.GetBuffer()[(int)memory.Length - 2] == '\r' &&
(char)memory.GetBuffer()[(int)memory.Length - 1] == '\n',
"post CreateRequest boundary didn't start with newline");
}
#endif
#endregion
// The boundary delimiter line following the last body part
// has two more hyphens after the boundary parameter value.
text.WriteLine("--{0}--", this.changesetBoundary);
}
text.WriteLine("--{0}--", this.batchBoundary);
text.Flush();
Debug.Assert(Object.ReferenceEquals(text.BaseStream, memory), "should be same");
Debug.Assert(this.ChangedEntries.All(o => o.ContentGeneratedForSave), "didn't generated content for all entities/links");
#region Validate batch format
#if DEBUG
int testGetCount = 0;
int testOpCount = 0;
int testBeginSetCount = 0;
int testEndSetCount = 0;
memory.Position = 0;
BatchStream testBatch = new BatchStream(memory, this.batchBoundary, HttpProcessUtility.EncodingUtf8NoPreamble, true);
while (testBatch.MoveNext())
{
switch (testBatch.State)
{
case BatchStreamState.StartBatch:
case BatchStreamState.EndBatch:
default:
Debug.Assert(false, "shouldn't happen");
break;
case BatchStreamState.Get:
testGetCount++;
break;
case BatchStreamState.BeginChangeSet:
testBeginSetCount++;
break;
case BatchStreamState.EndChangeSet:
testEndSetCount++;
break;
case BatchStreamState.Post:
case BatchStreamState.Put:
case BatchStreamState.Delete:
case BatchStreamState.Merge:
testOpCount++;
break;
}
}
Debug.Assert((null == this.Queries && 1 == testBeginSetCount) || (0 == testBeginSetCount), "more than one BeginChangeSet");
Debug.Assert(testBeginSetCount == testEndSetCount, "more than one EndChangeSet");
Debug.Assert((null == this.Queries && testGetCount == 0) || this.Queries.Length == testGetCount, "too many get count");
// Debug.Assert(this.ChangedEntries.Count == testOpCount, "too many op count");
Debug.Assert(BatchStreamState.EndBatch == testBatch.State, "should have ended propertly");
#endif
#endregion
this.changesetBoundary = null;
memory.Position = 0;
return memory;
}
#region handle batch response
///
/// process the batch changeset response
///
private void HandleBatchResponse()
{
string boundary = this.batchBoundary;
Encoding encoding = Encoding.UTF8;
Dictionary headers = null;
Exception exception = null;
try
{
if (IsFlagSet(this.options, SaveChangesOptions.Batch))
{
if ((null == this.batchResponse) || (HttpStatusCode.NoContent == this.batchResponse.StatusCode))
{ // we always expect a response to our batch POST request
throw Error.InvalidOperation(Strings.Batch_ExpectedResponse(1));
}
headers = WebUtil.WrapResponseHeaders(this.batchResponse);
HandleResponse(
this.batchResponse.StatusCode, // statusCode
this.batchResponse.Headers[XmlConstants.HttpDataServiceVersion], // responseVersion
delegate() { return this.httpWebResponseStream; }, // getResponseStream
true); // throwOnFailure
if (!BatchStream.GetBoundaryAndEncodingFromMultipartMixedContentType(this.batchResponse.ContentType, out boundary, out encoding))
{
string mime;
Exception inner = null;
HttpProcessUtility.ReadContentType(this.batchResponse.ContentType, out mime, out encoding);
if (String.Equals(XmlConstants.MimeTextPlain, mime))
{
inner = GetResponseText(this.batchResponse.GetResponseStream, this.batchResponse.StatusCode);
}
throw Error.InvalidOperation(Strings.Batch_ExpectedContentType(this.batchResponse.ContentType), inner);
}
if (null == this.httpWebResponseStream)
{
Error.ThrowBatchExpectedResponse(InternalError.NullResponseStream);
}
this.DataServiceResponse = new DataServiceResponse(headers, (int)this.batchResponse.StatusCode, this.Responses, true /*batchResponse*/);
}
bool close = true;
BatchStream batchStream = null;
try
{
batchStream = this.responseBatchStream ?? new BatchStream(this.httpWebResponseStream, boundary, encoding, false);
this.httpWebResponseStream = null;
this.responseBatchStream = null;
IEnumerable responses = this.HandleBatchResponse(batchStream);
if (IsFlagSet(this.options, SaveChangesOptions.Batch) && (null != this.Queries))
{
// ExecuteBatch, EndExecuteBatch
close = false;
this.responseBatchStream = batchStream;
this.DataServiceResponse = new DataServiceResponse(
(Dictionary)this.DataServiceResponse.BatchHeaders,
this.DataServiceResponse.BatchStatusCode,
responses,
true /*batchResponse*/);
}
else
{ // SaveChanges, EndSaveChanges
// enumerate the entire response
foreach (ChangeOperationResponse response in responses)
{
if (exception == null && response.Error != null)
{
exception = response.Error;
}
}
}
}
finally
{
if (close && (null != batchStream))
{
batchStream.Close();
}
}
}
catch (InvalidOperationException ex)
{
exception = ex;
}
if (exception != null)
{
if (this.DataServiceResponse == null)
{
int statusCode = this.batchResponse == null ? (int)HttpStatusCode.InternalServerError : (int)this.batchResponse.StatusCode;
this.DataServiceResponse = new DataServiceResponse(headers, statusCode, null, IsFlagSet(this.options, SaveChangesOptions.Batch));
}
throw new DataServiceRequestException(Strings.DataServiceException_GeneralError, exception, this.DataServiceResponse);
}
}
///
/// process the batch changeset response
///
/// batch stream
/// enumerable of QueryResponse or null
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506", Justification = "Central method of the API, likely to have many cross-references")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031", Justification = "Cache exception so user can examine it later")]
private IEnumerable HandleBatchResponse(BatchStream batch)
{
if (!batch.CanRead)
{
yield break;
}
string contentType;
string location;
string etag;
Uri editLink = null;
HttpStatusCode status;
int changesetIndex = 0;
int queryCount = 0;
int operationCount = 0;
this.entryIndex = 0;
while (batch.MoveNext())
{
var contentHeaders = batch.ContentHeaders; // get the headers before materialize clears them
Entry entry;
switch (batch.State)
{
#region BeginChangeSet
case BatchStreamState.BeginChangeSet:
if ((IsFlagSet(this.options, SaveChangesOptions.Batch) && (0 != changesetIndex)) ||
(!IsFlagSet(this.options, SaveChangesOptions.Batch) && (this.ChangedEntries.Count <= changesetIndex)) ||
(0 != operationCount))
{ // for now, we only send a single batch, single changeset
Error.ThrowBatchUnexpectedContent(InternalError.UnexpectedBeginChangeSet);
}
break;
#endregion
#region EndChangeSet
case BatchStreamState.EndChangeSet:
// move forward to next expected changelist
changesetIndex++;
operationCount = 0;
break;
#endregion
#region GetResponse
case BatchStreamState.GetResponse:
Debug.Assert(0 == operationCount, "missing an EndChangeSet 2");
contentHeaders.TryGetValue(XmlConstants.HttpContentType, out contentType);
status = (HttpStatusCode)(-1);
Exception ex = null;
QueryOperationResponse qresponse = null;
try
{
status = batch.GetStatusCode();
ex = HandleResponse(status, batch.GetResponseVersion(), batch.GetContentStream, false);
if (null == ex)
{
DataServiceRequest query = this.Queries[queryCount];
System.Collections.IEnumerable enumerable = query.Materialize(this.Context, contentType, batch.GetContentStream);
qresponse = QueryOperationResponse.GetInstance(query.ElementType, contentHeaders, query, enumerable);
}
}
catch (ArgumentException e)
{
ex = e;
}
catch (FormatException e)
{
ex = e;
}
catch (InvalidOperationException e)
{
ex = e;
}
if (null == qresponse)
{
DataServiceRequest query = this.Queries[queryCount];
qresponse = QueryOperationResponse.GetInstance(query.ElementType, contentHeaders, query, null);
qresponse.Error = ex;
}
qresponse.StatusCode = (int)status;
queryCount++;
yield return qresponse;
break;
#endregion
#region ChangeResponse
case BatchStreamState.ChangeResponse:
if (this.ChangedEntries.Count <= unchecked((uint)this.entryIndex))
{
Error.ThrowBatchUnexpectedContent(InternalError.TooManyBatchResponse);
}
HttpStatusCode statusCode = batch.GetStatusCode();
Exception error = HandleResponse(statusCode, batch.GetResponseVersion(), batch.GetContentStream, false);
int index = this.ValidateContentID(contentHeaders);
try
{
entry = this.ChangedEntries[index];
operationCount += this.Context.SaveResultProcessed(entry);
if (null != error)
{
throw error;
}
switch (entry.State)
{
#region Post
case EntityStates.Added:
if (entry.IsResource)
{
string mime = null;
Encoding postEncoding = null;
contentHeaders.TryGetValue(XmlConstants.HttpContentType, out contentType);
contentHeaders.TryGetValue(XmlConstants.HttpResponseLocation, out location);
contentHeaders.TryGetValue(XmlConstants.HttpResponseETag, out etag);
editLink = (null != location) ? Util.CreateUri(location, UriKind.Absolute) : null;
ResourceBox box = (ResourceBox)entry;
Stream stream = batch.GetContentStream();
if (null != stream)
{
HttpProcessUtility.ReadContentType(contentType, out mime, out postEncoding);
if (!String.Equals(XmlConstants.MimeApplicationAtom, mime, StringComparison.OrdinalIgnoreCase))
{
throw Error.InvalidOperation(Strings.Deserialize_UnknownMimeTypeSpecified(mime));
}
XmlReader reader = XmlUtil.CreateXmlReader(stream, postEncoding);
using (MaterializeAtom atom = new MaterializeAtom(this.Context, reader, box.Resource.GetType(), MergeOption.OverwriteChanges))
{
this.Context.HandleResponsePost(box, atom, editLink, etag);
}
}
else
{
if (null == editLink)
{
string entitySetName = box.Identity.OriginalString;
editLink = GenerateEditLinkUri(this.Context.baseUriWithSlash, entitySetName, box.Resource);
}
this.Context.HandleResponsePost(box, null, editLink, etag);
}
}
else
{
HandleResponsePost((RelatedEnd)entry);
}
break;
#endregion
#region Put, Merge
case EntityStates.Modified:
contentHeaders.TryGetValue(XmlConstants.HttpResponseETag, out etag);
HandleResponsePut(entry, etag);
break;
#endregion
#region Delete
case EntityStates.Deleted:
this.Context.HandleResponseDelete(entry);
break;
#endregion
}
}
catch (Exception e)
{
this.ChangedEntries[index].SaveError = e;
error = e;
}
ChangeOperationResponse changeOperationResponse = new ChangeOperationResponse(contentHeaders, BuildReturn(this.ChangedEntries[index]));
changeOperationResponse.StatusCode = (int)statusCode;
if (error != null)
{
changeOperationResponse.Error = error;
}
this.Responses.Add(changeOperationResponse);
operationCount++;
this.entryIndex++;
yield return changeOperationResponse;
break;
#endregion
default:
Error.ThrowBatchExpectedResponse(InternalError.UnexpectedBatchState);
break;
}
}
Debug.Assert(batch.State == BatchStreamState.EndBatch, "unexpected batch state");
// Check for a changeset without response (first line) or GET request without response (second line).
// either all saved entries must be processed or it was a batch and one of the entries has the error
if (((null == this.Queries) && ((0 == changesetIndex) ||
(0 < queryCount) ||
(this.ChangedEntries.Any(o => o.ContentGeneratedForSave != (0 != o.SaveResultWasProcessed)) &&
(!IsFlagSet(this.options, SaveChangesOptions.Batch) ||
(null == this.ChangedEntries.FirstOrDefault(o => (null != o.SaveError))))))) ||
((null != this.Queries) && (queryCount != this.Queries.Length)))
{
throw Error.InvalidOperation(Strings.Batch_IncompleteResponseCount);
}
batch.Dispose();
}
///
/// validate the content-id
///
/// headers
/// return the correct ChangedEntries index
private int ValidateContentID(Dictionary contentHeaders)
{
int contentID = 0;
string contentValueID;
if (!contentHeaders.TryGetValue(XmlConstants.HttpContentID, out contentValueID) ||
!Int32.TryParse(contentValueID, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out contentID))
{
Error.ThrowBatchUnexpectedContent(InternalError.ChangeResponseMissingContentID);
}
for (int i = 0; i < this.ChangedEntries.Count; ++i)
{
if (this.ChangedEntries[i].ChangeOrder == contentID)
{
return i;
}
}
Error.ThrowBatchUnexpectedContent(InternalError.ChangeResponseUnknownContentID);
return -1;
}
#endregion Batch
#region callback handlers
/// handle request.BeginGetRequestStream with request.EndGetRquestStream and then write out request stream
/// async result
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "required for this feature")]
private void AsyncEndGetRequestStream(IAsyncResult asyncResult)
{
PerRequest pereq = asyncResult.AsyncState as PerRequest;
try
{
int step = CompleteCheck(pereq, InternalError.InvalidEndGetRequestCompleted);
pereq.RequestCompletedSynchronously &= asyncResult.CompletedSynchronously; // BeginGetRequestStream
EqualRefCheck(this.request, pereq, InternalError.InvalidEndGetRequestStream);
HttpWebRequest httpWebRequest = Util.NullCheck(pereq.Request, InternalError.InvalidEndGetRequestStreamRequest);
Stream stream = Util.NullCheck(httpWebRequest.EndGetRequestStream(asyncResult), InternalError.InvalidEndGetRequestStreamStream);
pereq.RequestStream = stream;
MemoryStream memoryStream = Util.NullCheck(pereq.RequestStreamContent, InternalError.InvalidEndGetRequestStreamContent);
byte[] buffer = memoryStream.GetBuffer();
int bufferOffset = checked((int)memoryStream.Position);
int bufferLength = checked((int)memoryStream.Length) - bufferOffset;
if ((null == buffer) || (0 == bufferLength))
{
Error.ThrowInternalError(InternalError.InvalidEndGetRequestStreamContentLength);
}
// the following is useful in the debugging Immediate Window
// string x = System.Text.Encoding.UTF8.GetString(buffer, bufferOffset, bufferLength);
asyncResult = stream.BeginWrite(buffer, bufferOffset, bufferLength, this.AsyncEndWrite, pereq);
bool reallyCompletedSynchronously = asyncResult.CompletedSynchronously && (step < pereq.RequestStep);
pereq.RequestCompletedSynchronously &= reallyCompletedSynchronously; // BeginWrite
}
catch (Exception e)
{
if (this.HandleFailure(pereq, e))
{
throw;
}
}
finally
{
this.HandleCompleted(pereq);
}
}
/// handle reqestStream.BeginWrite with requestStream.EndWrite then BeginGetResponse
/// async result
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "required for this feature")]
private void AsyncEndWrite(IAsyncResult asyncResult)
{
PerRequest pereq = asyncResult.AsyncState as PerRequest;
try
{
int step = CompleteCheck(pereq, InternalError.InvalidEndWriteCompleted);
pereq.RequestCompletedSynchronously &= asyncResult.CompletedSynchronously; // BeginWrite
EqualRefCheck(this.request, pereq, InternalError.InvalidEndWrite);
HttpWebRequest httpWebRequest = Util.NullCheck(pereq.Request, InternalError.InvalidEndWriteRequest);
Stream stream = Util.NullCheck(pereq.RequestStream, InternalError.InvalidEndWriteStream);
stream.EndWrite(asyncResult);
pereq.RequestStream = null;
stream.Close();
stream = pereq.RequestStreamContent;
if (null != stream)
{
pereq.RequestStreamContent = null;
stream.Dispose();
}
asyncResult = httpWebRequest.BeginGetResponse(this.AsyncEndGetResponse, pereq);
bool reallyCompletedSynchronously = asyncResult.CompletedSynchronously && (step < pereq.RequestStep);
pereq.RequestCompletedSynchronously &= reallyCompletedSynchronously; // BeginGetResponse
}
catch (Exception e)
{
if (this.HandleFailure(pereq, e))
{
throw;
}
}
finally
{
this.HandleCompleted(pereq);
}
}
/// handle request.BeginGetResponse with request.EndGetResponse and then copy response stream
/// async result
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "required for this feature")]
private void AsyncEndGetResponse(IAsyncResult asyncResult)
{
PerRequest pereq = asyncResult.AsyncState as PerRequest;
try
{
int step = CompleteCheck(pereq, InternalError.InvalidEndGetResponseCompleted);
pereq.RequestCompletedSynchronously &= asyncResult.CompletedSynchronously; // BeginGetResponse
EqualRefCheck(this.request, pereq, InternalError.InvalidEndGetResponse);
HttpWebRequest httpWebRequest = Util.NullCheck(pereq.Request, InternalError.InvalidEndGetResponseRequest);
// the httpWebResponse is kept for batching, discarded by non-batch
HttpWebResponse response = null;
try
{
response = (HttpWebResponse)httpWebRequest.EndGetResponse(asyncResult);
}
catch (WebException e)
{
response = (HttpWebResponse)e.Response;
}
pereq.HttpWebResponse = Util.NullCheck(response, InternalError.InvalidEndGetResponseResponse);
if (!IsFlagSet(this.options, SaveChangesOptions.Batch))
{
this.HandleOperationResponse(httpWebRequest, response);
}
this.copiedContentLength = 0;
Stream stream = response.GetResponseStream();
pereq.ResponseStream = stream;
if ((null != stream) && stream.CanRead)
{
if (null != this.buildBatchWriter)
{
this.buildBatchWriter.Flush();
}
if (null == this.buildBatchBuffer)
{
this.buildBatchBuffer = new byte[8000];
}
bool reallyCompletedSynchronously = false;
do
{
asyncResult = stream.BeginRead(this.buildBatchBuffer, 0, this.buildBatchBuffer.Length, this.AsyncEndRead, pereq);
reallyCompletedSynchronously = asyncResult.CompletedSynchronously && (step < pereq.RequestStep);
pereq.RequestCompletedSynchronously &= reallyCompletedSynchronously; // BeginRead
}
while (reallyCompletedSynchronously && !pereq.RequestCompleted && !this.IsCompleted && stream.CanRead);
}
else
{
pereq.RequestCompleted = true;
// BeginGetResponse could fail and callback still invoked
if (!this.IsCompleted)
{
this.SaveNextChange(pereq);
}
}
}
catch (Exception e)
{
if (this.HandleFailure(pereq, e))
{
throw;
}
}
finally
{
this.HandleCompleted(pereq);
}
}
/// handle responseStream.BeginRead with responseStream.EndRead
/// async result
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "required for this feature")]
private void AsyncEndRead(IAsyncResult asyncResult)
{
PerRequest pereq = asyncResult.AsyncState as PerRequest;
int count = 0;
try
{
int step = CompleteCheck(pereq, InternalError.InvalidEndReadCompleted);
pereq.RequestCompletedSynchronously &= asyncResult.CompletedSynchronously; // BeginRead
EqualRefCheck(this.request, pereq, InternalError.InvalidEndRead);
Stream stream = Util.NullCheck(pereq.ResponseStream, InternalError.InvalidEndReadStream);
count = stream.EndRead(asyncResult);
if (0 < count)
{
Stream outputResponse = Util.NullCheck(this.httpWebResponseStream, InternalError.InvalidEndReadCopy);
outputResponse.Write(this.buildBatchBuffer, 0, count);
this.copiedContentLength += count;
if (!asyncResult.CompletedSynchronously && stream.CanRead)
{ // if CompletedSynchronously then caller will call and we reduce risk of stack overflow
bool reallyCompletedSynchronously = false;
do
{
asyncResult = stream.BeginRead(this.buildBatchBuffer, 0, this.buildBatchBuffer.Length, this.AsyncEndRead, pereq);
reallyCompletedSynchronously = asyncResult.CompletedSynchronously && (step < pereq.RequestStep);
pereq.RequestCompletedSynchronously &= reallyCompletedSynchronously; // BeginRead
}
while (reallyCompletedSynchronously && !pereq.RequestCompleted && !this.IsCompleted && stream.CanRead);
}
}
else
{
pereq.RequestCompleted = true;
// BeginRead could fail and callback still invoked
if (!this.IsCompleted)
{
this.SaveNextChange(pereq);
}
}
}
catch (Exception e)
{
if (this.HandleFailure(pereq, e))
{
throw;
}
}
finally
{
this.HandleCompleted(pereq);
}
}
/// continue with the next change
/// the completed per request object
private void SaveNextChange(PerRequest pereq)
{
Debug.Assert(this.executeAsync, "should be async");
if (!pereq.RequestCompleted)
{
Error.ThrowInternalError(InternalError.SaveNextChangeIncomplete);
}
++pereq.RequestStep;
EqualRefCheck(this.request, pereq, InternalError.InvalidSaveNextChange);
if (IsFlagSet(this.options, SaveChangesOptions.Batch))
{
this.httpWebResponseStream.Position = 0;
this.request = null;
this.IsCompleted = true;
}
else
{
if (0 == this.copiedContentLength)
{
this.HandleOperationResponseNoData();
}
this.HandleOperationEnd();
if (!this.procesingMediaLinkEntry)
{
this.changesCompleted++;
}
pereq.Dispose();
this.request = null;
if (!pereq.RequestCompletedSynchronously)
{ // you can't chain synchronously completed responses without risking StackOverflow, caller will loop to next
if (!this.IsCompleted)
{
this.BeginNextChange(IsFlagSet(this.options, SaveChangesOptions.ReplaceOnUpdate));
}
}
}
}
#endregion
/// wrap the full request
private sealed class PerRequest
{
/// ctor
internal PerRequest()
{
this.RequestCompletedSynchronously = true;
}
/// active web request
internal HttpWebRequest Request
{
get;
set;
}
/// active web request stream
internal Stream RequestStream
{
get;
set;
}
/// content to write to request stream
internal MemoryStream RequestStreamContent
{
get;
set;
}
/// web response
internal HttpWebResponse HttpWebResponse
{
get;
set;
}
/// async web response stream
internal Stream ResponseStream
{
get;
set;
}
/// did the request complete all of its steps synchronously?
internal bool RequestCompletedSynchronously
{
get;
set;
}
/// did the sequence (BeginGetRequest, EndGetRequest, ... complete
internal bool RequestCompleted
{
get;
set;
}
///
/// If CompletedSynchronously and requestStep didn't increment, then underlying implementation lied.
///
internal int RequestStep
{
get;
set;
}
///
/// dispose of the request object
///
internal void Dispose()
{
Stream stream;
if (null != (stream = this.ResponseStream))
{
this.ResponseStream = null;
stream.Dispose();
}
if (null != (stream = this.RequestStreamContent))
{
this.RequestStreamContent = null;
stream.Dispose();
}
if (null != (stream = this.RequestStream))
{
this.RequestStream = null;
stream.Dispose();
}
HttpWebResponse response = this.HttpWebResponse;
if (null != response)
{
response.Close();
}
this.Request = null;
this.RequestCompleted = true;
}
}
}
}
}
// File provided for Reference Use Only by Microsoft Corporation (c) 2007.
//----------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//
// context
//
//---------------------------------------------------------------------
namespace System.Data.Services.Client
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
#if !ASTORIA_LIGHT // Data.Services http stack
using System.Net;
#else
using System.Data.Services.Http;
#endif
using System.Text;
using System.Xml;
using System.Xml.Linq;
///
/// context
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506", Justification = "Central class of the API, likely to have many cross-references")]
public class DataServiceContext
{
/// represents identity for a resource without one
private const Uri NoIdentity = null;
/// represents entityset for a resource without one
private const string NoEntitySet = null;
/// represents empty etag
private const string NoETag = null;
/// base uri prepended to relative uri
private readonly System.Uri baseUri;
/// base uri with guranteed trailing slash
private readonly System.Uri baseUriWithSlash;
#if !ASTORIA_LIGHT // Credentials not available
/// Authentication interface for retrieving credentials for Web client authentication.
private System.Net.ICredentials credentials;
#endif
/// Override the namespace used for the data parts of the ATOM entries
private string dataNamespace;
/// resolve type from a typename
private Func resolveName;
/// resolve typename from a type
private Func resolveType;
#if !ASTORIA_LIGHT // Timeout not available
/// time-out value in seconds, 0 for default
private int timeout;
#endif
/// whether to use post-tunneling for PUT/DELETE
private bool postTunneling;
/// Options when deserializing properties to the target type.
private bool ignoreMissingProperties;
/// Used to specify a value synchronization strategy.
private MergeOption mergeOption;
/// Default options to be used while doing savechanges.
private SaveChangesOptions saveChangesDefaultOptions;
/// Override the namespace used for the scheme in the category for ATOM entries.
private Uri typeScheme;
#region Resource state management
/// change order
private uint nextChange;
/// Set of tracked resources by ResourceBox.Resource
private Dictionary objectToResource = new Dictionary();
/// Set of tracked resources by ResourceBox.Identity
private Dictionary identityToResource;
/// Set of tracked bindings by ResourceBox.Identity
private Dictionary bindings = new Dictionary(RelatedEnd.EquivalenceComparer);
#endregion
#region ctor
///
/// Instantiates a new context with the specified Uri.
/// The library expects the Uri to point to the root of a data service,
/// but does not issue a request to validate it does indeed identify the root of a service.
/// If the Uri does not identify the root of the service, the behavior of the client library is undefined.
///
///
/// An absolute, well formed http or https URI without a query or fragment which identifies the root of a data service.
/// A Uri provided with a trailing slash is equivalent to one without such a trailing character
///
/// if the is not an absolute, well formed http or https URI without a query or fragment
/// when the is null
///
/// With Silverlight, the can be a relative Uri
/// that will be combined with System.Windows.Browser.HtmlPage.Document.DocumentUri.
///
public DataServiceContext(Uri serviceRoot)
{
Util.CheckArgumentNull(serviceRoot, "serviceRoot");
#if ASTORIA_LIGHT
if (!serviceRoot.IsAbsoluteUri)
{
serviceRoot = new Uri(System.Windows.Browser.HtmlPage.Document.DocumentUri, serviceRoot);
}
#endif
if (!serviceRoot.IsAbsoluteUri ||
!Uri.IsWellFormedUriString(serviceRoot.OriginalString, UriKind.Absolute) ||
!String.IsNullOrEmpty(serviceRoot.Query) ||
!string.IsNullOrEmpty(serviceRoot.Fragment) ||
((serviceRoot.Scheme != "http") && (serviceRoot.Scheme != "https")))
{
throw Error.Argument(Strings.Context_BaseUri, "serviceRoot");
}
this.baseUri = serviceRoot;
this.baseUriWithSlash = serviceRoot;
if (!serviceRoot.OriginalString.EndsWith("/", StringComparison.Ordinal))
{
this.baseUriWithSlash = Util.CreateUri(serviceRoot.OriginalString + "/", UriKind.Absolute);
}
this.mergeOption = MergeOption.AppendOnly;
this.DataNamespace = XmlConstants.DataWebNamespace;
this.UsePostTunneling = false;
this.typeScheme = new Uri(XmlConstants.DataWebSchemeNamespace);
}
#endregion
#if !ASTORIA_LIGHT // Data.Services http stack
///
/// This event is fired before a request it sent to the server, giving
/// the handler the opportunity to inspect, adjust and/or replace the
/// WebRequest object used to perform the request.
///
///
/// When calling BeginSaveChanges and not using SaveChangesOptions.Batch,
/// this event may be raised from a different thread.
///
public event EventHandler SendingRequest;
#endif
///
/// This event fires once an entry has been read into a .NET object
/// but before the serializer returns to the caller, giving handlers
/// an opporunity to read further information from the incoming ATOM
/// entry and updating the object
///
///
/// This event should only be raised from the thread that was used to
/// invoke Execute, EndExecute, SaveChanges, EndSaveChanges.
///
public event EventHandler ReadingEntity;
///
/// This event fires once an ATOM entry is ready to be written to
/// the network for a request, giving handlers an opportunity to
/// customize the entry with information from the corresponding
/// .NET object or the environment.
///
///
/// When calling BeginSaveChanges and not using SaveChangesOptions.Batch,
/// this event may be raised from a different thread.
///
public event EventHandler WritingEntity;
#region BaseUri, Credentials, MergeOption, Timeout, Links, Entities
///
/// Absolute Uri identifying the root of the target data service.
/// A Uri provided with a trailing slash is equivalent to one without such a trailing character.
///
///
/// Example: http://server/host/myservice.svc
///
public Uri BaseUri
{
get { return this.baseUri; }
}
#if !ASTORIA_LIGHT // Credentials not available
///
/// Gets and sets the authentication information used by each query created using the context object.
///
public System.Net.ICredentials Credentials
{
get { return this.credentials; }
set { this.credentials = value; }
}
#endif
///
/// Used to specify a synchronization strategy when sending/receiving entities to/from a data service.
/// This value is read by the deserialization component of the client prior to materializing objects.
/// As such, it is recommended to set this property to the appropriate materialization strategy
/// before executing any queries/updates to the data service.
///
///
/// The default value is .AppendOnly.
///
public MergeOption MergeOption
{
get { return this.mergeOption; }
set { this.mergeOption = Util.CheckEnumerationValue(value, "MergeOption"); }
}
///
/// Are properties missing from target type ignored?
///
///
/// This also affects responses during SaveChanges.
///
public bool IgnoreMissingProperties
{
get { return this.ignoreMissingProperties; }
set { this.ignoreMissingProperties = value; }
}
/// Override the namespace used for the data parts of the ATOM entries
public string DataNamespace
{
get
{
return this.dataNamespace;
}
set
{
Util.CheckArgumentNull(value, "value");
this.dataNamespace = value;
}
}
///
/// Enables one to override the default type resolution strategy used by the client library.
/// Set this property to a delegate which identifies a function that resolves
/// a type within the client application to a namespace-qualified type name.
/// This enables the client to perform custom mapping between the type name
/// provided in a response from the server and a type on the client.
///
///
/// This method enables one to override the entity name that is serialized
/// to the target representation (ATOM,JSON, etc) for the specified type.
///
public Func ResolveName
{
get { return this.resolveName; }
set { this.resolveName = value; }
}
///
/// Enables one to override the default type resolution strategy used by the client library.
/// Set this property to a delegate which identifies a function that resolves a
/// namespace-qualified type name to type within the client application.
/// This enables the client to perform custom mapping between the type name
/// provided in a response from the server and a type on the client.
///
///
/// Overriding type resolution enables inserting a custom type name to type mapping strategy.
/// It does not enable one to affect how a response is materialized into the identified type.
///
public Func ResolveType
{
get { return this.resolveType; }
set { this.resolveType = value; }
}
#if !ASTORIA_LIGHT // Timeout not available
///
/// Get and sets the timeout span in seconds to use for the underlying HTTP request to the data service.
///
///
/// A value of 0 will use the default timeout of the underlying HTTP request.
/// This value must be set before executing any query or update operations against
/// the target data service for it to have effect on the on the request.
/// The value may be changed between requests to a data service and the new value
/// will be picked up by the next data service request.
///
public int Timeout
{
get
{
return this.timeout;
}
set
{
if (value < 0)
{
throw Error.ArgumentOutOfRange("Timeout");
}
this.timeout = value;
}
}
#endif
/// Gets or sets the URI used to indicate what type scheme is used by the service.
public Uri TypeScheme
{
get
{
return this.typeScheme;
}
set
{
Util.CheckArgumentNull(value, "value");
this.typeScheme = value;
}
}
/// whether to use post-tunneling for PUT/DELETE
public bool UsePostTunneling
{
get { return this.postTunneling; }
set { this.postTunneling = value; }
}
///
/// Returns a collection of all the links (ie. associations) currently being tracked by the context.
/// If no links are being tracked, a collection with 0 elements is returned.
///
public ReadOnlyCollection Links
{
get
{
return (from link in this.bindings.Values
orderby link.ChangeOrder
select new LinkDescriptor(link.SourceResource, link.SourceProperty, link.TargetResouce, link.State))
.ToList().AsReadOnly();
}
}
///
/// Returns a collection of all the resources currently being tracked by the context.
/// If no resources are being tracked, a collection with 0 elements is returned.
///
public ReadOnlyCollection Entities
{
get
{
return (from entity in this.objectToResource.Values
orderby entity.ChangeOrder
select new EntityDescriptor(entity.Resource, entity.ETag, entity.State))
.ToList().AsReadOnly();
}
}
///
/// Default SaveChangesOptions that needs to be used when doing SaveChanges.
///
public SaveChangesOptions SaveChangesDefaultOptions
{
get
{
return this.saveChangesDefaultOptions;
}
set
{
ValidateSaveChangesOptions(value);
this.saveChangesDefaultOptions = value;
}
}
#endregion
/// base uri with guranteed trailing slash
internal Uri BaseUriWithSlash
{
get { return this.baseUriWithSlash; }
}
/// Indicates if there are subscribers for the ReadingEntity event
internal bool HasReadingEntityHandlers
{
[DebuggerStepThrough]
get { return this.ReadingEntity != null; }
}
#region CreateQuery
///
/// create a query based on (BaseUri + relativeUri)
///
/// type of object to materialize
/// entitySetName
/// composible, enumerable query object
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "required for this feature")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1057:StringUriOverloadsCallSystemUriOverloads", Justification = "required for this feature")]
public DataServiceQuery CreateQuery(string entitySetName)
{
Util.CheckArgumentNotEmpty(entitySetName, "entitySetName");
this.ValidateEntitySetName(ref entitySetName);
ResourceSetExpression rse = new ResourceSetExpression(typeof(IOrderedQueryable), null, Expression.Constant(entitySetName), typeof(T), null, null);
return new DataServiceQuery.DataServiceOrderedQuery(rse, new DataServiceQueryProvider(this));
}
#endregion
#region GetMetadataUri
///
/// Given the base URI, resolves the location of the metadata endpoint for the service by using an HTTP OPTIONS request or falling back to convention ($metadata)
///
/// Uri to retrieve metadata from
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "required for this feature")]
public Uri GetMetadataUri()
{
//
Uri metadataUri = Util.CreateUri(this.baseUriWithSlash.OriginalString + XmlConstants.UriMetadataSegment, UriKind.Absolute);
return metadataUri;
}
#endregion
#region LoadProperty
///
/// Begin getting response to load a collection or reference property.
///
/// actually doesn't modify the property until EndLoadProperty is called.
/// entity
/// name of collection or reference property to load
/// The AsyncCallback delegate.
/// The state object for this request.
/// An IAsyncResult that references the asynchronous request for a response.
public IAsyncResult BeginLoadProperty(object entity, string propertyName, AsyncCallback callback, object state)
{
LoadPropertyAsyncResult result = this.CreateLoadPropertyRequest(entity, propertyName, callback, state);
result.BeginExecute(null);
return result;
}
///
/// Load a collection or reference property from a async result.
///
/// async result generated by BeginLoadProperty
/// QueryOperationResponse instance containing information about the response.
public QueryOperationResponse EndLoadProperty(IAsyncResult asyncResult)
{
LoadPropertyAsyncResult response = QueryAsyncResult.EndExecute(this, "LoadProperty", asyncResult);
return response.LoadProperty();
}
#if !ASTORIA_LIGHT // Synchronous methods not available
///
/// Load a collection or reference property.
///
///
/// An entity in detached or added state will throw an InvalidOperationException
/// since there is nothing it can load from the server.
///
/// An entity in unchanged or modified state will load its collection or
/// reference elements as unchanged with unchanged bindings.
///
/// An entity in deleted state will loads its collection or reference elements
/// in the unchanged state with bindings in the deleted state.
///
/// entity
/// name of collection or reference property to load
/// QueryOperationResponse instance containing information about the response.
public QueryOperationResponse LoadProperty(object entity, string propertyName)
{
LoadPropertyAsyncResult result = this.CreateLoadPropertyRequest(entity, propertyName, null, null);
result.Execute(null);
return result.LoadProperty();
}
#endif
#endregion
#region ExecuteBatch, BeginExecuteBatch, EndExecuteBatch
///
/// Batch multiple queries into a single async request.
///
/// User callback when results from batch are available.
/// user state in IAsyncResult
/// queries to batch
/// async result object
public IAsyncResult BeginExecuteBatch(AsyncCallback callback, object state, params DataServiceRequest[] queries)
{
Util.CheckArgumentNotEmpty(queries, "queries");
SaveAsyncResult result = new SaveAsyncResult(this, "ExecuteBatch", queries, SaveChangesOptions.Batch, callback, state, true);
result.BatchBeginRequest(false /*replaceOnUpdate*/);
return result;
}
///
/// Call when results from batch are desired.
///
/// async result object returned from BeginExecuteBatch
/// batch response from which query results can be enumerated.
public DataServiceResponse EndExecuteBatch(IAsyncResult asyncResult)
{
SaveAsyncResult result = BaseAsyncResult.EndExecute(this, "ExecuteBatch", asyncResult);
return result.EndRequest();
}
#if !ASTORIA_LIGHT // Synchronous methods not available
///
/// Batch multiple queries into a single request.
///
/// queries to batch
/// batch response from which query results can be enumerated.
public DataServiceResponse ExecuteBatch(params DataServiceRequest[] queries)
{
Util.CheckArgumentNotEmpty(queries, "queries");
SaveAsyncResult result = new SaveAsyncResult(this, "ExecuteBatch", queries, SaveChangesOptions.Batch, null, null, false);
result.BatchRequest(false /*replaceOnUpdate*/);
return result.EndRequest();
}
#endif
#endregion
#region Execute(Uri), BeginExecute(Uri), EndExecute(Uri)
/// begin the execution of the request uri
/// element type of the result
/// request to execute
/// User callback when results from execution are available.
/// user state in IAsyncResult
/// async result object
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Type is used to infer result")]
public IAsyncResult BeginExecute(Uri requestUri, AsyncCallback callback, object state)
{
requestUri = Util.CreateUri(this.baseUriWithSlash, requestUri);
return (new DataServiceRequest(requestUri)).BeginExecute(this, this, callback, state);
}
///
/// Call when results from batch are desired.
///
/// element type of the result
/// async result object returned from BeginExecuteBatch
/// batch response from which query results can be enumerated.
/// asyncResult is null
/// asyncResult did not originate from this instance or End was previously called
/// problem in request or materializing results of query into objects
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Type is used to infer result")]
public IEnumerable EndExecute(IAsyncResult asyncResult)
{
QueryAsyncResult response = QueryAsyncResult.EndExecute(this, "Execute", asyncResult);
IEnumerable results = response.ServiceRequest.Materialize(this, response.ContentType, response.GetResponseStream).Cast();
return (IEnumerable)response.GetResponse(results, typeof(TElement));
}
#if !ASTORIA_LIGHT // Synchronous methods not available
///
/// Execute the requestUri
///
/// element type of the result
/// request uri to execute
/// batch response from which query results can be enumerated.
/// null requestUri
/// !BaseUri.IsBaseOf(requestUri)
/// problem materializing results of query into objects
/// failure to get response for requestUri
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Justification = "Type is used to infer result")]
public IEnumerable Execute(Uri requestUri)
{
requestUri = Util.CreateUri(this.baseUriWithSlash, requestUri);
DataServiceRequest request = DataServiceRequest.GetInstance(typeof(TElement), requestUri);
return request.Execute(this, requestUri);
}
#endif
#endregion
#region SaveChanges, BeginSaveChanges, EndSaveChanges
///
/// submit changes to the server in a single change set
///
/// callback
/// state
/// async result
public IAsyncResult BeginSaveChanges(AsyncCallback callback, object state)
{
return this.BeginSaveChanges(this.SaveChangesDefaultOptions, callback, state);
}
///
/// begin submitting changes to the server
///
/// options on how to save changes
/// The AsyncCallback delegate.
/// The state object for this request.
/// An IAsyncResult that references the asynchronous request for a response.
public IAsyncResult BeginSaveChanges(SaveChangesOptions options, AsyncCallback callback, object state)
{
ValidateSaveChangesOptions(options);
SaveAsyncResult result = new SaveAsyncResult(this, "SaveChanges", null, options, callback, state, true);
bool replaceOnUpdate = IsFlagSet(options, SaveChangesOptions.ReplaceOnUpdate);
if (IsFlagSet(options, SaveChangesOptions.Batch))
{
result.BatchBeginRequest(replaceOnUpdate);
}
else
{
result.BeginNextChange(replaceOnUpdate); // may invoke callback before returning
}
return result;
}
///
/// done submitting changes to the server
///
/// The pending request for a response.
/// changeset response
public DataServiceResponse EndSaveChanges(IAsyncResult asyncResult)
{
SaveAsyncResult result = BaseAsyncResult.EndExecute(this, "SaveChanges", asyncResult);
return result.EndRequest();
}
#if !ASTORIA_LIGHT // Synchronous methods not available
///
/// submit changes to the server in a single change set
///
/// changeset response
public DataServiceResponse SaveChanges()
{
return this.SaveChanges(this.SaveChangesDefaultOptions);
}
///
/// submit changes to the server
///
/// options on how to save changes
/// changeset response
///
/// MergeOption.NoTracking is tricky but supported because to insert a relationship we need the identity
/// of both ends and if one end was an inserted object then its identity is attached, but may not match its value
///
/// This initial implementation does not do batching.
/// Things are sent to the server in the following order
/// 1) delete relationships
/// 2) delete objects
/// 3) update objects
/// 4) insert objects
/// 5) insert relationship
///
public DataServiceResponse SaveChanges(SaveChangesOptions options)
{
DataServiceResponse errors = null;
ValidateSaveChangesOptions(options);
SaveAsyncResult result = new SaveAsyncResult(this, "SaveChanges", null, options, null, null, false);
bool replaceOnUpdate = IsFlagSet(options, SaveChangesOptions.ReplaceOnUpdate);
if (IsFlagSet(options, SaveChangesOptions.Batch))
{
result.BatchRequest(replaceOnUpdate);
}
else
{
result.BeginNextChange(replaceOnUpdate);
}
errors = result.EndRequest();
Debug.Assert(null != errors, "null errors");
return errors;
}
#endif
#endregion
#region Add, Attach, Delete, Detach, Update, TryGetEntity, TryGetUri
///
/// Notifies the context that a new link exists between the and objects
/// and that the link is represented via the source. which is a collection.
/// The context adds this link to the set of newly created links to be sent to
/// the data service on the next call to SaveChanges().
///
///
/// Links are one way relationships. If a back pointer exists (ie. two way association),
/// this method should be called a second time to notify the context object of the second link.
///
/// Source object participating in the link.
/// The name of the property on the source object which represents a link from the source to the target object.
/// The target object involved in the link which is bound to the source object also specified in this call.
/// If , or are null.
/// if link already exists
/// if source or target are detached
/// if source or target are in deleted state
/// if sourceProperty is not a collection
public void AddLink(object source, string sourceProperty, object target)
{
this.EnsureRelatable(source, sourceProperty, target, EntityStates.Added);
RelatedEnd relation = new RelatedEnd(source, sourceProperty, target);
if (this.bindings.ContainsKey(relation))
{
throw Error.InvalidOperation(Strings.Context_RelationAlreadyContained);
}
relation.State = EntityStates.Added;
this.bindings.Add(relation, relation);
this.objectToResource[source].RelatedLinkCount++;
this.IncrementChange(relation);
}
///
/// Notifies the context to start tracking the specified link between source and the specified target entity.
///
/// Source object participating in the link.
/// The name of the property on the source object which represents a link from the source to the target object.
/// The target object involved in the link which is bound to the source object also specified in this call.
/// If , or are null.
/// if binding already exists
/// if source or target are in added state
/// if source or target are in deleted state
public void AttachLink(object source, string sourceProperty, object target)
{
this.AttachLink(source, sourceProperty, target, MergeOption.NoTracking);
}
///
/// Removes the specified link from the list of links being tracked by the context.
/// Any link being tracked by the context, regardless of its current state, can be detached.
///
/// Source object participating in the link.
/// The name of the property on the source object which represents a link from the source to the target object.
/// The target object involved in the link which is bound to the source object also specified in this call.
/// If or are null.
/// if sourceProperty is empty
/// true if binding was previously being tracked, false if not
public bool DetachLink(object source, string sourceProperty, object target)
{
Util.CheckArgumentNull(source, "source");
Util.CheckArgumentNotEmpty(sourceProperty, "sourceProperty");
RelatedEnd existing;
RelatedEnd relation = new RelatedEnd(source, sourceProperty, target);
if (!this.bindings.TryGetValue(relation, out existing))
{
return false;
}
this.DetachExistingLink(existing);
return true;
}
///
/// Notifies the context that a link exists between the and object
/// and that the link is represented via the source. which is a collection.
/// The context adds this link to the set of deleted links to be sent to
/// the data service on the next call to SaveChanges().
/// If the specified link exists in the "Added" state, then the link is detached (see DetachLink method) instead.
///
/// Source object participating in the link.
/// The name of the property on the source object which represents a link from the source to the target object.
/// The target object involved in the link which is bound to the source object also specified in this call.
/// If , or are null.
/// if source or target are detached
/// if source or target are in added state
/// if sourceProperty is not a collection
public void DeleteLink(object source, string sourceProperty, object target)
{
bool delay = this.EnsureRelatable(source, sourceProperty, target, EntityStates.Deleted);
RelatedEnd existing = null;
RelatedEnd relation = new RelatedEnd(source, sourceProperty, target);
if (this.bindings.TryGetValue(relation, out existing) && (EntityStates.Added == existing.State))
{ // Added -> Detached
this.DetachExistingLink(existing);
}
else
{
if (delay)
{ // can't have non-added relationship when source or target is in added state
throw Error.InvalidOperation(Strings.Context_NoRelationWithInsertEnd);
}
if (null == existing)
{ // detached -> deleted
this.bindings.Add(relation, relation);
this.objectToResource[source].RelatedLinkCount++;
existing = relation;
}
if (EntityStates.Deleted != existing.State)
{
existing.State = EntityStates.Deleted;
// It is the users responsibility to delete the link
// before deleting the entity when required.
this.IncrementChange(existing);
}
}
}
///
/// Notifies the context that a modified link exists between the and objects
/// and that the link is represented via the source. which is a reference.
/// The context adds this link to the set of modified created links to be sent to
/// the data service on the next call to SaveChanges().
///
///
/// Links are one way relationships. If a back pointer exists (ie. two way association),
/// this method should be called a second time to notify the context object of the second link.
///
/// Source object participating in the link.
/// The name of the property on the source object which represents a link from the source to the target object.
/// The target object involved in the link which is bound to the source object also specified in this call.
/// If , or are null.
/// if link already exists
/// if source or target are detached
/// if source or target are in deleted state
/// if sourceProperty is not a reference property
public void SetLink(object source, string sourceProperty, object target)
{
this.EnsureRelatable(source, sourceProperty, target, EntityStates.Modified);
RelatedEnd relation = this.DetachReferenceLink(source, sourceProperty, target, MergeOption.NoTracking);
if (null == relation)
{
relation = new RelatedEnd(source, sourceProperty, target);
this.bindings.Add(relation, relation);
}
Debug.Assert(
0 == relation.State ||
IncludeLinkState(relation.State),
"set link entity state");
if (EntityStates.Modified != relation.State)
{
relation.State = EntityStates.Modified;
this.objectToResource[source].RelatedLinkCount++;
this.IncrementChange(relation);
}
}
#endregion
#region AddObject, AttachTo, DeleteObject, Detach, TryGetEntity, TryGetUri
///
/// Add entity into the context in the Added state for tracking.
/// It does not follow the object graph and add related objects.
///
/// EntitySet for the object to be added.
/// entity graph to add
/// if entitySetName is null
/// if entitySetName is empty
/// if entity is null
/// if entity does not have a key property
/// if entity is already being tracked by the context
///
/// Any leading or trailing forward slashes will automatically be trimmed from entitySetName.
///
public void AddObject(string entitySetName, object entity)
{
this.ValidateEntitySetName(ref entitySetName);
ValidateEntityWithKey(entity);
Uri editLink = Util.CreateUri(entitySetName, UriKind.Relative);
ResourceBox resource = new ResourceBox(NoIdentity, editLink, entity);
resource.State = EntityStates.Added;
try
{
this.objectToResource.Add(entity, resource);
}
catch (ArgumentException)
{
throw Error.InvalidOperation(Strings.Context_EntityAlreadyContained);
}
this.IncrementChange(resource);
}
///
/// Attach entity into the context in the Unchanged state for tracking.
/// It does not follow the object graph and attach related objects.
///
/// EntitySet for the object to be attached.
/// entity graph to attach
/// if entitySetName is null
/// if entitySetName is empty
/// if entity is null
/// if entity does not have a key property
/// if entity is already being tracked by the context
public void AttachTo(string entitySetName, object entity)
{
this.AttachTo(entitySetName, entity, NoETag);
}
///
/// Attach entity into the context in the Unchanged state for tracking.
/// It does not follow the object graph and attach related objects.
///
/// EntitySet for the object to be attached.
/// entity graph to attach
/// etag
/// if entitySetName is null
/// if entitySetName is empty
/// if entity is null
/// if entity does not have a key property
/// if entity is already being tracked by the context
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704", MessageId = "etag", Justification = "represents ETag in request")]
public void AttachTo(string entitySetName, object entity, string etag)
{
this.ValidateEntitySetName(ref entitySetName);
ValidateEntityWithKey(entity);
Uri editLink = GenerateEditLinkUri(this.baseUriWithSlash, entitySetName, entity);
// we fake the identity by using the generated edit link
// ReferenceIdentity is a test hook to help verify we dont' use identity instead of editLink
Uri identity = Util.ReferenceIdentity(editLink);
this.AttachTo(identity, editLink, etag, entity, true);
}
///
/// Mark an existing object being tracked by the context for deletion.
///
/// entity to be mark deleted
/// if entity is null
/// if entity is not being tracked by the context
///
/// Existings objects in the Added state become detached.
///
public void DeleteObject(object entity)
{
Util.CheckArgumentNull(entity, "entity");
ResourceBox resource = null;
if (!this.objectToResource.TryGetValue(entity, out resource))
{ // detached object
throw Error.InvalidOperation(Strings.Context_EntityNotContained);
}
EntityStates state = resource.State;
if (EntityStates.Added == state)
{ // added -> detach
if ((null != resource.Identity) &&
!this.identityToResource.Remove(resource.Identity))
{ // added objects can have identity via NoTracking
Debug.Assert(false, "didn't remove identity");
}
this.DetachRelated(resource);
resource.State = EntityStates.Detached;
bool flag = this.objectToResource.Remove(entity);
Debug.Assert(flag, "should have removed existing entity");
}
else if (EntityStates.Deleted != state)
{
Debug.Assert(
IncludeLinkState(state),
"bad state transition to deleted");
// Leave related links alone which means we can have a link in the Added
// or Modified state referencing a source/target entity in the Deleted state.
resource.State = EntityStates.Deleted;
this.IncrementChange(resource);
}
}
///
/// Detach entity from the context.
///
/// entity to detach.
/// true if object was detached
/// if entity is null
public bool Detach(object entity)
{
Util.CheckArgumentNull(entity, "entity");
ResourceBox resource = null;
if (this.objectToResource.TryGetValue(entity, out resource))
{
return this.DetachResource(resource);
}
return false;
}
///
/// Mark an existing object for update in the context.
///
/// entity to be mark for update
/// if entity is null
/// if entity is detached
public void UpdateObject(object entity)
{
Util.CheckArgumentNull(entity, "entity");
ResourceBox resource = null;
if (!this.objectToResource.TryGetValue(entity, out resource))
{
throw Error.Argument(Strings.Context_EntityNotContained, "entity");
}
if (EntityStates.Unchanged == resource.State)
{
resource.State = EntityStates.Modified;
this.IncrementChange(resource);
}
}
///
/// Find tracked entity by its identity.
///
/// entities in added state are not likely to have a identity
/// entity type
/// identity
/// entity being tracked by context
/// true if entity was found
/// identity is null
public bool TryGetEntity(Uri identity, out TEntity entity) where TEntity : class
{
entity = null;
Util.CheckArgumentNull(identity, "relativeUri");
// ReferenceIdentity is a test hook to help verify we dont' use identity instead of editLink
identity = Util.ReferenceIdentity(identity);
EntityStates state;
entity = (TEntity)this.TryGetEntity(identity, null, MergeOption.AppendOnly, out state);
return (null != entity);
}
///
/// Identity uri for tracked entity.
/// Though the identity might use a dereferencable scheme, you MUST NOT assume it can be dereferenced.
///
/// Entities in added state are not likely to have an identity.
/// entity being tracked by context
/// identity
/// true if entity is being tracked and has a identity
/// entity is null
public bool TryGetUri(object entity, out Uri identity)
{
identity = null;
Util.CheckArgumentNull(entity, "entity");
ResourceBox resource = null;
if (this.objectToResource.TryGetValue(entity, out resource) &&
(null != resource.Identity))
{
// DereferenceIdentity is a test hook to help verify we dont' use identity instead of editLink
identity = Util.DereferenceIdentity(resource.Identity);
}
return (null != identity);
}
///
/// Handle response by looking at status and possibly throwing an exception.
///
/// response status code
/// Version string on the response header; possibly null.
/// delegate to get response stream
/// throw or return on failure
/// exception on failure
internal static Exception HandleResponse(
HttpStatusCode statusCode,
string responseVersion,
Func getResponseStream,
bool throwOnFailure)
{
InvalidOperationException failure = null;
if (!CanHandleResponseVersion(responseVersion))
{
string description = Strings.Context_VersionNotSupported(
responseVersion,
XmlConstants.DataServiceClientVersionCurrentMajor,
XmlConstants.DataServiceClientVersionCurrentMinor);
failure = Error.InvalidOperation(description);
}
if (failure == null && !WebUtil.SuccessStatusCode(statusCode))
{
failure = GetResponseText(getResponseStream, statusCode);
}
if (failure != null && throwOnFailure)
{
throw failure;
}
return failure;
}
/// response materialization has an identity to attach to the inserted object
/// identity of entity
/// edit link of entity
/// inserted object
/// etag of attached object
internal void AttachIdentity(Uri identity, Uri editLink, object entity, string etag)
{ // insert->unchanged
Debug.Assert(null != identity && identity.IsAbsoluteUri, "must have identity");
this.EnsureIdentityToResource();
ResourceBox resource = this.objectToResource[entity];
Debug.Assert(EntityStates.Added == resource.State, "didn't find expected entity in added state");
if ((null != resource.Identity) &&
!this.identityToResource.Remove(resource.Identity))
{
Debug.Assert(false, "didn't remove added identity");
}
resource.ETag = etag;
resource.Identity = identity; // always attach the identity
resource.EditLink = editLink;
resource.State = EntityStates.Unchanged;
this.identityToResource.Add(identity, resource);
}
/// use location from header to generate initial edit and identity
/// entity in added state
/// location from post header
internal void AttachLocation(object entity, string location)
{
Debug.Assert(null != entity, "null != entity");
Uri editLink = new Uri(location, UriKind.Absolute);
Uri identity = Util.ReferenceIdentity(editLink);
this.EnsureIdentityToResource();
ResourceBox resource = this.objectToResource[entity];
Debug.Assert(EntityStates.Added == resource.State, "didn't find expected entity in added state");
if ((null != resource.Identity) &&
!this.identityToResource.Remove(resource.Identity))
{
Debug.Assert(false, "didn't remove added identity");
}
resource.Identity = identity; // always attach the identity
resource.EditLink = editLink;
this.identityToResource.Add(identity, resource);
}
///
/// Track a binding.
///
/// Source resource.
/// Property on the source resource that relates to the target resource.
/// Target resource.
/// merge operation
internal void AttachLink(object source, string sourceProperty, object target, MergeOption linkMerge)
{
this.EnsureRelatable(source, sourceProperty, target, EntityStates.Unchanged);
RelatedEnd existing = null;
RelatedEnd relation = new RelatedEnd(source, sourceProperty, target);
if (this.bindings.TryGetValue(relation, out existing))
{
switch (linkMerge)
{
case MergeOption.AppendOnly:
break;
case MergeOption.OverwriteChanges:
relation = existing;
break;
case MergeOption.PreserveChanges:
if ((EntityStates.Added == existing.State) ||
(EntityStates.Unchanged == existing.State) ||
(EntityStates.Modified == existing.State && null != existing.TargetResouce))
{
relation = existing;
}
break;
case MergeOption.NoTracking: // public API point should throw if link exists
throw Error.InvalidOperation(Strings.Context_RelationAlreadyContained);
}
}
else
{
bool collectionProperty = (null != ClientType.Create(source.GetType()).GetProperty(sourceProperty, false).CollectionType);
if (collectionProperty || (null == (existing = this.DetachReferenceLink(source, sourceProperty, target, linkMerge))))
{
this.bindings.Add(relation, relation);
this.objectToResource[source].RelatedLinkCount++;
this.IncrementChange(relation);
}
else if (!((MergeOption.AppendOnly == linkMerge) ||
(MergeOption.PreserveChanges == linkMerge && EntityStates.Modified == existing.State)))
{
// AppendOnly doesn't change state or target
// OverWriteChanges changes target and state
// PreserveChanges changes target if unchanged, leaves modified target and state alone
relation = existing;
}
}
relation.State = EntityStates.Unchanged;
}
///
/// Attach entity into the context in the Unchanged state.
///
/// Identity for the object to be attached.
/// EntitySet for the object to be attached.
/// etag for the entity
/// entity graph to attach
/// fail for public api else change existing relationship to unchanged
/// if entitySetName is empty
/// if entitySetName is null
/// if entity is null
/// if entity is already being tracked by the context
internal void AttachTo(Uri identity, Uri editLink, string etag, object entity, bool fail)
{
Debug.Assert((null != identity && identity.IsAbsoluteUri), "must have identity");
Debug.Assert(null != editLink, "must have editLink");
Debug.Assert(null != entity && ClientType.Create(entity.GetType()).HasKeys, "entity must have keys to attach");
this.EnsureIdentityToResource();
Debug.Assert(identity.IsAbsoluteUri, "Uri is not absolute");
ResourceBox resource;
this.objectToResource.TryGetValue(entity, out resource);
ResourceBox existing;
this.identityToResource.TryGetValue(identity, out existing);
if (fail && (null != resource))
{
throw Error.InvalidOperation(Strings.Context_EntityAlreadyContained);
}
else if (resource != existing)
{
throw Error.InvalidOperation(Strings.Context_DifferentEntityAlreadyContained);
}
else if (null == resource)
{
resource = new ResourceBox(identity, editLink, entity);
this.IncrementChange(resource);
this.objectToResource.Add(entity, resource);
this.identityToResource.Add(identity, resource);
}
resource.State = EntityStates.Unchanged;
resource.ETag = etag;
}
#endregion
///
/// create the request object
///
/// requestUri
/// updating
/// Whether the request/response should request/assume ATOM or any MIME type
/// content type for the request
/// a request ready to get a response
internal HttpWebRequest CreateRequest(Uri requestUri, string method, bool allowAnyType, string contentType)
{
Debug.Assert(null != requestUri, "request uri is null");
Debug.Assert(requestUri.IsAbsoluteUri, "request uri is not absolute uri");
Debug.Assert(UriUtil.UriInvariantInsensitiveIsBaseOf(this.baseUriWithSlash, requestUri), "context is not base of request uri");
Debug.Assert(
Object.ReferenceEquals(XmlConstants.HttpMethodDelete, method) ||
Object.ReferenceEquals(XmlConstants.HttpMethodGet, method) ||
Object.ReferenceEquals(XmlConstants.HttpMethodPost, method) ||
Object.ReferenceEquals(XmlConstants.HttpMethodPut, method) ||
Object.ReferenceEquals(XmlConstants.HttpMethodMerge, method),
"unexpected http method string reference");
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(requestUri);
#if !ASTORIA_LIGHT // Credentials not available
if (null != this.Credentials)
{
request.Credentials = this.Credentials;
}
#endif
#if !ASTORIA_LIGHT // Timeout not available
if (0 != this.timeout)
{
request.Timeout = (int)Math.Min(Int32.MaxValue, new TimeSpan(0, 0, this.timeout).TotalMilliseconds);
}
#endif
#if !ASTORIA_LIGHT // KeepAlive not available
request.KeepAlive = true;
#endif
#if !ASTORIA_LIGHT // UserAgent not available
request.UserAgent = "Microsoft ADO.NET Data Services";
#endif
if (this.UsePostTunneling &&
(!Object.ReferenceEquals(XmlConstants.HttpMethodPost, method)) &&
(!Object.ReferenceEquals(XmlConstants.HttpMethodGet, method)))
{
request.Headers[XmlConstants.HttpXMethod] = method;
request.Method = XmlConstants.HttpMethodPost;
}
else
{
request.Method = method;
}
#if !ASTORIA_LIGHT // Data.Services http stack
// Fires whenever a new HttpWebRequest has been created
// The event fires early - before the client library sets many of its required property values.
// This ensures the client library has the last say on the value of mandated properties
// such as the HTTP verb being used for the request.
if (this.SendingRequest != null)
{
SendingRequestEventArgs args = new SendingRequestEventArgs(request);
this.SendingRequest(this, args);
if (!Object.ReferenceEquals(args.Request, request))
{
request = (HttpWebRequest)args.Request;
}
}
#endif
request.Accept = allowAnyType ?
XmlConstants.MimeAny :
(XmlConstants.MimeApplicationAtom + "," + XmlConstants.MimeApplicationXml);
request.Headers[HttpRequestHeader.AcceptCharset] = XmlConstants.Utf8Encoding;
// Always sending the version along allows the server to fail before processing.
request.Headers[XmlConstants.HttpDataServiceVersion] = XmlConstants.DataServiceClientVersionCurrent;
request.Headers[XmlConstants.HttpMaxDataServiceVersion] = XmlConstants.DataServiceClientVersionCurrent;
#if !ASTORIA_LIGHT // AllowWriteStreamBuffering not available
bool allowStreamBuffering = false;
#endif
bool removeXMethod = true;
if (!Object.ReferenceEquals(XmlConstants.HttpMethodGet, method))
{
Debug.Assert(!String.IsNullOrEmpty(contentType), "Content-Type must be specified for non get operation");
request.ContentType = contentType;
if (Object.ReferenceEquals(XmlConstants.HttpMethodDelete, method))
{
request.ContentLength = 0;
}
#if !ASTORIA_LIGHT // AllowWriteStreamBuffering not available
// else
{ // always set to workaround NullReferenceException in HttpWebRequest.GetResponse when ContentLength = 0
allowStreamBuffering = true;
}
#endif
if (this.UsePostTunneling && (!Object.ReferenceEquals(XmlConstants.HttpMethodPost, method)))
{
request.Headers[XmlConstants.HttpXMethod] = method;
method = XmlConstants.HttpMethodPost;
removeXMethod = false;
}
}
else
{
Debug.Assert(contentType == null, "Content-Type for get methods should be null");
}
#if !ASTORIA_LIGHT // AllowWriteStreamBuffering not available
// When AllowWriteStreamBuffering is true, the data is buffered in memory so it is ready to be resent
// in the event of redirections or authentication requests.
request.AllowWriteStreamBuffering = allowStreamBuffering;
#endif
ICollection headers;
#if !ASTORIA_LIGHT // alternate Headers.AllKeys
headers = request.Headers.AllKeys;
#else
headers = request.Headers.Headers;
#endif
if (headers.Contains(XmlConstants.HttpRequestIfMatch))
{
#if !ASTORIA_LIGHT // alternate IfMatch header doesn't work
request.Headers.Remove(HttpRequestHeader.IfMatch);
#endif
}
if (removeXMethod && headers.Contains(XmlConstants.HttpXMethod))
{
#if !ASTORIA_LIGHT // alternate HttpXMethod header doesn't work
request.Headers.Remove(XmlConstants.HttpXMethod);
#endif
}
request.Method = method;
return request;
}
///
/// get an enumerable materializes the objects the response
///
/// http response
/// base elementType being materialized
/// delegate to create the materializer
/// an enumerable
internal MaterializeAtom GetMaterializer(QueryAsyncResult response, Type elementType, Func create)
{
if (HttpStatusCode.NoContent == response.StatusCode)
{ // object was deleted
return null;
}
if (HttpStatusCode.Created == response.StatusCode &&
response.ContentLength == 0)
{
// created but no response back
return null;
}
if (null != response)
{
return (MaterializeAtom)this.GetMaterializer(elementType, response.ContentType, response.GetResponseStream, create);
}
return null;
}
///
/// get an enumerable materializes the objects the response
///
/// elementType
/// contentType
/// method to get http response stream
/// method to create a materializer
/// an enumerable
internal object GetMaterializer(
Type elementType,
string contentType,
Func response,
Func create)
{
Debug.Assert(null != create, "null create");
string mime = null;
Encoding encoding = null;
if (!String.IsNullOrEmpty(contentType))
{
HttpProcessUtility.ReadContentType(contentType, out mime, out encoding);
}
if (String.Equals(mime, XmlConstants.MimeApplicationAtom, StringComparison.OrdinalIgnoreCase) ||
String.Equals(mime, XmlConstants.MimeApplicationXml, StringComparison.OrdinalIgnoreCase))
{
System.IO.Stream rstream = response();
if (null != rstream)
{
XmlReader reader = XmlUtil.CreateXmlReader(rstream, encoding);
return create(this, reader, elementType);
}
return null;
}
throw Error.InvalidOperation(Strings.Deserialize_UnknownMimeTypeSpecified(mime));
}
///
/// Find tracked entity by its resourceUri and update its etag.
///
/// resource id
/// updated etag
/// merge option
/// state of entity
/// entity if found else null
internal object TryGetEntity(Uri resourceUri, string etag, MergeOption merger, out EntityStates state)
{
Debug.Assert(null != resourceUri, "null uri");
state = EntityStates.Detached;
ResourceBox resource = null;
if ((null != this.identityToResource) &&
this.identityToResource.TryGetValue(resourceUri, out resource))
{
state = resource.State;
if ((null != etag) && (MergeOption.AppendOnly != merger))
{ // don't update the etag if AppendOnly
resource.ETag = etag;
}
Debug.Assert(null != resource.Resource, "null entity");
return resource.Resource;
}
return null;
}
///
/// get the resource box for an entity
///
/// entity
/// resource box
internal ResourceBox GetEntity(object source)
{
return this.objectToResource[source];
}
///
/// get the related links ignoring target entity
///
/// source entity
/// source entity's property
/// enumerable of related ends
internal IEnumerable GetLinks(object source, string sourceProperty)
{
return this.bindings.Values.Where(o => (o.SourceResource == source) && (o.SourceProperty == sourceProperty));
}
///
/// user hook to resolve name into a type
///
/// name to resolve
/// base type associated with name
/// null to skip node
/// if ResolveType function returns a type not assignable to the userType
internal Type ResolveTypeFromName(string wireName, Type userType)
{
Debug.Assert(null != userType, "null != baseType");
if (String.IsNullOrEmpty(wireName))
{
return userType;
}
Type payloadType;
if (!ClientConvert.ToNamedType(wireName, out payloadType))
{
payloadType = null;
Func resolve = this.ResolveType;
if (null != resolve)
{
// if the ResolveType property is set, call the provided type resultion method
payloadType = resolve(wireName);
}
if (null == payloadType)
{
// if the type resolution method returns null or the ResolveType property was not set
#if !ASTORIA_LIGHT
payloadType = ClientType.ResolveFromName(wireName, userType);
#else
payloadType = ClientType.ResolveFromName(wireName, userType, this.GetType());
#endif
}
if ((null != payloadType) && (!userType.IsAssignableFrom(payloadType)))
{
// throw an exception if the type from the resolver is not assignable to the expected type
throw Error.InvalidOperation(Strings.Deserialize_Current(userType, payloadType));
}
}
return payloadType ?? userType;
}
///
/// The reverse of ResolveType
///
/// client type
/// type for the server
internal string ResolveNameFromType(Type type)
{
Debug.Assert(null != type, "null type");
Func resolve = this.ResolveName;
return ((null != resolve) ? resolve(type) : (String)null);
}
///
/// Fires the ReadingEntity event
///
/// Entity being (de)serialized
/// XML data of the ATOM entry
internal void FireReadingEntityEvent(object entity, XElement data)
{
Debug.Assert(entity != null, "entity != null");
Debug.Assert(data != null, "data != null");
ReadingWritingEntityEventArgs args = new ReadingWritingEntityEventArgs(entity, data);
this.ReadingEntity(this, args);
}
#region Ensure
/// Filter to verify states
/// x
/// true if added/updated/deleted
private static bool HasModifiedResourceState(Entry x)
{
Debug.Assert(
(EntityStates.Added == x.State) ||
(EntityStates.Modified == x.State) ||
(EntityStates.Unchanged == x.State) ||
(EntityStates.Deleted == x.State),
"entity state is not valid");
return (EntityStates.Unchanged != x.State);
}
/// modified or unchanged
/// state to test
/// true if modified or unchanged
private static bool IncludeLinkState(EntityStates x)
{
return ((EntityStates.Modified == x) || (EntityStates.Unchanged == x));
}
#endregion
/// Checks whether an ADO.NET Data Service version string can be handled.
/// Version string on the response header; possibly null.
/// true if the version can be handled; false otherwise.
private static bool CanHandleResponseVersion(string responseVersion)
{
if (!String.IsNullOrEmpty(responseVersion))
{
KeyValuePair version;
if (!HttpProcessUtility.TryReadVersion(responseVersion, out version))
{
return false;
}
// For the time being, we only handle 1.0 responses.
if (version.Key.Major != XmlConstants.DataServiceClientVersionCurrentMajor ||
version.Key.Minor != XmlConstants.DataServiceClientVersionCurrentMinor)
{
return false;
}
}
return true;
}
/// generate a Uri based on key properties of the entity
/// baseUri
/// entitySetName
/// entity
/// absolute uri
private static Uri GenerateEditLinkUri(Uri baseUriWithSlash, string entitySetName, object entity)
{
Debug.Assert(null != baseUriWithSlash && baseUriWithSlash.IsAbsoluteUri && baseUriWithSlash.OriginalString.EndsWith("/", StringComparison.Ordinal), "baseUriWithSlash");
Debug.Assert(!String.IsNullOrEmpty(entitySetName) && !entitySetName.StartsWith("/", StringComparison.Ordinal), "entitySetName");
Debug.Assert(null != entity, "entity");
StringBuilder builder = new StringBuilder();
builder.Append(baseUriWithSlash.AbsoluteUri);
builder.Append(entitySetName);
builder.Append("(");
string prefix = String.Empty;
ClientType clientType = ClientType.Create(entity.GetType());
Debug.Assert(clientType.HasKeys, "requires keys");
ClientType.ClientProperty[] keys = clientType.Properties.Where(ClientType.ClientProperty.GetKeyProperty).ToArray();
foreach (ClientType.ClientProperty property in keys)
{
#if ASTORIA_OPEN_OBJECT
Debug.Assert(!property.OpenObjectProperty, "key property values can't be OpenProperties");
#endif
builder.Append(prefix);
if (1 < keys.Length)
{
builder.Append(property.PropertyName).Append("=");
}
object value = property.GetValue(entity);
if (null == value)
{
throw Error.InvalidOperation(Strings.Serializer_NullKeysAreNotSupported(property.PropertyName));
}
string converted;
if (!ClientConvert.TryKeyPrimitiveToString(value, out converted))
{
throw Error.InvalidOperation(Strings.Deserialize_Current(typeof(string), value.GetType()));
}
builder.Append(converted);
prefix = ",";
}
builder.Append(")");
return Util.CreateUri(builder.ToString(), UriKind.Absolute);
}
/// Get http method string from entity resource state
/// resource state
/// whether we need to update MERGE or PUT method for update.
/// http method string delete, put or post
private static string GetEntityHttpMethod(EntityStates state, bool replaceOnUpdate)
{
switch (state)
{
case EntityStates.Deleted:
return XmlConstants.HttpMethodDelete;
case EntityStates.Modified:
if (replaceOnUpdate)
{
return XmlConstants.HttpMethodPut;
}
else
{
return XmlConstants.HttpMethodMerge;
}
case EntityStates.Added:
return XmlConstants.HttpMethodPost;
default:
throw Error.InternalError(InternalError.UnvalidatedEntityState);
}
}
/// Get http method string from link resource state
/// resource
/// http method string put or post
private static string GetLinkHttpMethod(RelatedEnd link)
{
bool collection = (null != ClientType.Create(link.SourceResource.GetType()).GetProperty(link.SourceProperty, false).CollectionType);
if (!collection)
{
Debug.Assert(EntityStates.Modified == link.State, "not Modified state");
if (null == link.TargetResouce)
{ // REMOVE/DELETE a reference
return XmlConstants.HttpMethodDelete;
}
else
{ // UPDATE/PUT a reference
return XmlConstants.HttpMethodPut;
}
}
else if (EntityStates.Deleted == link.State)
{ // you call DELETE on $links
return XmlConstants.HttpMethodDelete;
}
else
{ // you INSERT/POST into a collection
Debug.Assert(EntityStates.Added == link.State, "not Added state");
return XmlConstants.HttpMethodPost;
}
}
///
/// get the response text into a string
///
/// method to get response stream
/// status code
/// text
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031", Justification = "Cache exception so user can examine it later")]
private static DataServiceClientException GetResponseText(Func getResponseStream, HttpStatusCode statusCode)
{
string message = null;
using (System.IO.Stream stream = getResponseStream())
{
if ((null != stream) && stream.CanRead)
{
message = new StreamReader(stream).ReadToEnd();
}
}
if (String.IsNullOrEmpty(message))
{
message = statusCode.ToString();
}
return new DataServiceClientException(message, (int)statusCode);
}
/// Handle changeset response.
/// headers of changeset response
private static void HandleResponsePost(RelatedEnd entry)
{
if (!((EntityStates.Added == entry.State) || (EntityStates.Modified == entry.State && null != entry.TargetResouce)))
{
Error.ThrowBatchUnexpectedContent(InternalError.LinkNotAddedState);
}
entry.State = EntityStates.Unchanged;
}
/// Handle changeset response.
/// updated entity or link
/// updated etag
private static void HandleResponsePut(Entry entry, string etag)
{
if (entry.IsResource)
{
if (EntityStates.Modified != entry.State)
{
Error.ThrowBatchUnexpectedContent(InternalError.EntryNotModified);
}
entry.State = EntityStates.Unchanged;
((ResourceBox)entry).ETag = etag;
}
else
{
RelatedEnd link = (RelatedEnd)entry;
if ((EntityStates.Added == entry.State) || (EntityStates.Modified == entry.State))
{
link.State = EntityStates.Unchanged;
}
else if (EntityStates.Detached != entry.State)
{ // this link may have been previously detached by a detaching entity
Error.ThrowBatchUnexpectedContent(InternalError.LinkBadState);
}
}
}
///
/// write out an individual property value which can be a primitive or link
///
/// writer
/// namespaceName in which we need to write the property element.
/// property which contains name, type, is key (if false and null value, will throw)
/// property value
private static void WriteContentProperty(XmlWriter writer, string namespaceName, ClientType.ClientProperty property, object propertyValue)
{
writer.WriteStartElement(property.PropertyName, namespaceName);
string typename = ClientConvert.GetEdmType(property.PropertyType);
if (null != typename)
{
writer.WriteAttributeString(XmlConstants.AtomTypeAttributeName, XmlConstants.DataWebMetadataNamespace, typename);
}
if (null == propertyValue)
{ //
writer.WriteAttributeString(XmlConstants.AtomNullAttributeName, XmlConstants.DataWebMetadataNamespace, XmlConstants.XmlTrueLiteral);
if (property.KeyProperty)
{
throw Error.InvalidOperation(Strings.Serializer_NullKeysAreNotSupported(property.PropertyName));
}
}
else
{
string convertedValue = ClientConvert.ToString(propertyValue);
if (0 == convertedValue.Length)
{ //
writer.WriteAttributeString(XmlConstants.AtomNullAttributeName, XmlConstants.DataWebMetadataNamespace, XmlConstants.XmlFalseLiteral);
}
else
{ // value
if (Char.IsWhiteSpace(convertedValue[0]) ||
Char.IsWhiteSpace(convertedValue[convertedValue.Length - 1]))
{ // xml:space="preserve"
writer.WriteAttributeString(XmlConstants.XmlSpaceAttributeName, XmlConstants.XmlNamespacesNamespace, XmlConstants.XmlSpacePreserveValue);
}
writer.WriteValue(convertedValue);
}
}
writer.WriteEndElement();
}
/// validate
/// entity to validate
/// if entity was null
/// if entity does not have a key property
private static void ValidateEntityWithKey(object entity)
{
Util.CheckArgumentNull(entity, "entity");
if (!ClientType.Create(entity.GetType()).HasKeys)
{
throw Error.Argument(Strings.Content_EntityWithoutKey, "entity");
}
}
///
/// Validate the SaveChanges Option
///
/// options as specified by the user.
private static void ValidateSaveChangesOptions(SaveChangesOptions options)
{
const SaveChangesOptions All =
SaveChangesOptions.ContinueOnError |
SaveChangesOptions.Batch |
SaveChangesOptions.ReplaceOnUpdate;
// Make sure no higher order bits are set.
if ((options | All) != All)
{
throw Error.ArgumentOutOfRange("options");
}
// Both batch and continueOnError can't be set together
if (IsFlagSet(options, SaveChangesOptions.Batch | SaveChangesOptions.ContinueOnError))
{
throw Error.ArgumentOutOfRange("options");
}
}
///
/// checks whether the given flag is set on the options
///
/// options as specified by the user.
/// whether the given flag is set on the options
/// true if the given flag is set, otherwise false.
private static bool IsFlagSet(SaveChangesOptions options, SaveChangesOptions flag)
{
return ((options & flag) == flag);
}
///
/// Write the batch headers along with the first http header for the batch operation.
///
/// Stream writer which writes to the underlying stream.
/// HTTP method name for the operation.
/// uri for the operation.
private static void WriteOperationRequestHeaders(StreamWriter writer, string methodName, string uri)
{
writer.WriteLine("{0}: {1}", XmlConstants.HttpContentType, XmlConstants.MimeApplicationHttp);
writer.WriteLine("{0}: {1}", XmlConstants.HttpContentTransferEncoding, XmlConstants.BatchRequestContentTransferEncoding);
writer.WriteLine();
writer.WriteLine("{0} {1} {2}", methodName, uri, XmlConstants.HttpVersionInBatching);
}
///
/// Write the batch headers along with the first http header for the batch operation.
///
/// Stream writer which writes to the underlying stream.
/// status code for the response.
private static void WriteOperationResponseHeaders(StreamWriter writer, int statusCode)
{
writer.WriteLine("{0}: {1}", XmlConstants.HttpContentType, XmlConstants.MimeApplicationHttp);
writer.WriteLine("{0}: {1}", XmlConstants.HttpContentTransferEncoding, XmlConstants.BatchRequestContentTransferEncoding);
writer.WriteLine();
writer.WriteLine("{0} {1} {2}", XmlConstants.HttpVersionInBatching, statusCode, (HttpStatusCode)statusCode);
}
///
/// Check to see if the resource to be inserted is a media entry, and if so
/// setup a POST request for the media content first and turn the rest of
/// the operation into a PUT to update the rest of the properties.
///
/// The resource to check/process
/// A web request setup to do POST the media resource
private HttpWebRequest CheckAndProcessMediaEntry(ResourceBox box)
{
//
ClientType type = ClientType.Create(box.Resource.GetType());
if (type.MediaDataMember == null)
{
// this is not a media link entry, process normally
return null;
}
HttpWebRequest mediaRequest = this.CreateRequest(box.GetResourceUri(this.baseUriWithSlash), XmlConstants.HttpMethodPost, true, XmlConstants.MimeApplicationAtom);
if (type.MediaDataMember.MimeTypeProperty == null)
{
mediaRequest.ContentType = XmlConstants.MimeApplicationOctetStream;
}
else
{
string mimeType = type.MediaDataMember.MimeTypeProperty.GetValue(box.Resource).ToString();
if (string.IsNullOrEmpty(mimeType))
{
throw Error.InvalidOperation(
Strings.Context_NoContentTypeForMediaLink(
type.ElementTypeName,
type.MediaDataMember.MimeTypeProperty.PropertyName));
}
mediaRequest.ContentType = mimeType;
}
object value = type.MediaDataMember.GetValue(box.Resource);
if (value == null)
{
mediaRequest.ContentLength = 0;
}
else
{
byte[] buffer = value as byte[];
if (buffer == null)
{
string mime;
Encoding encoding;
HttpProcessUtility.ReadContentType(mediaRequest.ContentType, out mime, out encoding);
if (encoding == null)
{
encoding = Encoding.UTF8;
mediaRequest.ContentType += XmlConstants.MimeTypeUtf8Encoding;
}
buffer = encoding.GetBytes(ClientConvert.ToString(value));
}
mediaRequest.ContentLength = buffer.Length;
using (Stream s = mediaRequest.GetRequestStream())
{
s.Write(buffer, 0, buffer.Length);
}
}
// Convert the insert into an update for the media link entry we just created
// (note that the identity still needs to be fixed up on the resbox once
// the response comes with the 'location' header; that happens during processing
// of the response in SavedResource())
box.State = EntityStates.Modified;
return mediaRequest;
}
/// the work to detach a resource
/// resource to detach
/// true if detached
private bool DetachResource(ResourceBox resource)
{
this.DetachRelated(resource);
resource.ChangeOrder = UInt32.MaxValue;
resource.State = EntityStates.Detached;
bool flag = this.objectToResource.Remove(resource.Resource);
Debug.Assert(flag, "should have removed existing entity");
if (null != resource.Identity)
{
flag = this.identityToResource.Remove(resource.Identity);
Debug.Assert(flag, "should have removed existing identity");
}
return true;
}
///
/// write out binding payload using POST with http method override for PUT
///
/// binding
/// for non-batching its a request object ready to get a response from else null when batching
private HttpWebRequest CreateRequest(RelatedEnd binding)
{
Debug.Assert(null != binding, "null binding");
if (binding.ContentGeneratedForSave)
{
return null;
}
ResourceBox sourceResource = this.objectToResource[binding.SourceResource];
ResourceBox targetResource = (null != binding.TargetResouce) ? this.objectToResource[binding.TargetResouce] : null;
// these failures should only with SaveChangesOptions.ContinueOnError
if (null == sourceResource.Identity)
{
Debug.Assert(!binding.ContentGeneratedForSave, "already saved link");
binding.ContentGeneratedForSave = true;
Debug.Assert(EntityStates.Added == sourceResource.State, "expected added state");
throw Error.InvalidOperation(Strings.Context_LinkResourceInsertFailure, sourceResource.SaveError);
}
else if ((null != targetResource) && (null == targetResource.Identity))
{
Debug.Assert(!binding.ContentGeneratedForSave, "already saved link");
binding.ContentGeneratedForSave = true;
Debug.Assert(EntityStates.Added == targetResource.State, "expected added state");
throw Error.InvalidOperation(Strings.Context_LinkResourceInsertFailure, targetResource.SaveError);
}
Debug.Assert(null != sourceResource.Identity, "missing sourceResource.Identity");
return this.CreateRequest(this.CreateRequestUri(sourceResource, binding), GetLinkHttpMethod(binding), false, XmlConstants.MimeApplicationXml);
}
/// create the uri for a link
/// edit link of source
/// link
/// appropriate uri for link state
private Uri CreateRequestUri(ResourceBox sourceResource, RelatedEnd binding)
{
Uri requestUri = Util.CreateUri(sourceResource.GetResourceUri(this.baseUriWithSlash), this.CreateRequestRelativeUri(binding));
return requestUri;
}
///
/// create the uri for the link relative to its source entity
///
/// link
/// uri
private Uri CreateRequestRelativeUri(RelatedEnd binding)
{
Uri relative;
bool collection = (null != ClientType.Create(binding.SourceResource.GetType()).GetProperty(binding.SourceProperty, false).CollectionType);
if (collection && (EntityStates.Added != binding.State))
{ // you DELETE(PUT NULL) from a collection
Debug.Assert(null != binding.TargetResouce, "null target in collection");
ResourceBox targetResource = this.objectToResource[binding.TargetResouce];
// For collections, we need to generate the uri with the property name followed by the keys.
// GenerateEditLinkUri generates an absolute uri
// First parameters is the base service uri
// Second parameter is the segment name (in this case, navigation property name)
// Third parameter is the resource whose key values need to be appended after the segment
// For e.g. If the navigation property name is "Purchases" and the resource type is Order with key '1', then this method will generate 'baseuri/Purchases(1)'
Uri navigationPropertyUri = this.BaseUriWithSlash.MakeRelativeUri(DataServiceContext.GenerateEditLinkUri(this.BaseUriWithSlash, binding.SourceProperty, targetResource.Resource));
// Get the relative uri and appends links segment at the start.
relative = Util.CreateUri(XmlConstants.UriLinkSegment + "/" + navigationPropertyUri.OriginalString, UriKind.Relative);
}
else
{ // UPDATE(PUT ID) a reference && INSERT(POST ID) into a collection
relative = Util.CreateUri(XmlConstants.UriLinkSegment + "/" + binding.SourceProperty, UriKind.Relative);
}
Debug.Assert(!relative.IsAbsoluteUri, "should be relative uri");
return relative;
}
///
/// write content to batch text stream
///
/// link
/// batch text stream
private void CreateRequestBatch(RelatedEnd binding, StreamWriter text)
{
Uri relative = this.CreateRequestRelativeUri(binding);
ResourceBox sourceResource = this.objectToResource[binding.SourceResource];
string requestString;
if (null != sourceResource.Identity)
{
requestString = this.CreateRequestUri(sourceResource, binding).AbsoluteUri;
}
else
{
requestString = "$" + sourceResource.ChangeOrder.ToString(CultureInfo.InvariantCulture) + "/" + relative.OriginalString;
}
WriteOperationRequestHeaders(text, GetLinkHttpMethod(binding), requestString);
text.WriteLine("{0}: {1}", XmlConstants.HttpDataServiceVersion, XmlConstants.DataServiceClientVersionCurrent);
text.WriteLine("{0}: {1}", XmlConstants.HttpContentID, binding.ChangeOrder);
// if (EntityStates.Deleted || (EntityState.Modifed && null == TargetResource))
// then the server will fail the batch section if content type exists
if ((EntityStates.Added == binding.State) || (EntityStates.Modified == binding.State && (null != binding.TargetResouce)))
{
text.WriteLine("{0}: {1}", XmlConstants.HttpContentType, XmlConstants.MimeApplicationXml);
}
}
///
/// create content memory stream for link
///
/// link
/// should newline be written
/// memory stream
private MemoryStream CreateRequestData(RelatedEnd binding, bool newline)
{
Debug.Assert(
(binding.State == EntityStates.Added) ||
(binding.State == EntityStates.Modified && null != binding.TargetResouce),
"This method must be called only when a binding is added or put");
MemoryStream stream = new MemoryStream();
XmlWriter writer = XmlUtil.CreateXmlWriterAndWriteProcessingInstruction(stream, HttpProcessUtility.EncodingUtf8NoPreamble);
ResourceBox targetResource = this.objectToResource[binding.TargetResouce];
#region
writer.WriteStartElement(XmlConstants.UriElementName, XmlConstants.DataWebMetadataNamespace);
string id;
if (null != targetResource.Identity)
{
id = Util.DereferenceIdentity(targetResource.Identity).AbsoluteUri;
}
else
{
id = "$" + targetResource.ChangeOrder.ToString(CultureInfo.InvariantCulture);
}
writer.WriteValue(id);
writer.WriteEndElement(); //
#endregion
writer.Flush();
if (newline)
{
// end the xml content stream with a newline
stream.WriteByte((byte)'\r');
stream.WriteByte((byte)'\n');
}
// strip the preamble.
stream.Position = 0;
return stream;
}
///
/// Create HttpWebRequest from a resource
///
/// resource
/// resource state
/// whether we need to update MERGE or PUT method for update.
/// web request
private HttpWebRequest CreateRequest(ResourceBox box, EntityStates state, bool replaceOnUpdate)
{
Debug.Assert(null != box && ((EntityStates.Added == state) || (EntityStates.Modified == state) || (EntityStates.Deleted == state)), "unexpected entity ResourceState");
string httpMethod = GetEntityHttpMethod(state, replaceOnUpdate);
Uri requestUri = box.GetResourceUri(this.baseUriWithSlash);
HttpWebRequest request = this.CreateRequest(requestUri, httpMethod, false, XmlConstants.MimeApplicationAtom);
if ((null != box.ETag) && ((EntityStates.Deleted == state) || (EntityStates.Modified == state)))
{
#if !ASTORIA_LIGHT // different way to write Request headers
request.Headers.Set(HttpRequestHeader.IfMatch, box.ETag);
#else
request.Headers[XmlConstants.HttpRequestIfMatch] = box.ETag;
#endif
}
return request;
}
///
/// generate batch request for entity
///
/// entity
/// batch stream to write to
/// whether we need to update MERGE or PUT method for update.
private void CreateRequestBatch(ResourceBox box, StreamWriter text, bool replaceOnUpdate)
{
Debug.Assert(null != box, "null box");
Debug.Assert(null != text, "null text");
Uri requestUri = box.GetResourceUri(this.baseUriWithSlash);
Debug.Assert(null != requestUri, "request uri is null");
Debug.Assert(requestUri.IsAbsoluteUri, "request uri is not absolute uri");
Debug.Assert(UriUtil.UriInvariantInsensitiveIsBaseOf(this.baseUriWithSlash, requestUri), "context is not base of request uri");
WriteOperationRequestHeaders(text, GetEntityHttpMethod(box.State, replaceOnUpdate), requestUri.AbsoluteUri);
text.WriteLine("{0}: {1}", XmlConstants.HttpContentID, box.ChangeOrder);
if (EntityStates.Deleted != box.State)
{
text.WriteLine("{0}: {1};{2}", XmlConstants.HttpContentType, XmlConstants.MimeApplicationAtom, XmlConstants.MimeTypeEntry);
}
if ((null != box.ETag) && (EntityStates.Deleted == box.State || EntityStates.Modified == box.State))
{
text.WriteLine("{0}: {1}", XmlConstants.HttpRequestIfMatch, box.ETag);
}
}
///
/// create memory stream with entity data
///
/// entity
/// should newline be written
/// memory stream containing data
private MemoryStream CreateRequestData(ResourceBox box, bool newline)
{
Debug.Assert(null != box, "null box");
MemoryStream stream = null;
switch (box.State)
{
case EntityStates.Deleted:
break;
case EntityStates.Modified:
case EntityStates.Added:
stream = new MemoryStream();
break;
default:
Error.ThrowInternalError(InternalError.UnvalidatedEntityState);
break;
}
if (null != stream)
{
XmlWriter writer;
XDocument node = null;
if (this.WritingEntity != null)
{
// if we have to fire the WritingEntity event, buffer the content
// in an XElement so we can present the handler with the data
node = new XDocument();
writer = node.CreateWriter();
}
else
{
writer = XmlUtil.CreateXmlWriterAndWriteProcessingInstruction(stream, HttpProcessUtility.EncodingUtf8NoPreamble);
}
ClientType type = ClientType.Create(box.Resource.GetType());
string typeName = this.ResolveNameFromType(type.ElementType);
#region
writer.WriteStartElement(XmlConstants.AtomEntryElementName, XmlConstants.AtomNamespace);
writer.WriteAttributeString(XmlConstants.DataWebNamespacePrefix, XmlConstants.XmlNamespacesNamespace, this.DataNamespace);
writer.WriteAttributeString(XmlConstants.DataWebMetadataNamespacePrefix, XmlConstants.XmlNamespacesNamespace, XmlConstants.DataWebMetadataNamespace);
//
if (!String.IsNullOrEmpty(typeName))
{
writer.WriteStartElement(XmlConstants.AtomCategoryElementName, XmlConstants.AtomNamespace);
writer.WriteAttributeString(XmlConstants.AtomCategorySchemeAttributeName, this.typeScheme.OriginalString);
writer.WriteAttributeString(XmlConstants.AtomCategoryTermAttributeName, typeName);
writer.WriteEndElement();
}
//
// 2008-05-05T21:44:55Z
//
writer.WriteElementString(XmlConstants.AtomTitleElementName, XmlConstants.AtomNamespace, String.Empty);
writer.WriteElementString(XmlConstants.AtomUpdatedElementName, XmlConstants.AtomNamespace, XmlConvert.ToString(DateTime.UtcNow, XmlDateTimeSerializationMode.RoundtripKind));
writer.WriteStartElement(XmlConstants.AtomAuthorElementName, XmlConstants.AtomNamespace);
writer.WriteElementString(XmlConstants.AtomNameElementName, XmlConstants.AtomNamespace, String.Empty);
writer.WriteEndElement();
if (EntityStates.Modified == box.State)
{
// http://host/service/entityset(key)
writer.WriteElementString(XmlConstants.AtomIdElementName, Util.DereferenceIdentity(box.Identity).AbsoluteUri);
}
else
{
writer.WriteElementString(XmlConstants.AtomIdElementName, XmlConstants.AtomNamespace, String.Empty);
}
#region
if (EntityStates.Added == box.State)
{
this.CreateRequestDataLinks(box, writer);
}
#endregion
#region or
if (type.MediaDataMember == null)
{
writer.WriteStartElement(XmlConstants.AtomContentElementName, XmlConstants.AtomNamespace);
writer.WriteAttributeString(XmlConstants.AtomTypeAttributeName, XmlConstants.MimeApplicationXml); // empty namespace
}
writer.WriteStartElement(XmlConstants.AtomPropertiesElementName, XmlConstants.DataWebMetadataNamespace);
this.WriteContentProperties(writer, type, box.Resource);
writer.WriteEndElement(); //
if (type.MediaDataMember == null)
{
writer.WriteEndElement(); //
}
writer.WriteEndElement(); //
writer.Flush();
writer.Close();
#endregion
#endregion
if (this.WritingEntity != null)
{
ReadingWritingEntityEventArgs args = new ReadingWritingEntityEventArgs(box.Resource, node.Root);
this.WritingEntity(this, args);
// copy the buffered XDocument to the memory stream. no easy way of avoiding
// the copy given that we need to know the length before scanning the stream
node.Save(new StreamWriter(stream)); // defaults to UTF8 w/o preamble & Save will Flush
}
if (newline)
{
// end the xml content stream with a newline
stream.WriteByte((byte)'\r');
stream.WriteByte((byte)'\n');
}
stream.Position = 0;
}
return stream;
}
///
/// add the related links for new entites to non-new entites
///
/// entity in added state
/// writer to add links to
private void CreateRequestDataLinks(ResourceBox box, XmlWriter writer)
{
Debug.Assert(EntityStates.Added == box.State, "entity not added state");
ClientType clientType = null;
foreach (RelatedEnd end in this.RelatedLinks(box))
{
Debug.Assert(!end.ContentGeneratedForSave, "already saved link");
end.ContentGeneratedForSave = true;
if (null == clientType)
{
clientType = ClientType.Create(box.Resource.GetType());
}
string typeAttributeValue;
if (null != clientType.GetProperty(end.SourceProperty, false).CollectionType)
{
typeAttributeValue = XmlConstants.MimeApplicationAtom + ";" + XmlConstants.MimeTypeFeed;
}
else
{
typeAttributeValue = XmlConstants.MimeApplicationAtom + ";" + XmlConstants.MimeTypeEntry;
}
Debug.Assert(null != end.TargetResouce, "null is DELETE");
Uri targetIdentity = Util.DereferenceIdentity(this.objectToResource[end.TargetResouce].Identity);
writer.WriteStartElement(XmlConstants.AtomLinkElementName, XmlConstants.AtomNamespace);
writer.WriteAttributeString(XmlConstants.AtomHRefAttributeName, targetIdentity.ToString());
writer.WriteAttributeString(XmlConstants.AtomLinkRelationAttributeName, XmlConstants.DataWebRelatedNamespace + end.SourceProperty);
writer.WriteAttributeString(XmlConstants.AtomTypeAttributeName, typeAttributeValue);
writer.WriteEndElement();
}
}
/// Handle response to deleted entity.
/// deleted entity
private void HandleResponseDelete(Entry entry)
{
if (EntityStates.Deleted != entry.State)
{
Error.ThrowBatchUnexpectedContent(InternalError.EntityNotDeleted);
}
if (entry.IsResource)
{
ResourceBox resource = (ResourceBox)entry;
this.DetachResource(resource);
}
else
{
this.DetachExistingLink((RelatedEnd)entry);
}
}
/// Handle changeset response.
/// headers of changeset response
/// changeset response stream
/// editLink of the newly created item (non-null if materialize is null)
/// ETag header value from the server response (or null if no etag or if there is an actual response)
private void HandleResponsePost(ResourceBox entry, MaterializeAtom materializer, Uri editLink, string etag)
{
Debug.Assert((materializer != null) || (editLink != null), "must have either materializer or editLink");
if (EntityStates.Added != entry.State)
{
Error.ThrowBatchUnexpectedContent(InternalError.EntityNotAddedState);
}
ResourceBox box = (ResourceBox)entry;
if (materializer == null)
{
Uri identity = Util.ReferenceIdentity(editLink);
this.AttachIdentity(identity, editLink, entry.Resource, etag);
}
else
{
materializer.SetInsertingObject(box.Resource);
foreach (object x in materializer)
{
Debug.Assert(null != box.Identity, "updated inserted should always gain an identity");
Debug.Assert(x == box.Resource, "x == box.Resource, should have same object generated by response");
Debug.Assert(EntityStates.Unchanged == box.State, "should have moved out of insert");
Debug.Assert((null != this.identityToResource) && this.identityToResource.ContainsKey(box.Identity), "should have identity tracked");
}
}
foreach (RelatedEnd end in this.RelatedLinks(box))
{
Debug.Assert(0 != end.SaveResultWasProcessed, "link should have been saved with the enty");
if (IncludeLinkState(end.SaveResultWasProcessed))
{
HandleResponsePost(end);
}
}
}
/// flag results as being processed
/// result entry being processed
/// count of related links that were also processed
private int SaveResultProcessed(Entry entry)
{
Debug.Assert(0 == entry.SaveResultWasProcessed, "this entity/link already had a result");
entry.SaveResultWasProcessed = entry.State;
int count = 0;
if (entry.IsResource && (EntityStates.Added == entry.State))
{
foreach (RelatedEnd end in this.RelatedLinks((ResourceBox)entry))
{
Debug.Assert(end.ContentGeneratedForSave, "link should have been saved with the enty");
if (end.ContentGeneratedForSave)
{
Debug.Assert(0 == end.SaveResultWasProcessed, "this link already had a result");
end.SaveResultWasProcessed = end.State;
count++;
}
}
}
return count;
}
///
/// enumerate the related Modified/Unchanged links for an added item
///
/// entity
/// related links
///
/// During a non-batch SaveChanges, an Added entity can become an Unchanged entity
/// and should be included in the set of related links for the second Added entity.
///
private IEnumerable RelatedLinks(ResourceBox box)
{
int related = box.RelatedLinkCount;
if (0 < related)
{
foreach (RelatedEnd end in this.bindings.Values)
{
if (end.SourceResource == box.Resource)
{
if (null != end.TargetResouce)
{ // null TargetResource is equivalent to Deleted
ResourceBox target = this.objectToResource[end.TargetResouce];
// assumption: the source entity started in the Added state
// note: SaveChanges operates with two passes
// a) first send the request and then attach identity and append the result into a batch response (Example: BeginSaveChanges)
// b) process the batch response (shared code with SaveChanges(Batch)) (Example: EndSaveChanges)
// note: SaveResultWasProcessed is set when to the pre-save state when the save result is sucessfully processed
// scenario #1 when target entity started in modified or unchanged state
// 1) the link target entity was modified and now implicitly assumed to be unchanged (this is true in second pass)
// 2) or link target entity has not been saved is in the modified or unchanged state (this is true in first pass)
// scenario #2 when target entity started in added state
// 1) target entity has an identity (true in first pass for non-batch)
// 2) target entity is processed before source to qualify (1) better during the second pass
// 3) the link target has not been saved and is in the added state
// 4) or the link target has been saved and was in the added state
if (IncludeLinkState(target.SaveResultWasProcessed) || ((0 == target.SaveResultWasProcessed) && IncludeLinkState(target.State)) ||
((null != target.Identity) && (target.ChangeOrder < box.ChangeOrder) &&
((0 == target.SaveResultWasProcessed && EntityStates.Added == target.State) ||
(EntityStates.Added == target.SaveResultWasProcessed))))
{
Debug.Assert(box.ChangeOrder < end.ChangeOrder, "saving is out of order");
yield return end;
}
}
if (0 == --related)
{
break;
}
}
}
}
Debug.Assert(0 == related, "related count mismatch");
}
///
/// detach related bindings
///
/// detached entity
private void DetachRelated(ResourceBox entity)
{
foreach (RelatedEnd end in this.bindings.Values.Where(entity.IsRelatedEntity).ToList())
{
this.DetachExistingLink(end);
}
}
///
/// create the load property request
///
/// entity
/// name of collection or reference property to load
/// The AsyncCallback delegate.
/// user state
/// a aync result that you can get a response from
private LoadPropertyAsyncResult CreateLoadPropertyRequest(object entity, string propertyName, AsyncCallback callback, object state)
{
ResourceBox box = this.EnsureContained(entity, "entity");
Util.CheckArgumentNotEmpty(propertyName, "propertyName");
ClientType type = ClientType.Create(entity.GetType());
Debug.Assert(type.HasKeys, "must have keys to be contained");
if (EntityStates.Added == box.State)
{
throw Error.InvalidOperation(Strings.Context_NoLoadWithInsertEnd);
}
ClientType.ClientProperty property = type.GetProperty(propertyName, false);
Debug.Assert(null != property, "should have thrown if propertyName didn't exist");
Uri relativeUri;
bool allowAnyType = false;
if (type.MediaDataMember != null && propertyName == type.MediaDataMember.PropertyName)
{
// special case for requesting the "media" value of an ATOM media link entry
relativeUri = Util.CreateUri(XmlConstants.UriValueSegment, UriKind.Relative);
allowAnyType = true; // $value can be of any MIME type
}
else
{
relativeUri = Util.CreateUri(propertyName + (null != property.CollectionType ? "()" : String.Empty), UriKind.Relative);
}
Uri requestUri = Util.CreateUri(box.GetResourceUri(this.baseUriWithSlash), relativeUri);
HttpWebRequest request = this.CreateRequest(requestUri, XmlConstants.HttpMethodGet, allowAnyType, null);
DataServiceRequest dataServiceRequest = DataServiceRequest.GetInstance(property.PropertyType, requestUri);
return new LoadPropertyAsyncResult(entity, propertyName, this, request, callback, state, dataServiceRequest);
}
///
/// write the content section of the atom payload
///
/// writer
/// resource type
/// resource value
private void WriteContentProperties(XmlWriter writer, ClientType type, object resource)
{
#region value
foreach (ClientType.ClientProperty property in type.Properties)
{
// don't write mime data member or the mime type member for it
if (property == type.MediaDataMember ||
(type.MediaDataMember != null &&
type.MediaDataMember.MimeTypeProperty == property))
{
continue;
}
object propertyValue = property.GetValue(resource);
if (property.IsKnownType)
{
WriteContentProperty(writer, this.DataNamespace, property, propertyValue);
}
#if ASTORIA_OPEN_OBJECT
else if (property.OpenObjectProperty)
{
foreach (KeyValuePair pair in (IDictionary)propertyValue)
{
if ((null == pair.Value) || ClientConvert.IsKnownType(pair.Value.GetType()))
{
WriteContentProperty(writer, pair.Key, pair.Value, false);
}
}
}
#endif
else if (null == property.CollectionType)
{
ClientType nested = ClientType.Create(property.PropertyType);
if (!nested.HasKeys)
{
#region complex type
writer.WriteStartElement(property.PropertyName, this.DataNamespace);
string typeName = this.ResolveNameFromType(nested.ElementType);
if (!String.IsNullOrEmpty(typeName))
{
writer.WriteAttributeString(XmlConstants.AtomTypeAttributeName, XmlConstants.DataWebMetadataNamespace, typeName);
}
this.WriteContentProperties(writer, nested, propertyValue);
writer.WriteEndElement();
#endregion
}
}
}
#endregion
}
///
/// detach existing link
///
/// link to detach
private void DetachExistingLink(RelatedEnd existing)
{
if (this.bindings.Remove(existing))
{ // this link may have been previously detached by a detaching entity
existing.State = EntityStates.Detached;
this.objectToResource[existing.SourceResource].RelatedLinkCount--;
}
}
///
/// find and detach link for reference property
///
/// source entity
/// source entity property name for target entity
/// target entity
/// link merge option
/// true if found and not removed
private RelatedEnd DetachReferenceLink(object source, string sourceProperty, object target, MergeOption linkMerge)
{
RelatedEnd existing = this.GetLinks(source, sourceProperty).FirstOrDefault();
if (null != existing)
{
if ((target == existing.TargetResouce) ||
(MergeOption.AppendOnly == linkMerge) ||
(MergeOption.PreserveChanges == linkMerge && EntityStates.Modified == existing.State))
{
return existing;
}
this.DetachExistingLink(existing);
Debug.Assert(!this.bindings.Values.Any(o => (o.SourceResource == source) && (o.SourceProperty == sourceProperty)), "only expecting one");
}
return null;
}
///
/// verify the resource being tracked by context
///
/// resource
/// parameter name to include in ArgumentException
/// The given resource.
/// if resource is not contained
private ResourceBox EnsureContained(object resource, string parameterName)
{
Util.CheckArgumentNull(resource, parameterName);
ResourceBox box = null;
if (!this.objectToResource.TryGetValue(resource, out box))
{
throw Error.InvalidOperation(Strings.Context_EntityNotContained);
}
return box;
}
///
/// verify the source and target are relatable
///
/// source Resource
/// source Property
/// target Resource
/// destination state of relationship to evaluate for
/// true if DeletedState and one of the ends is in the added state
/// if source or target are null
/// if source or target are not contained
/// if source property is null
/// if source property empty
/// Can only relate ends with keys.
/// If target doesn't match property type.
/// If adding relationship where one of the ends is in the deleted state.
/// If attaching relationship where one of the ends is in the added or deleted state.
private bool EnsureRelatable(object source, string sourceProperty, object target, EntityStates state)
{
ResourceBox sourceResource = this.EnsureContained(source, "source");
ResourceBox targetResource = null;
if ((null != target) || ((EntityStates.Modified != state) && (EntityStates.Unchanged != state)))
{
targetResource = this.EnsureContained(target, "target");
}
Util.CheckArgumentNotEmpty(sourceProperty, "sourceProperty");
ClientType type = ClientType.Create(source.GetType());
Debug.Assert(type.HasKeys, "should be enforced by just adding an object");
// will throw InvalidOperationException if property doesn't exist
ClientType.ClientProperty property = type.GetProperty(sourceProperty, false);
if (property.IsKnownType)
{
throw Error.InvalidOperation(Strings.Context_RelationNotRefOrCollection);
}
if ((EntityStates.Unchanged == state) && (null == target) && (null != property.CollectionType))
{
targetResource = this.EnsureContained(target, "target");
}
if (((EntityStates.Added == state) || (EntityStates.Deleted == state)) && (null == property.CollectionType))
{
throw Error.InvalidOperation(Strings.Context_AddLinkCollectionOnly);
}
else if ((EntityStates.Modified == state) && (null != property.CollectionType))
{
throw Error.InvalidOperation(Strings.Context_SetLinkReferenceOnly);
}
// if (property.IsCollection) then property.PropertyType is the collection elementType
// either way you can only have a relation ship between keyed objects
type = ClientType.Create(property.CollectionType ?? property.PropertyType);
Debug.Assert(type.HasKeys, "should be enforced by just adding an object");
if ((null != target) && !type.ElementType.IsInstanceOfType(target))
{
// target is not of the correct type
throw Error.Argument(Strings.Context_RelationNotRefOrCollection, "target");
}
if ((EntityStates.Added == state) || (EntityStates.Unchanged == state))
{
if ((sourceResource.State == EntityStates.Deleted) ||
((targetResource != null) && (targetResource.State == EntityStates.Deleted)))
{
// can't add/attach new relationship when source or target in deleted state
throw Error.InvalidOperation(Strings.Context_NoRelationWithDeleteEnd);
}
}
if ((EntityStates.Deleted == state) || (EntityStates.Unchanged == state))
{
if ((sourceResource.State == EntityStates.Added) ||
((targetResource != null) && (targetResource.State == EntityStates.Added)))
{
// can't have non-added relationship when source or target is in added state
if (EntityStates.Deleted == state)
{
return true;
}
throw Error.InvalidOperation(Strings.Context_NoRelationWithInsertEnd);
}
}
return false;
}
/// validate and trim leading and trailing forward slashes
/// resource name to validate
/// if entitySetName was null
/// if entitySetName was empty or contained only forward slash
private void ValidateEntitySetName(ref string entitySetName)
{
Util.CheckArgumentNotEmpty(entitySetName, "entitySetName");
entitySetName = entitySetName.Trim(Util.ForwardSlash);
Util.CheckArgumentNotEmpty(entitySetName, "entitySetName");
Uri tmp = Util.CreateUri(entitySetName, UriKind.RelativeOrAbsolute);
if (tmp.IsAbsoluteUri ||
!String.IsNullOrEmpty(Util.CreateUri(this.baseUriWithSlash, tmp)
.GetComponents(UriComponents.Query | UriComponents.Fragment, UriFormat.SafeUnescaped)))
{
throw Error.Argument(Strings.Context_EntitySetName, "entitySetName");
}
}
/// create this.identityToResource when necessary
private void EnsureIdentityToResource()
{
if (null == this.identityToResource)
{
System.Threading.Interlocked.CompareExchange(ref this.identityToResource, new Dictionary(), null);
}
}
///
/// increment the resource change for sorting during submit changes
///
/// the resource to update the change order
private void IncrementChange(RelatedEnd box)
{
box.ChangeOrder = ++this.nextChange;
}
///
/// increment the resource change for sorting during submit changes
///
/// the resource to update the change order
private void IncrementChange(ResourceBox box)
{
box.ChangeOrder = ++this.nextChange;
}
///
/// Entity and LinkDescriptor base class that contains change order and state
///
internal abstract class Entry
{
/// change order
private uint changeOrder = UInt32.MaxValue;
/// was content generated for the entity
private bool saveContentGenerated;
/// was this entity save result processed
/// 0 - no processed, otherwise reflects the previous state
private EntityStates saveResultProcessed;
/// state
private EntityStates state;
/// last save exception per entry
private Exception saveError;
/// changeOrder
internal uint ChangeOrder
{
get { return this.changeOrder; }
set { this.changeOrder = value; }
}
/// true if resource, false if link
internal abstract bool IsResource
{
get;
}
/// was content generated for the entity
internal bool ContentGeneratedForSave
{
get { return this.saveContentGenerated; }
set { this.saveContentGenerated = value; }
}
/// was this entity save result processed
internal EntityStates SaveResultWasProcessed
{
get { return this.saveResultProcessed; }
set { this.saveResultProcessed = value; }
}
/// last save exception per entry
internal Exception SaveError
{
get { return this.saveError; }
set { this.saveError = value; }
}
/// state
internal EntityStates State
{
get { return this.state; }
set { this.state = value; }
}
}
///
/// An untyped container for a resource and its identity
///
[DebuggerDisplay("State = {state}, Uri = {editLink}, Element = {resource.GetType().ToString()}")]
internal class ResourceBox : Entry
{
/// uri to identitfy the entity
/// <atom:id>identity</id>
private Uri identity;
/// uri to edit the entity
/// <atom:link rel="edit" href="editLink" />
private Uri editLink;
// /// uri to query the entity
// /// <atom:link rel="self" href="queryLink" />
// private Uri queryLink;
/// entity ETag (concurrency token)
private string etag;
/// entity
private object resource;
/// count of links for which this entity is the source
private int relatedLinkCount;
/// constructor
/// resource Uri
/// resource EntitySet
/// non-null resource
internal ResourceBox(Uri identity, Uri editLink, object resource)
{
Debug.Assert(null == identity || identity.IsAbsoluteUri, "bad identity");
Debug.Assert(null != editLink, "null editLink");
this.identity = identity;
this.editLink = editLink;
this.resource = resource;
}
/// this is a entity
internal override bool IsResource
{
get { return true; }
}
/// uri to edit entity
internal Uri EditLink
{
get { return this.editLink; }
set { this.editLink = value; }
}
/// etag
internal string ETag
{
get { return this.etag; }
set { this.etag = value; }
}
/// entity uri identity
internal Uri Identity
{
get { return this.identity; }
set { this.identity = Util.CheckArgumentNull(value, "Identity"); }
}
/// count of links for which this entity is the source
internal int RelatedLinkCount
{
get { return this.relatedLinkCount; }
set { this.relatedLinkCount = value; }
}
/// entity
internal object Resource
{
get { return this.resource; }
}
/// uri to edit the entity
/// baseUriWithSlash
/// absolute uri which can be used to edit the entity
internal Uri GetResourceUri(Uri baseUriWithSlash)
{
Uri result = Util.CreateUri(baseUriWithSlash, this.EditLink);
return result;
}
/// is the entity the same as the source or target entity
/// related end
/// true if same as source or target entity
internal bool IsRelatedEntity(RelatedEnd related)
{
return ((this.resource == related.SourceResource) || (this.resource == related.TargetResouce));
}
}
///
/// An untyped container for a resource and its related end
///
[DebuggerDisplay("State = {state}")]
internal sealed class RelatedEnd : Entry
{
/// IEqualityComparer to compare equivalence between to related ends
internal static readonly IEqualityComparer EquivalenceComparer = new EqualityComparer();
/// source entity
private readonly object source;
/// name of property on source entity that references the target entity
private readonly string sourceProperty;
/// target entity
private readonly object target;
// /// Property on the target resource that relates back to the source resource
// internal readonly string ChildProperty;
/// constructor
/// parentResource
/// sourceProperty
/// childResource
internal RelatedEnd(object source, string property, object target)
{
Debug.Assert(null != source, "null source");
Debug.Assert(!String.IsNullOrEmpty(property), "null target");
this.source = source;
this.sourceProperty = property;
this.target = target;
}
/// this is a link
internal override bool IsResource
{
get { return false; }
}
/// target resource
internal object TargetResouce
{
get { return this.target; }
}
/// source resource property name
internal string SourceProperty
{
get { return this.sourceProperty; }
}
/// source resource
internal object SourceResource
{
get { return this.source; }
}
///
/// Are the two related ends equivalent?
///
/// x
/// y
/// true if the related ends are equivalent
public static bool Equals(RelatedEnd x, RelatedEnd y)
{
return ((x.SourceResource == y.SourceResource) &&
(x.TargetResouce == y.TargetResouce) &&
(x.SourceProperty == y.SourceProperty));
}
/// test for equivalence
private sealed class EqualityComparer : IEqualityComparer
{
/// test for equivalence
/// x
/// y
/// true if equivalent
bool IEqualityComparer.Equals(RelatedEnd x, RelatedEnd y)
{
return RelatedEnd.Equals(x, y);
}
/// hash code based on the contained resources
/// x
/// hash code
int IEqualityComparer.GetHashCode(RelatedEnd x)
{
return (x.SourceResource.GetHashCode() ^
((null != x.TargetResouce) ? x.TargetResouce.GetHashCode() : 0) ^
x.SourceProperty.GetHashCode());
}
}
}
/// wrapper around loading a property from a response
private class LoadPropertyAsyncResult : QueryAsyncResult
{
/// entity whose property is being loaded
private readonly object entity;
/// name of the property on the entity that is being loaded
private readonly string propertyName;
/// constructor
/// entity
/// name of collection or reference property to load
/// Originating context
/// Originating WebRequest
/// user callback
/// user state
/// request object.
internal LoadPropertyAsyncResult(object entity, string propertyName, DataServiceContext context, HttpWebRequest request, AsyncCallback callback, object state, DataServiceRequest dataServiceRequest)
: base(context, "LoadProperty", dataServiceRequest, request, callback, state)
{
this.entity = entity;
this.propertyName = propertyName;
}
///
/// loading a property from a response
///
/// QueryOperationResponse instance containing information about the response.
internal QueryOperationResponse LoadProperty()
{
IEnumerable results = null;
DataServiceContext context = (DataServiceContext)this.Source;
ClientType type = ClientType.Create(this.entity.GetType());
Debug.Assert(type.HasKeys, "must have keys to be contained");
ResourceBox box = context.EnsureContained(this.entity, "entity");
if (EntityStates.Added == box.State)
{
throw Error.InvalidOperation(Strings.Context_NoLoadWithInsertEnd);
}
ClientType.ClientProperty property = type.GetProperty(this.propertyName, false);
Type elementType = property.CollectionType ?? property.NullablePropertyType;
try
{
if (type.MediaDataMember == property)
{
results = this.ReadPropertyFromRawData(property);
}
else
{
results = this.ReadPropertyFromAtom(box, property);
}
return this.GetResponse(results, elementType);
}
catch (InvalidOperationException ex)
{
QueryOperationResponse response = this.GetResponse(results, elementType);
if (response != null)
{
response.Error = ex;
throw new DataServiceQueryException(Strings.DataServiceException_GeneralError, ex, response);
}
throw;
}
}
///
/// Load property data from an ATOM response
///
/// Box pointing to the entity to load this to
/// The property being loaded
/// property values as IEnumerable.
private IEnumerable ReadPropertyFromAtom(ResourceBox box, ClientType.ClientProperty property)
{
DataServiceContext context = (DataServiceContext)this.Source;
bool deletedState = (EntityStates.Deleted == box.State);
Type nestedType;
#if ASTORIA_OPEN_OBJECT
if (property.OpenObjectProperty)
{
nestedType = typeof(OpenObject);
}
else
#endif
{
nestedType = property.CollectionType ?? property.NullablePropertyType;
}
ClientType clientType = ClientType.Create(nestedType);
// when setting a reference, use the entity
// when adding an item to a collection, use the collection object referenced by the entity
bool setNestedValue = false;
object collection = this.entity;
if (null != property.CollectionType)
{ // get the collection that we actually add nested
collection = property.GetValue(this.entity);
if (null == collection)
{
setNestedValue = true;
collection = Activator.CreateInstance(typeof(List<>).MakeGenericType(nestedType));
}
}
Func create = delegate(DataServiceContext ctx, XmlReader reader, Type elmentType)
{
return new MaterializeAtom(ctx, reader, elmentType, ctx.MergeOption);
};
// store the results so that they can be there in the response body.
Type elementType = property.CollectionType ?? property.NullablePropertyType;
IList results = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType));
// elementType.ElementType has Nullable stripped away, use nestedType for materializer
using (MaterializeAtom materializer = context.GetMaterializer(this, nestedType, create))
{
if (null != materializer)
{
int count = 0;
#if ASTORIA_OPEN_OBJECT
object openProperties = null;
#endif
foreach (object child in materializer)
{
results.Add(child);
count++;
#if ASTORIA_OPEN_OBJECT
property.SetValue(collection, child, this.propertyName, ref openProperties, true);
#else
property.SetValue(collection, child, this.propertyName, true);
#endif
// via LoadProperty, you can have a property with and null value
if ((null != child) && (MergeOption.NoTracking != materializer.MergeOptionValue) && clientType.HasKeys)
{
if (deletedState)
{
context.DeleteLink(this.entity, this.propertyName, child);
}
else
{ // put link into unchanged state
context.AttachLink(this.entity, this.propertyName, child, materializer.MergeOptionValue);
}
}
}
}
// we don't do this because we are loading, not refreshing
// if ((0 == count) && (MergeOption.OverwriteChanges == this.mergeOption))
// { property.Clear(entity); }
}
if (setNestedValue)
{
#if ASTORIA_OPEN_OBJECT
object openProperties = null;
property.SetValue(this.entity, collection, this.propertyName, ref openProperties, false);
#else
property.SetValue(this.entity, collection, this.propertyName, false);
#endif
}
return results;
}
///
/// Load property data form a raw response
///
/// The property being loaded
/// property values as IEnumerable.
private IEnumerable ReadPropertyFromRawData(ClientType.ClientProperty property)
{
// if this is the data property for a media entry, what comes back
// is the raw value (no markup)
#if ASTORIA_OPEN_OBJECT
object openProps = null;
#endif
string mimeType = null;
Encoding encoding = null;
Type elementType = property.CollectionType ?? property.NullablePropertyType;
IList results = (IList)Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType));
HttpProcessUtility.ReadContentType(this.ContentType, out mimeType, out encoding);
using (Stream responseStream = this.GetResponseStream())
{
// special case byte[], and for everything else let std conversion kick-in
if (property.PropertyType == typeof(byte[]))
{
int total = checked((int)this.ContentLength);
byte[] buffer = new byte[total];
int read = 0;
while (read < total)
{
int r = responseStream.Read(buffer, read, total - read);
if (r <= 0)
{
throw Error.InvalidOperation(Strings.Context_UnexpectedZeroRawRead);
}
read += r;
}
results.Add(buffer);
#if ASTORIA_OPEN_OBJECT
property.SetValue(this.entity, buffer, this.propertyName, ref openProps, false);
#else
property.SetValue(this.entity, buffer, this.propertyName, false);
#endif
}
else
{
StreamReader reader = new StreamReader(responseStream, encoding);
object convertedValue = property.PropertyType == typeof(string) ?
reader.ReadToEnd() :
ClientConvert.ChangeType(reader.ReadToEnd(), property.PropertyType);
results.Add(convertedValue);
#if ASTORIA_OPEN_OBJECT
property.SetValue(this.entity, convertedValue, this.propertyName, ref openProps, false);
#else
property.SetValue(this.entity, convertedValue, this.propertyName, false);
#endif
}
}
#if ASTORIA_OPEN_OBJECT
Debug.Assert(openProps == null, "These should not be set in this path");
#endif
if (property.MimeTypeProperty != null)
{
// an implication of this 3rd-arg-null is that mime type properties cannot be open props
#if ASTORIA_OPEN_OBJECT
property.MimeTypeProperty.SetValue(this.entity, mimeType, null, ref openProps, false);
Debug.Assert(openProps == null, "These should not be set in this path");
#else
property.MimeTypeProperty.SetValue(this.entity, mimeType, null, false);
#endif
}
return results;
}
}
///
/// implementation of IAsyncResult for SaveChanges
///
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable", Justification = "Pending")]
private class SaveAsyncResult : BaseAsyncResult
{
/// where to pull the changes from
private readonly DataServiceContext Context;
/// sorted list of entries by change order
private readonly List ChangedEntries;
/// array of queries being executed
private readonly DataServiceRequest[] Queries;
/// operations
private readonly List Responses;
/// boundary used when generating batch boundary
private readonly string batchBoundary;
/// option in use for SaveChanges
private readonly SaveChangesOptions options;
/// if true then async, else [....]
private readonly bool executeAsync;
/// debugging trick to track number of completed requests
private int changesCompleted;
/// wrapped request
private PerRequest request;
/// batch web response
private HttpWebResponse batchResponse;
/// response stream for the batch
private Stream httpWebResponseStream;
/// service response
private DataServiceResponse service;
/// The ResourceBox or RelatedEnd currently in flight
private int entryIndex = -1;
///
/// True if the current in-flight request is a media link entry POST
/// that needs to be followed by a PUT for the rest of the properties
///
private bool procesingMediaLinkEntry;
/// response stream
private BatchStream responseBatchStream;
/// temporary buffer when cache results from CUD op in non-batching save changes
private byte[] buildBatchBuffer;
/// temporary writer when cache results from CUD op in non-batching save changes
private StreamWriter buildBatchWriter;
/// count of data actually copied
private long copiedContentLength;
/// what is the changset boundary
private string changesetBoundary;
/// is a change set being cached
private bool changesetStarted;
#region constructors
///
/// constructor for async operations
///
/// context
/// method
/// queries
/// options
/// user callback
/// user state object
/// async or [....]
internal SaveAsyncResult(DataServiceContext context, string method, DataServiceRequest[] queries, SaveChangesOptions options, AsyncCallback callback, object state, bool async)
: base(context, method, callback, state)
{
this.executeAsync = async;
this.Context = context;
this.Queries = queries;
this.options = options;
this.Responses = new List();
if (null == queries)
{
#region changed entries
this.ChangedEntries = context.objectToResource.Values.Cast()
.Union(context.bindings.Values.Cast())
.Where(HasModifiedResourceState)
.OrderBy(o => o.ChangeOrder)
.ToList();
foreach (Entry e in this.ChangedEntries)
{
e.ContentGeneratedForSave = false;
e.SaveResultWasProcessed = 0;
e.SaveError = null;
if (!e.IsResource)
{
object target = ((RelatedEnd)e).TargetResouce;
if (null != target)
{
Entry f = context.objectToResource[target];
if (EntityStates.Unchanged == f.State)
{
f.ContentGeneratedForSave = false;
f.SaveResultWasProcessed = 0;
f.SaveError = null;
}
}
}
}
#endregion
}
else
{
this.ChangedEntries = new List();
}
if (IsFlagSet(options, SaveChangesOptions.Batch))
{
this.batchBoundary = XmlConstants.HttpMultipartBoundaryBatch + "_" + Guid.NewGuid().ToString();
}
else
{
this.batchBoundary = XmlConstants.HttpMultipartBoundaryBatchResponse + "_" + Guid.NewGuid().ToString();
this.DataServiceResponse = new DataServiceResponse(null, -1, this.Responses, false /*batchResponse*/);
}
}
#endregion constructor
#region end
/// generate the batch request of all changes to save
internal DataServiceResponse DataServiceResponse
{
get
{
Debug.Assert(null != this.service, "null service");
return this.service;
}
set
{
this.service = value;
}
}
/// process the batch
/// data service response
internal DataServiceResponse EndRequest()
{
if ((null != this.responseBatchStream) || (null != this.httpWebResponseStream))
{
this.HandleBatchResponse();
}
return this.DataServiceResponse;
}
#endregion
#region start a batch
/// initial the async batch save changeset
/// whether we need to update MERGE or PUT method for update.
internal void BatchBeginRequest(bool replaceOnUpdate)
{
PerRequest pereq = null;
try
{
MemoryStream memory = this.GenerateBatchRequest(replaceOnUpdate);
if (null != memory)
{
HttpWebRequest httpWebRequest = this.CreateBatchRequest(memory);
this.request = pereq = new PerRequest();
pereq.Request = httpWebRequest;
pereq.RequestStreamContent = memory;
this.httpWebResponseStream = new MemoryStream();
int step = ++pereq.RequestStep;
IAsyncResult asyncResult = httpWebRequest.BeginGetRequestStream(this.AsyncEndGetRequestStream, pereq);
bool reallyCompletedSynchronously = asyncResult.CompletedSynchronously && (step < pereq.RequestStep);
pereq.RequestCompletedSynchronously &= reallyCompletedSynchronously;
}
else
{
Debug.Assert(this.CompletedSynchronously, "completedSynchronously");
Debug.Assert(this.IsCompleted, "completed");
}
}
catch (Exception e)
{
this.HandleFailure(pereq, e);
throw; // to user on BeginSaveChangeSet, will still invoke Callback
}
finally
{
this.HandleCompleted(pereq); // will invoke user callback
}
Debug.Assert((this.CompletedSynchronously && this.IsCompleted) || !this.CompletedSynchronously, "[....] without complete");
}
#if !ASTORIA_LIGHT // Synchronous methods not available
///
/// Synchronous batch request
///
/// whether we need to update MERGE or PUT method for update.
internal void BatchRequest(bool replaceOnUpdate)
{
MemoryStream memory = this.GenerateBatchRequest(replaceOnUpdate);
if ((null != memory) && (0 < memory.Length))
{
HttpWebRequest httpWebRequest = this.CreateBatchRequest(memory);
using (System.IO.Stream requestStream = httpWebRequest.GetRequestStream())
{
byte[] buffer = memory.GetBuffer();
int bufferOffset = checked((int)memory.Position);
int bufferLength = checked((int)memory.Length) - bufferOffset;
// the following is useful in the debugging Immediate Window
// string x = System.Text.Encoding.UTF8.GetString(buffer, bufferOffset, bufferLength);
requestStream.Write(buffer, bufferOffset, bufferLength);
}
HttpWebResponse httpWebResponse = (HttpWebResponse)httpWebRequest.GetResponse();
this.batchResponse = httpWebResponse;
if (null != httpWebResponse)
{
this.httpWebResponseStream = httpWebResponse.GetResponseStream();
}
}
}
#endif
#endregion
#region start a non-batch requests
///
/// This starts the next change
///
/// whether we need to update MERGE or PUT method for update.
internal void BeginNextChange(bool replaceOnUpdate)
{
Debug.Assert(!this.IsCompleted, "why being called if already completed?");
// SaveCallback can't chain synchronously completed responses, caller will loop the to next change
PerRequest pereq = null;
do
{
HttpWebRequest httpWebRequest = null;
HttpWebResponse response = null;
try
{
if (null != this.request)
{
this.IsCompleted = true;
Error.ThrowInternalError(InternalError.InvalidBeginNextChange);
}
httpWebRequest = this.CreateNextRequest(replaceOnUpdate);
if ((null != httpWebRequest) || (this.entryIndex < this.ChangedEntries.Count))
{
if (this.ChangedEntries[this.entryIndex].ContentGeneratedForSave)
{
Debug.Assert(this.ChangedEntries[this.entryIndex] is RelatedEnd, "only expected RelatedEnd to presave");
Debug.Assert(
this.ChangedEntries[this.entryIndex].State == EntityStates.Added ||
this.ChangedEntries[this.entryIndex].State == EntityStates.Modified,
"only expected added to presave");
continue;
}
MemoryStream memoryStream = null;
if (this.executeAsync)
{
#region async
this.request = pereq = new PerRequest();
pereq.Request = httpWebRequest;
IAsyncResult asyncResult;
int step = ++pereq.RequestStep;
if (this.procesingMediaLinkEntry || (null == (memoryStream = this.CreateChangeData(this.entryIndex, false))))
{
asyncResult = httpWebRequest.BeginGetResponse(this.AsyncEndGetResponse, pereq);
}
else
{
httpWebRequest.ContentLength = memoryStream.Length - memoryStream.Position;
pereq.RequestStreamContent = memoryStream;
asyncResult = httpWebRequest.BeginGetRequestStream(this.AsyncEndGetRequestStream, pereq);
}
bool reallyCompletedSynchronously = asyncResult.CompletedSynchronously && (step < pereq.RequestStep);
pereq.RequestCompletedSynchronously &= reallyCompletedSynchronously;
this.CompletedSynchronously &= reallyCompletedSynchronously;
#endregion
}
#if !ASTORIA_LIGHT // Synchronous methods not available
else
{
#region [....]
memoryStream = this.CreateChangeData(this.entryIndex, false);
if (null != memoryStream)
{
byte[] buffer = memoryStream.GetBuffer();
int bufferOffset = checked((int)memoryStream.Position);
int bufferLength = checked((int)memoryStream.Length) - bufferOffset;
httpWebRequest.ContentLength = bufferLength;
using (Stream stream = httpWebRequest.GetRequestStream())
{
// the following is useful in the debugging Immediate Window
// string x = System.Text.Encoding.UTF8.GetString(memoryStream.GetBuffer(), bufferOffset, (int)memoryStream.Length);
stream.Write(buffer, bufferOffset, bufferLength);
}
}
response = (HttpWebResponse)httpWebRequest.GetResponse();
if (!this.procesingMediaLinkEntry)
{
this.changesCompleted++;
}
this.HandleOperationResponse(httpWebRequest, response);
this.HandleOperationResponseData(response);
this.HandleOperationEnd();
this.request = null;
#endregion
}
#endif
}
else
{
this.IsCompleted = true;
if (this.CompletedSynchronously)
{
this.HandleCompleted(pereq);
}
}
}
catch (InvalidOperationException e)
{
WebUtil.GetHttpWebResponse(e, ref response);
this.HandleOperationException(e, httpWebRequest, response);
this.HandleCompleted(pereq);
}
finally
{
if (null != response)
{
response.Close();
}
}
// either everything completed synchronously until a change is saved and its state changed
// and we don't return to this loop until then or something was asynchronous
// and we won't continue in this loop, instead letting the inner most loop start the next request
}
while (((null == pereq) || (pereq.RequestCompleted && pereq.RequestCompletedSynchronously)) && !this.IsCompleted);
Debug.Assert(this.executeAsync || this.CompletedSynchronously, "[....] !CompletedSynchronously");
Debug.Assert((this.CompletedSynchronously && this.IsCompleted) || !this.CompletedSynchronously, "[....] without complete");
Debug.Assert(this.entryIndex < this.ChangedEntries.Count || this.ChangedEntries.All(o => o.ContentGeneratedForSave), "didn't generated content for all entities/links");
}
/// cleanup work to do once the batch / savechanges is complete
protected override void CompletedRequest()
{
this.buildBatchBuffer = null;
if (null != this.buildBatchWriter)
{
Debug.Assert(!IsFlagSet(this.options, SaveChangesOptions.Batch), "should be non-batch");
this.HandleOperationEnd();
this.buildBatchWriter.WriteLine("--{0}--", this.batchBoundary);
this.buildBatchWriter.Flush();
Debug.Assert(Object.ReferenceEquals(this.httpWebResponseStream, this.buildBatchWriter.BaseStream), "expected different stream");
this.httpWebResponseStream.Position = 0;
this.buildBatchWriter = null;
// the following is useful in the debugging Immediate Window
// string x = System.Text.Encoding.UTF8.GetString(memoryStream.GetBuffer(), 0, (int)memoryStream.Length);
this.responseBatchStream = new BatchStream(this.httpWebResponseStream, this.batchBoundary, HttpProcessUtility.EncodingUtf8NoPreamble, false);
}
}
/// build the *Descriptor object in the ChangeList
/// entry to build from
/// EntityDescriptor or LinkDescriptor
private static Descriptor BuildReturn(Entry entry)
{
if (entry.IsResource)
{
ResourceBox box = (ResourceBox)entry;
EntityDescriptor obj = new EntityDescriptor(box.Resource, box.ETag, box.State);
return obj;
}
else
{
RelatedEnd end = (RelatedEnd)entry;
LinkDescriptor obj = new LinkDescriptor(end.SourceResource, end.SourceProperty, end.TargetResouce, end.State);
return obj;
}
}
/// verify non-null and not completed
/// the request in progress
/// error code if null or completed
/// the next step to validate CompletedSyncronously
private static int CompleteCheck(PerRequest value, InternalError errorcode)
{
if ((null == value) || value.RequestCompleted)
{
Error.ThrowInternalError(errorcode);
}
return ++value.RequestStep;
}
/// verify they have the same reference
/// the actual thing
/// the expected thing
/// error code if they are not
private static void EqualRefCheck(PerRequest actual, PerRequest expected, InternalError errorcode)
{
if (!Object.ReferenceEquals(actual, expected))
{
Error.ThrowInternalError(errorcode);
}
}
/// Set the AsyncWait and invoke the user callback.
/// the request object
private void HandleCompleted(PerRequest pereq)
{
if (null != pereq)
{
this.CompletedSynchronously &= pereq.RequestCompletedSynchronously;
if (pereq.RequestCompleted)
{
System.Threading.Interlocked.CompareExchange(ref this.request, null, pereq);
if (IsFlagSet(this.options, SaveChangesOptions.Batch))
{ // all competing thread must complete this before user calback is invoked
System.Threading.Interlocked.CompareExchange(ref this.batchResponse, pereq.HttpWebResponse, null);
pereq.HttpWebResponse = null;
}
pereq.Dispose();
}
}
this.HandleCompleted();
}
/// Cache the exception that happened on the background thread for the caller of EndSaveChanges.
/// the request object
/// exception object from background thread
/// true if the exception should be rethrown
private bool HandleFailure(PerRequest pereq, Exception e)
{
if (null != pereq)
{
pereq.RequestCompleted = true;
}
return this.HandleFailure(e);
}
///
/// Create HttpWebRequest from the next availabe resource
///
/// whether we need to update MERGE or PUT method for update.
/// web request
private HttpWebRequest CreateNextRequest(bool replaceOnUpdate)
{
if (!this.procesingMediaLinkEntry)
{
this.entryIndex++;
}
else
{
// if we were creating a media entry before, then the "next change"
// is to do the second step of the creation, a PUT to update
// metadata
this.procesingMediaLinkEntry = false;
}
if (unchecked((uint)this.entryIndex < (uint)this.ChangedEntries.Count))
{
Entry entry = this.ChangedEntries[this.entryIndex];
if (entry.IsResource)
{
ResourceBox box = (ResourceBox)entry;
HttpWebRequest req;
if ((EntityStates.Added == entry.State) && (null != (req = this.Context.CheckAndProcessMediaEntry(box))))
{
this.procesingMediaLinkEntry = true;
}
else
{
Debug.Assert(!this.procesingMediaLinkEntry || entry.State == EntityStates.Modified, "!this.procesingMediaLinkEntry || entry.State == EntityStates.Modified");
req = this.Context.CreateRequest(box, entry.State, replaceOnUpdate);
}
return req;
}
return this.Context.CreateRequest((RelatedEnd)entry);
}
return null;
}
///
/// create memory stream for entry (entity or link)
///
/// index into changed entries
/// include newline in output
/// memory stream of data for entry
private MemoryStream CreateChangeData(int index, bool newline)
{
Entry entry = this.ChangedEntries[index];
Debug.Assert(!entry.ContentGeneratedForSave, "already saved entity/link");
entry.ContentGeneratedForSave = true;
if (entry.IsResource)
{
ResourceBox box = (ResourceBox)entry;
if (!this.procesingMediaLinkEntry)
{
Debug.Assert(!this.procesingMediaLinkEntry || entry.State == EntityStates.Modified, "!this.procesingMediaLinkEntry || entry.State == EntityStates.Modified");
return this.Context.CreateRequestData(box, newline);
}
}
else
{
RelatedEnd link = (RelatedEnd)entry;
if ((EntityStates.Added == link.State) ||
((EntityStates.Modified == link.State) && (null != link.TargetResouce)))
{
return this.Context.CreateRequestData(link, newline);
}
}
return null;
}
#endregion
#region generate batch response from non-batch
/// basic separator between response
private void HandleOperationStart()
{
this.HandleOperationEnd();
if (null == this.httpWebResponseStream)
{
this.httpWebResponseStream = new MemoryStream();
}
if (null == this.buildBatchWriter)
{
this.buildBatchWriter = new StreamWriter(this.httpWebResponseStream); // defaults to UTF8 w/o preamble
}
if (null == this.changesetBoundary)
{
this.changesetBoundary = XmlConstants.HttpMultipartBoundaryChangesetResponse + "_" + Guid.NewGuid().ToString();
}
this.changesetStarted = true;
this.buildBatchWriter.WriteLine("--{0}", this.batchBoundary);
this.buildBatchWriter.WriteLine("{0}: {1}; boundary={2}", XmlConstants.HttpContentType, XmlConstants.MimeMultiPartMixed, this.changesetBoundary);
this.buildBatchWriter.WriteLine();
this.buildBatchWriter.WriteLine("--{0}", this.changesetBoundary);
}
/// write the trailing --changesetboundary--
private void HandleOperationEnd()
{
if (this.changesetStarted)
{
Debug.Assert(null != this.buildBatchWriter, "buildBatchWriter");
Debug.Assert(null != this.changesetBoundary, "changesetBoundary");
this.buildBatchWriter.WriteLine();
this.buildBatchWriter.WriteLine("--{0}--", this.changesetBoundary);
this.changesetStarted = false;
}
}
/// operation with exception
/// exception object
/// request object
/// response object
private void HandleOperationException(Exception e, HttpWebRequest httpWebRequest, HttpWebResponse response)
{
if (null != response)
{
this.HandleOperationResponse(httpWebRequest, response);
this.HandleOperationResponseData(response);
this.HandleOperationEnd();
}
else
{
this.HandleOperationStart();
WriteOperationResponseHeaders(this.buildBatchWriter, 500);
this.buildBatchWriter.WriteLine("{0}: {1}", XmlConstants.HttpContentType, XmlConstants.MimeTextPlain);
this.buildBatchWriter.WriteLine("{0}: {1}", XmlConstants.HttpContentID, this.ChangedEntries[this.entryIndex].ChangeOrder);
this.buildBatchWriter.WriteLine();
this.buildBatchWriter.WriteLine(e.ToString());
this.HandleOperationEnd();
}
this.request = null;
if (!IsFlagSet(this.options, SaveChangesOptions.ContinueOnError))
{
this.IsCompleted = true;
// if it was a media link entry don't even try to do a PUT if the POST didn't succeed
this.procesingMediaLinkEntry = false;
}
}
/// operation with HttpWebResponse
/// request object
/// response object
private void HandleOperationResponse(HttpWebRequest httpWebRequest, HttpWebResponse response)
{
this.HandleOperationStart();
string location = null;
if (this.ChangedEntries[this.entryIndex].IsResource &&
this.ChangedEntries[this.entryIndex].State == EntityStates.Added)
{
location = response.Headers[XmlConstants.HttpResponseLocation];
if (WebUtil.SuccessStatusCode(response.StatusCode))
{
if (null != location)
{
this.Context.AttachLocation(((ResourceBox)this.ChangedEntries[this.entryIndex]).Resource, location);
}
else
{
throw Error.NotSupported(Strings.Deserialize_NoLocationHeader);
}
}
}
if ((null == location) && (null != httpWebRequest))
{
location = httpWebRequest.RequestUri.OriginalString;
}
WriteOperationResponseHeaders(this.buildBatchWriter, (int)response.StatusCode);
foreach (string name in response.Headers.AllKeys)
{
if (XmlConstants.HttpContentLength != name)
{
this.buildBatchWriter.WriteLine("{0}: {1}", name, response.Headers[name]);
}
}
this.buildBatchWriter.WriteLine("{0}: {1}", XmlConstants.HttpContentID, this.ChangedEntries[this.entryIndex].ChangeOrder);
this.buildBatchWriter.WriteLine();
}
///
/// copy the response data
///
/// response object
private void HandleOperationResponseData(HttpWebResponse response)
{
using (Stream stream = response.GetResponseStream())
{
if (null != stream)
{
this.buildBatchWriter.Flush();
if (0 == WebUtil.CopyStream(stream, this.buildBatchWriter.BaseStream, ref this.buildBatchBuffer))
{
this.HandleOperationResponseNoData();
}
}
}
}
/// only call when no data was written to added "Content-Length: 0"
private void HandleOperationResponseNoData()
{
#if DEBUG
MemoryStream memory = this.buildBatchWriter.BaseStream as MemoryStream;
Debug.Assert(null != memory, "expected MemoryStream");
Debug.Assert(
(char)memory.GetBuffer()[(int)memory.Length - 2] == '\r' &&
(char)memory.GetBuffer()[(int)memory.Length - 1] == '\n',
"didn't end with newline");
#endif
this.buildBatchWriter.BaseStream.Position -= 2;
this.buildBatchWriter.WriteLine("{0}: {1}", XmlConstants.HttpContentLength, 0);
this.buildBatchWriter.WriteLine();
}
#endregion
///
/// create the web request for a batch
///
/// memory stream for length
/// httpweb request
private HttpWebRequest CreateBatchRequest(MemoryStream memory)
{
Uri requestUri = Util.CreateUri(this.Context.baseUriWithSlash, Util.CreateUri("$batch", UriKind.Relative));
string contentType = XmlConstants.MimeMultiPartMixed + "; " + XmlConstants.HttpMultipartBoundary + "=" + this.batchBoundary;
HttpWebRequest httpWebRequest = this.Context.CreateRequest(requestUri, XmlConstants.HttpMethodPost, false, contentType);
httpWebRequest.ContentLength = memory.Length - memory.Position;
return httpWebRequest;
}
/// generate the batch request of all changes to save
/// whether we need to update MERGE or PUT method for update.
/// buffer containing data for request stream
private MemoryStream GenerateBatchRequest(bool replaceOnUpdate)
{
this.changesetBoundary = null;
if (null != this.Queries)
{
}
else if (0 == this.ChangedEntries.Count)
{
this.DataServiceResponse = new DataServiceResponse(null, (int)WebExceptionStatus.Success, this.Responses, true /*batchResponse*/);
this.IsCompleted = true;
return null;
}
else
{
this.changesetBoundary = XmlConstants.HttpMultipartBoundaryChangeSet + "_" + Guid.NewGuid().ToString();
}
MemoryStream memory = new MemoryStream();
StreamWriter text = new StreamWriter(memory); // defaults to UTF8 w/o preamble
if (null != this.Queries)
{
for (int i = 0; i < this.Queries.Length; ++i)
{
Uri requestUri = Util.CreateUri(this.Context.baseUriWithSlash, this.Queries[i].RequestUri);
Debug.Assert(null != requestUri, "request uri is null");
Debug.Assert(requestUri.IsAbsoluteUri, "request uri is not absolute uri");
Debug.Assert(UriUtil.UriInvariantInsensitiveIsBaseOf(this.Context.baseUriWithSlash, requestUri), "context is not base of request uri");
text.WriteLine("--{0}", this.batchBoundary);
WriteOperationRequestHeaders(text, XmlConstants.HttpMethodGet, requestUri.AbsoluteUri);
text.WriteLine();
}
}
else if (0 < this.ChangedEntries.Count)
{
text.WriteLine("--{0}", this.batchBoundary);
text.WriteLine("{0}: {1}; boundary={2}", XmlConstants.HttpContentType, XmlConstants.MimeMultiPartMixed, this.changesetBoundary);
text.WriteLine();
for (int i = 0; i < this.ChangedEntries.Count; ++i)
{
#region validate changeset boundary starts on newline
#if DEBUG
{
text.Flush();
Debug.Assert(
(char)memory.GetBuffer()[(int)memory.Length - 2] == '\r' &&
(char)memory.GetBuffer()[(int)memory.Length - 1] == '\n',
"boundary didn't start with newline");
}
#endif
#endregion
Entry entry = this.ChangedEntries[i];
if (entry.ContentGeneratedForSave)
{
continue;
}
text.WriteLine("--{0}", this.changesetBoundary);
MemoryStream stream = this.CreateChangeData(i, true);
if (entry.IsResource)
{
ResourceBox box = (ResourceBox)entry;
// media link entry creation is not supported in batch mode
if (box.State == EntityStates.Added &&
ClientType.Create(box.Resource.GetType()).MediaDataMember != null)
{
throw Error.NotSupported(Strings.Context_BatchNotSupportedForMediaLink);
}
this.Context.CreateRequestBatch(box, text, replaceOnUpdate);
}
else
{
this.Context.CreateRequestBatch((RelatedEnd)entry, text);
}
byte[] buffer = null;
int bufferOffset = 0, bufferLength = 0;
if (null != stream)
{
buffer = stream.GetBuffer();
bufferOffset = checked((int)stream.Position);
bufferLength = checked((int)stream.Length) - bufferOffset;
}
if (0 < bufferLength)
{
text.WriteLine("{0}: {1}", XmlConstants.HttpContentLength, bufferLength);
}
text.WriteLine(); // NewLine separates header from message
if (0 < bufferLength)
{
text.Flush();
text.BaseStream.Write(buffer, bufferOffset, bufferLength);
}
}
#region validate changeset boundary ended with newline
#if DEBUG
{
text.Flush();
Debug.Assert(
(char)memory.GetBuffer()[(int)memory.Length - 2] == '\r' &&
(char)memory.GetBuffer()[(int)memory.Length - 1] == '\n',
"post CreateRequest boundary didn't start with newline");
}
#endif
#endregion
// The boundary delimiter line following the last body part
// has two more hyphens after the boundary parameter value.
text.WriteLine("--{0}--", this.changesetBoundary);
}
text.WriteLine("--{0}--", this.batchBoundary);
text.Flush();
Debug.Assert(Object.ReferenceEquals(text.BaseStream, memory), "should be same");
Debug.Assert(this.ChangedEntries.All(o => o.ContentGeneratedForSave), "didn't generated content for all entities/links");
#region Validate batch format
#if DEBUG
int testGetCount = 0;
int testOpCount = 0;
int testBeginSetCount = 0;
int testEndSetCount = 0;
memory.Position = 0;
BatchStream testBatch = new BatchStream(memory, this.batchBoundary, HttpProcessUtility.EncodingUtf8NoPreamble, true);
while (testBatch.MoveNext())
{
switch (testBatch.State)
{
case BatchStreamState.StartBatch:
case BatchStreamState.EndBatch:
default:
Debug.Assert(false, "shouldn't happen");
break;
case BatchStreamState.Get:
testGetCount++;
break;
case BatchStreamState.BeginChangeSet:
testBeginSetCount++;
break;
case BatchStreamState.EndChangeSet:
testEndSetCount++;
break;
case BatchStreamState.Post:
case BatchStreamState.Put:
case BatchStreamState.Delete:
case BatchStreamState.Merge:
testOpCount++;
break;
}
}
Debug.Assert((null == this.Queries && 1 == testBeginSetCount) || (0 == testBeginSetCount), "more than one BeginChangeSet");
Debug.Assert(testBeginSetCount == testEndSetCount, "more than one EndChangeSet");
Debug.Assert((null == this.Queries && testGetCount == 0) || this.Queries.Length == testGetCount, "too many get count");
// Debug.Assert(this.ChangedEntries.Count == testOpCount, "too many op count");
Debug.Assert(BatchStreamState.EndBatch == testBatch.State, "should have ended propertly");
#endif
#endregion
this.changesetBoundary = null;
memory.Position = 0;
return memory;
}
#region handle batch response
///
/// process the batch changeset response
///
private void HandleBatchResponse()
{
string boundary = this.batchBoundary;
Encoding encoding = Encoding.UTF8;
Dictionary headers = null;
Exception exception = null;
try
{
if (IsFlagSet(this.options, SaveChangesOptions.Batch))
{
if ((null == this.batchResponse) || (HttpStatusCode.NoContent == this.batchResponse.StatusCode))
{ // we always expect a response to our batch POST request
throw Error.InvalidOperation(Strings.Batch_ExpectedResponse(1));
}
headers = WebUtil.WrapResponseHeaders(this.batchResponse);
HandleResponse(
this.batchResponse.StatusCode, // statusCode
this.batchResponse.Headers[XmlConstants.HttpDataServiceVersion], // responseVersion
delegate() { return this.httpWebResponseStream; }, // getResponseStream
true); // throwOnFailure
if (!BatchStream.GetBoundaryAndEncodingFromMultipartMixedContentType(this.batchResponse.ContentType, out boundary, out encoding))
{
string mime;
Exception inner = null;
HttpProcessUtility.ReadContentType(this.batchResponse.ContentType, out mime, out encoding);
if (String.Equals(XmlConstants.MimeTextPlain, mime))
{
inner = GetResponseText(this.batchResponse.GetResponseStream, this.batchResponse.StatusCode);
}
throw Error.InvalidOperation(Strings.Batch_ExpectedContentType(this.batchResponse.ContentType), inner);
}
if (null == this.httpWebResponseStream)
{
Error.ThrowBatchExpectedResponse(InternalError.NullResponseStream);
}
this.DataServiceResponse = new DataServiceResponse(headers, (int)this.batchResponse.StatusCode, this.Responses, true /*batchResponse*/);
}
bool close = true;
BatchStream batchStream = null;
try
{
batchStream = this.responseBatchStream ?? new BatchStream(this.httpWebResponseStream, boundary, encoding, false);
this.httpWebResponseStream = null;
this.responseBatchStream = null;
IEnumerable responses = this.HandleBatchResponse(batchStream);
if (IsFlagSet(this.options, SaveChangesOptions.Batch) && (null != this.Queries))
{
// ExecuteBatch, EndExecuteBatch
close = false;
this.responseBatchStream = batchStream;
this.DataServiceResponse = new DataServiceResponse(
(Dictionary)this.DataServiceResponse.BatchHeaders,
this.DataServiceResponse.BatchStatusCode,
responses,
true /*batchResponse*/);
}
else
{ // SaveChanges, EndSaveChanges
// enumerate the entire response
foreach (ChangeOperationResponse response in responses)
{
if (exception == null && response.Error != null)
{
exception = response.Error;
}
}
}
}
finally
{
if (close && (null != batchStream))
{
batchStream.Close();
}
}
}
catch (InvalidOperationException ex)
{
exception = ex;
}
if (exception != null)
{
if (this.DataServiceResponse == null)
{
int statusCode = this.batchResponse == null ? (int)HttpStatusCode.InternalServerError : (int)this.batchResponse.StatusCode;
this.DataServiceResponse = new DataServiceResponse(headers, statusCode, null, IsFlagSet(this.options, SaveChangesOptions.Batch));
}
throw new DataServiceRequestException(Strings.DataServiceException_GeneralError, exception, this.DataServiceResponse);
}
}
///
/// process the batch changeset response
///
/// batch stream
/// enumerable of QueryResponse or null
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1506", Justification = "Central method of the API, likely to have many cross-references")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031", Justification = "Cache exception so user can examine it later")]
private IEnumerable HandleBatchResponse(BatchStream batch)
{
if (!batch.CanRead)
{
yield break;
}
string contentType;
string location;
string etag;
Uri editLink = null;
HttpStatusCode status;
int changesetIndex = 0;
int queryCount = 0;
int operationCount = 0;
this.entryIndex = 0;
while (batch.MoveNext())
{
var contentHeaders = batch.ContentHeaders; // get the headers before materialize clears them
Entry entry;
switch (batch.State)
{
#region BeginChangeSet
case BatchStreamState.BeginChangeSet:
if ((IsFlagSet(this.options, SaveChangesOptions.Batch) && (0 != changesetIndex)) ||
(!IsFlagSet(this.options, SaveChangesOptions.Batch) && (this.ChangedEntries.Count <= changesetIndex)) ||
(0 != operationCount))
{ // for now, we only send a single batch, single changeset
Error.ThrowBatchUnexpectedContent(InternalError.UnexpectedBeginChangeSet);
}
break;
#endregion
#region EndChangeSet
case BatchStreamState.EndChangeSet:
// move forward to next expected changelist
changesetIndex++;
operationCount = 0;
break;
#endregion
#region GetResponse
case BatchStreamState.GetResponse:
Debug.Assert(0 == operationCount, "missing an EndChangeSet 2");
contentHeaders.TryGetValue(XmlConstants.HttpContentType, out contentType);
status = (HttpStatusCode)(-1);
Exception ex = null;
QueryOperationResponse qresponse = null;
try
{
status = batch.GetStatusCode();
ex = HandleResponse(status, batch.GetResponseVersion(), batch.GetContentStream, false);
if (null == ex)
{
DataServiceRequest query = this.Queries[queryCount];
System.Collections.IEnumerable enumerable = query.Materialize(this.Context, contentType, batch.GetContentStream);
qresponse = QueryOperationResponse.GetInstance(query.ElementType, contentHeaders, query, enumerable);
}
}
catch (ArgumentException e)
{
ex = e;
}
catch (FormatException e)
{
ex = e;
}
catch (InvalidOperationException e)
{
ex = e;
}
if (null == qresponse)
{
DataServiceRequest query = this.Queries[queryCount];
qresponse = QueryOperationResponse.GetInstance(query.ElementType, contentHeaders, query, null);
qresponse.Error = ex;
}
qresponse.StatusCode = (int)status;
queryCount++;
yield return qresponse;
break;
#endregion
#region ChangeResponse
case BatchStreamState.ChangeResponse:
if (this.ChangedEntries.Count <= unchecked((uint)this.entryIndex))
{
Error.ThrowBatchUnexpectedContent(InternalError.TooManyBatchResponse);
}
HttpStatusCode statusCode = batch.GetStatusCode();
Exception error = HandleResponse(statusCode, batch.GetResponseVersion(), batch.GetContentStream, false);
int index = this.ValidateContentID(contentHeaders);
try
{
entry = this.ChangedEntries[index];
operationCount += this.Context.SaveResultProcessed(entry);
if (null != error)
{
throw error;
}
switch (entry.State)
{
#region Post
case EntityStates.Added:
if (entry.IsResource)
{
string mime = null;
Encoding postEncoding = null;
contentHeaders.TryGetValue(XmlConstants.HttpContentType, out contentType);
contentHeaders.TryGetValue(XmlConstants.HttpResponseLocation, out location);
contentHeaders.TryGetValue(XmlConstants.HttpResponseETag, out etag);
editLink = (null != location) ? Util.CreateUri(location, UriKind.Absolute) : null;
ResourceBox box = (ResourceBox)entry;
Stream stream = batch.GetContentStream();
if (null != stream)
{
HttpProcessUtility.ReadContentType(contentType, out mime, out postEncoding);
if (!String.Equals(XmlConstants.MimeApplicationAtom, mime, StringComparison.OrdinalIgnoreCase))
{
throw Error.InvalidOperation(Strings.Deserialize_UnknownMimeTypeSpecified(mime));
}
XmlReader reader = XmlUtil.CreateXmlReader(stream, postEncoding);
using (MaterializeAtom atom = new MaterializeAtom(this.Context, reader, box.Resource.GetType(), MergeOption.OverwriteChanges))
{
this.Context.HandleResponsePost(box, atom, editLink, etag);
}
}
else
{
if (null == editLink)
{
string entitySetName = box.Identity.OriginalString;
editLink = GenerateEditLinkUri(this.Context.baseUriWithSlash, entitySetName, box.Resource);
}
this.Context.HandleResponsePost(box, null, editLink, etag);
}
}
else
{
HandleResponsePost((RelatedEnd)entry);
}
break;
#endregion
#region Put, Merge
case EntityStates.Modified:
contentHeaders.TryGetValue(XmlConstants.HttpResponseETag, out etag);
HandleResponsePut(entry, etag);
break;
#endregion
#region Delete
case EntityStates.Deleted:
this.Context.HandleResponseDelete(entry);
break;
#endregion
}
}
catch (Exception e)
{
this.ChangedEntries[index].SaveError = e;
error = e;
}
ChangeOperationResponse changeOperationResponse = new ChangeOperationResponse(contentHeaders, BuildReturn(this.ChangedEntries[index]));
changeOperationResponse.StatusCode = (int)statusCode;
if (error != null)
{
changeOperationResponse.Error = error;
}
this.Responses.Add(changeOperationResponse);
operationCount++;
this.entryIndex++;
yield return changeOperationResponse;
break;
#endregion
default:
Error.ThrowBatchExpectedResponse(InternalError.UnexpectedBatchState);
break;
}
}
Debug.Assert(batch.State == BatchStreamState.EndBatch, "unexpected batch state");
// Check for a changeset without response (first line) or GET request without response (second line).
// either all saved entries must be processed or it was a batch and one of the entries has the error
if (((null == this.Queries) && ((0 == changesetIndex) ||
(0 < queryCount) ||
(this.ChangedEntries.Any(o => o.ContentGeneratedForSave != (0 != o.SaveResultWasProcessed)) &&
(!IsFlagSet(this.options, SaveChangesOptions.Batch) ||
(null == this.ChangedEntries.FirstOrDefault(o => (null != o.SaveError))))))) ||
((null != this.Queries) && (queryCount != this.Queries.Length)))
{
throw Error.InvalidOperation(Strings.Batch_IncompleteResponseCount);
}
batch.Dispose();
}
///
/// validate the content-id
///
/// headers
/// return the correct ChangedEntries index
private int ValidateContentID(Dictionary contentHeaders)
{
int contentID = 0;
string contentValueID;
if (!contentHeaders.TryGetValue(XmlConstants.HttpContentID, out contentValueID) ||
!Int32.TryParse(contentValueID, NumberStyles.Integer, NumberFormatInfo.InvariantInfo, out contentID))
{
Error.ThrowBatchUnexpectedContent(InternalError.ChangeResponseMissingContentID);
}
for (int i = 0; i < this.ChangedEntries.Count; ++i)
{
if (this.ChangedEntries[i].ChangeOrder == contentID)
{
return i;
}
}
Error.ThrowBatchUnexpectedContent(InternalError.ChangeResponseUnknownContentID);
return -1;
}
#endregion Batch
#region callback handlers
/// handle request.BeginGetRequestStream with request.EndGetRquestStream and then write out request stream
/// async result
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "required for this feature")]
private void AsyncEndGetRequestStream(IAsyncResult asyncResult)
{
PerRequest pereq = asyncResult.AsyncState as PerRequest;
try
{
int step = CompleteCheck(pereq, InternalError.InvalidEndGetRequestCompleted);
pereq.RequestCompletedSynchronously &= asyncResult.CompletedSynchronously; // BeginGetRequestStream
EqualRefCheck(this.request, pereq, InternalError.InvalidEndGetRequestStream);
HttpWebRequest httpWebRequest = Util.NullCheck(pereq.Request, InternalError.InvalidEndGetRequestStreamRequest);
Stream stream = Util.NullCheck(httpWebRequest.EndGetRequestStream(asyncResult), InternalError.InvalidEndGetRequestStreamStream);
pereq.RequestStream = stream;
MemoryStream memoryStream = Util.NullCheck(pereq.RequestStreamContent, InternalError.InvalidEndGetRequestStreamContent);
byte[] buffer = memoryStream.GetBuffer();
int bufferOffset = checked((int)memoryStream.Position);
int bufferLength = checked((int)memoryStream.Length) - bufferOffset;
if ((null == buffer) || (0 == bufferLength))
{
Error.ThrowInternalError(InternalError.InvalidEndGetRequestStreamContentLength);
}
// the following is useful in the debugging Immediate Window
// string x = System.Text.Encoding.UTF8.GetString(buffer, bufferOffset, bufferLength);
asyncResult = stream.BeginWrite(buffer, bufferOffset, bufferLength, this.AsyncEndWrite, pereq);
bool reallyCompletedSynchronously = asyncResult.CompletedSynchronously && (step < pereq.RequestStep);
pereq.RequestCompletedSynchronously &= reallyCompletedSynchronously; // BeginWrite
}
catch (Exception e)
{
if (this.HandleFailure(pereq, e))
{
throw;
}
}
finally
{
this.HandleCompleted(pereq);
}
}
/// handle reqestStream.BeginWrite with requestStream.EndWrite then BeginGetResponse
/// async result
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "required for this feature")]
private void AsyncEndWrite(IAsyncResult asyncResult)
{
PerRequest pereq = asyncResult.AsyncState as PerRequest;
try
{
int step = CompleteCheck(pereq, InternalError.InvalidEndWriteCompleted);
pereq.RequestCompletedSynchronously &= asyncResult.CompletedSynchronously; // BeginWrite
EqualRefCheck(this.request, pereq, InternalError.InvalidEndWrite);
HttpWebRequest httpWebRequest = Util.NullCheck(pereq.Request, InternalError.InvalidEndWriteRequest);
Stream stream = Util.NullCheck(pereq.RequestStream, InternalError.InvalidEndWriteStream);
stream.EndWrite(asyncResult);
pereq.RequestStream = null;
stream.Close();
stream = pereq.RequestStreamContent;
if (null != stream)
{
pereq.RequestStreamContent = null;
stream.Dispose();
}
asyncResult = httpWebRequest.BeginGetResponse(this.AsyncEndGetResponse, pereq);
bool reallyCompletedSynchronously = asyncResult.CompletedSynchronously && (step < pereq.RequestStep);
pereq.RequestCompletedSynchronously &= reallyCompletedSynchronously; // BeginGetResponse
}
catch (Exception e)
{
if (this.HandleFailure(pereq, e))
{
throw;
}
}
finally
{
this.HandleCompleted(pereq);
}
}
/// handle request.BeginGetResponse with request.EndGetResponse and then copy response stream
/// async result
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "required for this feature")]
private void AsyncEndGetResponse(IAsyncResult asyncResult)
{
PerRequest pereq = asyncResult.AsyncState as PerRequest;
try
{
int step = CompleteCheck(pereq, InternalError.InvalidEndGetResponseCompleted);
pereq.RequestCompletedSynchronously &= asyncResult.CompletedSynchronously; // BeginGetResponse
EqualRefCheck(this.request, pereq, InternalError.InvalidEndGetResponse);
HttpWebRequest httpWebRequest = Util.NullCheck(pereq.Request, InternalError.InvalidEndGetResponseRequest);
// the httpWebResponse is kept for batching, discarded by non-batch
HttpWebResponse response = null;
try
{
response = (HttpWebResponse)httpWebRequest.EndGetResponse(asyncResult);
}
catch (WebException e)
{
response = (HttpWebResponse)e.Response;
}
pereq.HttpWebResponse = Util.NullCheck(response, InternalError.InvalidEndGetResponseResponse);
if (!IsFlagSet(this.options, SaveChangesOptions.Batch))
{
this.HandleOperationResponse(httpWebRequest, response);
}
this.copiedContentLength = 0;
Stream stream = response.GetResponseStream();
pereq.ResponseStream = stream;
if ((null != stream) && stream.CanRead)
{
if (null != this.buildBatchWriter)
{
this.buildBatchWriter.Flush();
}
if (null == this.buildBatchBuffer)
{
this.buildBatchBuffer = new byte[8000];
}
bool reallyCompletedSynchronously = false;
do
{
asyncResult = stream.BeginRead(this.buildBatchBuffer, 0, this.buildBatchBuffer.Length, this.AsyncEndRead, pereq);
reallyCompletedSynchronously = asyncResult.CompletedSynchronously && (step < pereq.RequestStep);
pereq.RequestCompletedSynchronously &= reallyCompletedSynchronously; // BeginRead
}
while (reallyCompletedSynchronously && !pereq.RequestCompleted && !this.IsCompleted && stream.CanRead);
}
else
{
pereq.RequestCompleted = true;
// BeginGetResponse could fail and callback still invoked
if (!this.IsCompleted)
{
this.SaveNextChange(pereq);
}
}
}
catch (Exception e)
{
if (this.HandleFailure(pereq, e))
{
throw;
}
}
finally
{
this.HandleCompleted(pereq);
}
}
/// handle responseStream.BeginRead with responseStream.EndRead
/// async result
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "required for this feature")]
private void AsyncEndRead(IAsyncResult asyncResult)
{
PerRequest pereq = asyncResult.AsyncState as PerRequest;
int count = 0;
try
{
int step = CompleteCheck(pereq, InternalError.InvalidEndReadCompleted);
pereq.RequestCompletedSynchronously &= asyncResult.CompletedSynchronously; // BeginRead
EqualRefCheck(this.request, pereq, InternalError.InvalidEndRead);
Stream stream = Util.NullCheck(pereq.ResponseStream, InternalError.InvalidEndReadStream);
count = stream.EndRead(asyncResult);
if (0 < count)
{
Stream outputResponse = Util.NullCheck(this.httpWebResponseStream, InternalError.InvalidEndReadCopy);
outputResponse.Write(this.buildBatchBuffer, 0, count);
this.copiedContentLength += count;
if (!asyncResult.CompletedSynchronously && stream.CanRead)
{ // if CompletedSynchronously then caller will call and we reduce risk of stack overflow
bool reallyCompletedSynchronously = false;
do
{
asyncResult = stream.BeginRead(this.buildBatchBuffer, 0, this.buildBatchBuffer.Length, this.AsyncEndRead, pereq);
reallyCompletedSynchronously = asyncResult.CompletedSynchronously && (step < pereq.RequestStep);
pereq.RequestCompletedSynchronously &= reallyCompletedSynchronously; // BeginRead
}
while (reallyCompletedSynchronously && !pereq.RequestCompleted && !this.IsCompleted && stream.CanRead);
}
}
else
{
pereq.RequestCompleted = true;
// BeginRead could fail and callback still invoked
if (!this.IsCompleted)
{
this.SaveNextChange(pereq);
}
}
}
catch (Exception e)
{
if (this.HandleFailure(pereq, e))
{
throw;
}
}
finally
{
this.HandleCompleted(pereq);
}
}
/// continue with the next change
/// the completed per request object
private void SaveNextChange(PerRequest pereq)
{
Debug.Assert(this.executeAsync, "should be async");
if (!pereq.RequestCompleted)
{
Error.ThrowInternalError(InternalError.SaveNextChangeIncomplete);
}
++pereq.RequestStep;
EqualRefCheck(this.request, pereq, InternalError.InvalidSaveNextChange);
if (IsFlagSet(this.options, SaveChangesOptions.Batch))
{
this.httpWebResponseStream.Position = 0;
this.request = null;
this.IsCompleted = true;
}
else
{
if (0 == this.copiedContentLength)
{
this.HandleOperationResponseNoData();
}
this.HandleOperationEnd();
if (!this.procesingMediaLinkEntry)
{
this.changesCompleted++;
}
pereq.Dispose();
this.request = null;
if (!pereq.RequestCompletedSynchronously)
{ // you can't chain synchronously completed responses without risking StackOverflow, caller will loop to next
if (!this.IsCompleted)
{
this.BeginNextChange(IsFlagSet(this.options, SaveChangesOptions.ReplaceOnUpdate));
}
}
}
}
#endregion
/// wrap the full request
private sealed class PerRequest
{
/// ctor
internal PerRequest()
{
this.RequestCompletedSynchronously = true;
}
/// active web request
internal HttpWebRequest Request
{
get;
set;
}
/// active web request stream
internal Stream RequestStream
{
get;
set;
}
/// content to write to request stream
internal MemoryStream RequestStreamContent
{
get;
set;
}
/// web response
internal HttpWebResponse HttpWebResponse
{
get;
set;
}
/// async web response stream
internal Stream ResponseStream
{
get;
set;
}
/// did the request complete all of its steps synchronously?
internal bool RequestCompletedSynchronously
{
get;
set;
}
/// did the sequence (BeginGetRequest, EndGetRequest, ... complete
internal bool RequestCompleted
{
get;
set;
}
///
/// If CompletedSynchronously and requestStep didn't increment, then underlying implementation lied.
///
internal int RequestStep
{
get;
set;
}
///
/// dispose of the request object
///
internal void Dispose()
{
Stream stream;
if (null != (stream = this.ResponseStream))
{
this.ResponseStream = null;
stream.Dispose();
}
if (null != (stream = this.RequestStreamContent))
{
this.RequestStreamContent = null;
stream.Dispose();
}
if (null != (stream = this.RequestStream))
{
this.RequestStream = null;
stream.Dispose();
}
HttpWebResponse response = this.HttpWebResponse;
if (null != response)
{
response.Close();
}
this.Request = null;
this.RequestCompleted = true;
}
}
}
}
}
// File provided for Reference Use Only by Microsoft Corporation (c) 2007.