Code:
/ Net / Net / 3.5.50727.3053 / DEVDIV / depot / DevDiv / releases / Orcas / SP / ndp / fx / src / DataWeb / Server / System / Data / Services / DataService.cs / 4 / DataService.cs
//----------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//
// Provides a base class for DataWeb services.
//
//
// @owner [....]
//---------------------------------------------------------------------
namespace System.Data.Services
{
#region Namespaces.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Objects;
using System.Data.Services.Caching;
using System.Data.Services.Providers;
using System.Data.Services.Serializers;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Channels;
using System.Text;
#endregion Namespaces.
///
/// Represents a strongly typed service that can process data-oriented
/// resource requests.
///
/// The type of the store to provide resources.
///
/// will typically be a subtype of
/// System.Data.Object.ObjectContext or another class that provides IQueryable
/// properties.
///
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class DataService : IRequestHandler, IDataService
{
#region Private fields.
/// A delegate used to create an instance of the data context.
private static Func cachedConstructor;
/// Service configuration information.
private DataServiceConfiguration configuration;
/// Host implementation for this data service.
private IDataServiceHost host;
/// Data provider for this data service.
private IDataServiceProvider provider;
/// Cached request headers.
private CachedRequestParams requestParams;
/// dummy data service for batch requests.
private BatchDataService batchDataService;
#endregion Private fields.
#region Properties.
/// Service configuration information.
DataServiceConfiguration IDataService.Configuration
{
[DebuggerStepThrough]
get { return this.configuration; }
}
/// Host implementation for this data service
IDataServiceHost IDataService.Host
{
[DebuggerStepThrough]
get { return this.host; }
}
/// Data provider for this data service
IDataServiceProvider IDataService.Provider
{
[DebuggerStepThrough]
get
{
Debug.Assert(this.provider != null, "this.provider != null -- otherwise EnsureProviderAndConfigForRequest didn't");
return this.provider;
}
}
/// Returns the instance of data service.
IDataService IDataService.Instance
{
[DebuggerStepThrough]
get { return this; }
}
/// Cached request headers.
CachedRequestParams IDataService.RequestParams
{
[DebuggerStepThrough]
get { return this.requestParams; }
}
/// The data source used in the current request processing.
protected T CurrentDataSource
{
get { return (T)this.provider.CurrentDataSource; }
}
#endregion Properties.
#region Public / interface methods.
///
/// This method is called during query processing to validate and customize
/// paths for the $expand options are applied by the provider.
///
/// Query which will be composed.
/// Collection of segment paths to be expanded.
void IDataService.InternalApplyingExpansions(IQueryable queryable, ICollection expandPaths)
{
Debug.Assert(queryable != null, "queryable != null");
Debug.Assert(expandPaths != null, "expandPaths != null");
Debug.Assert(this.configuration != null, "this.configuration != null");
// Check the expand depth and count.
int actualExpandDepth = 0;
int actualExpandCount = 0;
foreach (ExpandSegmentCollection collection in expandPaths)
{
int segmentDepth = collection.Count;
if (segmentDepth > actualExpandDepth)
{
actualExpandDepth = segmentDepth;
}
actualExpandCount += segmentDepth;
}
if (this.configuration.MaxExpandDepth < actualExpandDepth)
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_ExpandDepthExceeded(actualExpandDepth, this.configuration.MaxExpandDepth));
}
if (this.configuration.MaxExpandCount < actualExpandCount)
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_ExpandCountExceeded(actualExpandCount, this.configuration.MaxExpandCount));
}
}
/// Processes a catchable exception.
/// The arguments describing how to handle the exception.
void IDataService.InternalHandleException(HandleExceptionArgs args)
{
Debug.Assert(args != null, "args != null");
try
{
this.HandleException(args);
}
catch (Exception handlingException)
{
if (!WebUtil.IsCatchableExceptionType(handlingException))
{
throw;
}
args.Exception = handlingException;
}
}
///
/// Returns the segmentInfo of the resource referred by the given content Id;
///
/// content id for a operation in the batch request.
/// segmentInfo for the resource referred by the given content id.
SegmentInfo IDataService.GetSegmentForContentId(string contentId)
{
return null;
}
///
/// Get the resource referred by the segment in the request with the given index
///
/// description about the request url.
/// index of the segment that refers to the resource that needs to be returned.
/// typename of the resource.
/// the resource as returned by the provider.
object IDataService.GetResource(RequestDescription description, int segmentIndex, string typeFullName)
{
Debug.Assert(description.SegmentInfos[segmentIndex].RequestEnumerable != null, "requestDescription.SegmentInfos[segmentIndex].RequestEnumerable != null");
return Deserializer.GetResource(description.SegmentInfos[segmentIndex], typeFullName, ((IDataService)this), false /*checkForNull*/);
}
/// Disposes the data source of the current if necessary.
///
/// Because the provider has affinity with a specific data source
/// (which is created and set by the DataService), we set
/// the provider to null so we remember to re-create it if the
/// service gets reused for a different request.
///
void IDataService.DisposeDataSource()
{
if (this.provider != null)
{
this.provider.DisposeDataSource();
this.provider = null;
}
}
///
/// This method is called before a request is processed.
///
/// Information about the request that is going to be processed.
void IDataService.InternalOnStartProcessingRequest(ProcessRequestArgs args)
{
this.OnStartProcessingRequest(args);
}
/// Attaches the specified host to this service.
/// Host for service to interact with.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "host", Justification = "Makes 1:1 argument-to-field correspondence obvious.")]
public void AttachHost(IDataServiceHost host)
{
WebUtil.CheckArgumentNull(host, "host");
this.host = host;
}
/// Processes the specified .
/// with message body to process.
/// The response .
public Message ProcessRequestForMessage(Stream messageBody)
{
WebUtil.CheckArgumentNull(messageBody, "messageBody");
HttpContextServiceHost httpHost = new HttpContextServiceHost(messageBody);
this.AttachHost(httpHost);
bool shouldDispose = true;
try
{
this.EnsureProviderAndConfigForRequest();
Action writer = this.HandleRequest();
Debug.Assert(writer != null, "writer != null");
Message result = CreateMessage(MessageVersion.None, "", ((IDataServiceHost)httpHost).ResponseContentType, writer, this);
shouldDispose = false;
return result;
}
finally
{
if (shouldDispose)
{
((IDataService)this).DisposeDataSource();
}
}
}
/// Provides a host-agnostic entry point for request processing.
public void ProcessRequest()
{
if (this.host == null)
{
throw new InvalidOperationException(Strings.DataService_HostNotAttached);
}
try
{
this.EnsureProviderAndConfigForRequest();
Action writer = this.HandleRequest();
if (writer != null)
{
writer(this.host.ResponseStream);
}
}
finally
{
((IDataService)this).DisposeDataSource();
}
}
#endregion Public / interface methods.
#region Protected methods.
/// Initializes a new data source instance.
/// A new data source instance.
///
/// The default implementation uses a constructor with no parameters
/// to create a new instance.
///
/// The instance will only be used for the duration of a single
/// request, and will be disposed after the request has been
/// handled.
///
protected virtual T CreateDataSource()
{
if (cachedConstructor == null)
{
Type dataContextType = typeof(T);
if (dataContextType.IsAbstract)
{
throw new InvalidOperationException(
Strings.DataService_ContextTypeIsAbstract(dataContextType, this.GetType()));
}
cachedConstructor = (Func)WebUtil.CreateNewInstanceConstructor(dataContextType, null, dataContextType);
}
return cachedConstructor();
}
/// Handles an exception thrown while processing a request.
/// Arguments to the exception.
protected virtual void HandleException(HandleExceptionArgs args)
{
WebUtil.CheckArgumentNull(args, "arg");
Debug.Assert(args.Exception != null, "args.Exception != null -- .ctor should have checked");
}
///
/// This method is called before processing each request. For batch requests
/// it is called once for the top batch request and once for each operation
/// in the batch.
///
/// args containing information about the request.
protected virtual void OnStartProcessingRequest(ProcessRequestArgs args)
{
// Do nothing. Application writers can override this and look
// at the request args and do some processing.
}
#endregion Protected methods.
#region Private methods.
/// Checks that the specified has a known version.
/// Service to check.
private static void CheckVersion(IDataService service)
{
Debug.Assert(service != null, "service != null");
Debug.Assert(service.RequestParams != null, "service.RequestParams != null");
// Check that the request/payload version is understood.
string versionText = service.RequestParams.Version;
if (!String.IsNullOrEmpty(versionText))
{
KeyValuePair version;
if (!HttpProcessUtility.TryReadVersion(versionText, out version))
{
throw DataServiceException.CreateBadRequestError(
Strings.DataService_VersionCannotBeParsed(versionText));
}
// Currently we only recognize an exact match. In future versions
// we may choose to allow different major/minor combinations.
if (version.Key.Major != XmlConstants.DataServiceVersionCurrentMajor ||
version.Key.Minor != XmlConstants.DataServiceVersionCurrentMinor)
{
string message = Strings.DataService_VersionNotSupported(
version.Key.ToString(2),
XmlConstants.DataServiceVersionCurrentMajor,
XmlConstants.DataServiceVersionCurrentMinor);
throw DataServiceException.CreateBadRequestError(message);
}
}
// Check that the maximum version for the client will understand our response.
versionText = service.RequestParams.MaxVersion;
if (!String.IsNullOrEmpty(versionText))
{
KeyValuePair version;
if (!HttpProcessUtility.TryReadVersion(versionText, out version))
{
throw DataServiceException.CreateBadRequestError(
Strings.DataService_VersionCannotBeParsed(versionText));
}
if (version.Key.Major < XmlConstants.DataServiceVersionCurrentMajor ||
(version.Key.Major == XmlConstants.DataServiceVersionCurrentMajor &&
version.Key.Minor < XmlConstants.DataServiceVersionCurrentMinor))
{
string message = Strings.DataService_VersionTooLow(
version.Key.ToString(2),
XmlConstants.DataServiceVersionCurrentMajor,
XmlConstants.DataServiceVersionCurrentMinor);
throw DataServiceException.CreateBadRequestError(message);
}
}
}
///
/// Checks that if etag values are specified in the header, they must be valid.
///
/// header values.
private static void CheckETagValues(CachedRequestParams requestParams)
{
Debug.Assert(requestParams != null, "requestParams != null");
if (!IsETagValueValid(requestParams.IfMatch))
{
throw DataServiceException.CreateBadRequestError(Strings.DataServiceException_GeneralError);
}
if (!IsETagValueValid(requestParams.IfNoneMatch))
{
throw DataServiceException.CreateBadRequestError(Strings.DataServiceException_GeneralError);
}
}
///
/// Returns false if the given etag value is not valid.
/// Look in http://www.ietf.org/rfc/rfc2616.txt?number=2616 (Section 14.26) for more information
///
/// etag value to be checked.
/// returns true if the etag value is valid, otherwise returns false.
private static bool IsETagValueValid(string etag)
{
if (String.IsNullOrEmpty(etag) || etag == XmlConstants.HttpAnyETag)
{
return true;
}
if (etag.Length <= 4 || etag[0] != 'W' || etag[1] != '/' || etag[2] != '"' || etag[etag.Length - 1] != '"')
{
return false;
}
for (int i = 3; i < etag.Length - 1; i++)
{
// Format of etag looks something like: W/"etag property values"
// according to HTTP RFC 2616, if someone wants to specify more than 1 etag value,
// then need to specify something like this: W/"etag values", W/"etag values", ...
// To make sure only one etag is specified, we need to ensure that
// only the third and last characters are quotes.
// If " is part of the key value, it needs to be escaped.
if (etag[i] == '"')
{
return false;
}
}
return true;
}
///
/// Creates a that invokes the specified
/// callback to write its body.
///
/// Version for message.
/// Action for message.
/// MIME content type for body.
/// Callback.
/// Service with context to dispose once the response has been written.
/// A new .
private static Message CreateMessage(MessageVersion version, string action, string contentType, Action writer, IDataService service)
{
Debug.Assert(version != null, "version != null");
Debug.Assert(writer != null, "writer != null");
Debug.Assert(service != null, "service != null");
DelegateBodyWriter bodyWriter = new DelegateBodyWriter(writer, service);
Message message = Message.CreateMessage(version, action, bodyWriter);
message.Properties.Add(WebBodyFormatMessageProperty.Name, new WebBodyFormatMessageProperty(WebContentFormat.Raw));
HttpResponseMessageProperty response = new HttpResponseMessageProperty();
response.Headers[System.Net.HttpResponseHeader.ContentType] = contentType;
message.Properties.Add(HttpResponseMessageProperty.Name, response);
return message;
}
/// Creates a configuration for the specified service.
/// Type of DataService with authorization methods.
/// Provider with metadata.
/// Instance of the data source for the provider.
///
/// A (possibly shared) configuration implementation for the specified service.
///
private static DataServiceConfiguration CreateConfiguration(Type dataServiceType, IDataServiceProvider provider, object dataSourceInstance)
{
Debug.Assert(dataServiceType != null, "dataServiceType != null");
DataServiceConfiguration result = new DataServiceConfiguration(provider);
result.InvokeStaticInitialization(dataServiceType);
result.RegisterCallbacks(dataServiceType);
result.ApplyToProvider(dataSourceInstance);
return result;
}
///
/// Creates a provider implementation that wraps the T type.
///
/// Type of DataService with service operations.
/// Instance of the data source for the provider.
/// Service configuration information.
///
/// A (possibly shared) provider implementation that wraps the T type.
///
private static IDataServiceProvider CreateProvider(Type dataServiceType, object dataSourceInstance, out DataServiceConfiguration configuration)
{
Debug.Assert(dataServiceType != null, "dataServiceType != null");
Debug.Assert(dataSourceInstance != null, "dataSourceInstance != null");
Type dataContextType = typeof(T);
Debug.Assert(
dataContextType.IsAssignableFrom(dataSourceInstance.GetType()),
"dataContextType.IsAssignableFrom(dataSourceInstance.GetType()) -- otherwise the wrong data source instance was created.");
MetadataCacheItem metadata = MetadataCache.TryLookup(dataServiceType, dataSourceInstance);
bool metadataRequiresInitialization = metadata == null;
if (metadataRequiresInitialization)
{
metadata = new MetadataCacheItem(dataContextType);
}
BaseServiceProvider result;
if (typeof(ObjectContext).IsAssignableFrom(dataContextType))
{
result = new ObjectContextServiceProvider(metadata, dataSourceInstance);
}
else
{
result = new ReflectionServiceProvider(metadata, dataSourceInstance);
}
if (metadataRequiresInitialization)
{
// Populate metadata in provider.
result.PopulateMetadata();
result.AddOperationsFromType(dataServiceType);
// Create and cache configuration, which goes hand-in-hand with metadata.
metadata.Configuration = CreateConfiguration(dataServiceType, result, dataSourceInstance);
metadata.Seal();
MetadataCache.AddCacheItem(dataServiceType, dataSourceInstance, metadata);
}
configuration = metadata.Configuration;
return (IDataServiceProvider)result;
}
///
/// Gets the appropriate encoding specified by the request, taking
/// the format into consideration.
///
/// Content format for response.
/// Accept-Charset header as specified in request.
/// The requested encoding, possibly null.
private static Encoding GetRequestAcceptEncoding(ContentFormat responseFormat, string acceptCharset)
{
if (responseFormat == ContentFormat.Binary)
{
return null;
}
else
{
return HttpProcessUtility.EncodingFromAcceptCharset(acceptCharset);
}
}
///
/// Selects a response format for the host's request and sets the
/// appropriate response header.
///
/// Host with request.
/// An comma-delimited list of client-supported MIME accept types.
/// Whether the target is an entity.
/// The selected response format.
private static ContentFormat SelectResponseFormat(IDataServiceHost host, string acceptTypesText, bool entityTarget)
{
Debug.Assert(host != null, "host != null");
string[] availableTypes;
if (entityTarget)
{
availableTypes = new string[]
{
XmlConstants.MimeApplicationAtom,
XmlConstants.MimeApplicationJson
};
}
else
{
availableTypes = new string[]
{
XmlConstants.MimeApplicationXml,
XmlConstants.MimeTextXml,
XmlConstants.MimeApplicationJson
};
}
string mime = HttpProcessUtility.SelectMimeType(acceptTypesText, availableTypes);
if (mime == null)
{
return ContentFormat.Unsupported;
}
else
{
host.ResponseContentType = mime;
return GetContentFormat(mime);
}
}
/// Validate the given request.
/// Request parameters.
private static void ValidateRequest(CachedRequestParams requestParams)
{
if (!String.IsNullOrEmpty(requestParams.IfMatch) && !String.IsNullOrEmpty(requestParams.IfNoneMatch))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_BothIfMatchAndIfNoneMatchHeaderSpecified);
}
}
///
/// Processes the incoming request, without writing anything to the response body.
///
/// description about the request uri
/// data service to which the request was made.
///
/// A delegate to be called to write the body; null if no body should be written out.
///
private static RequestDescription ProcessIncomingRequest(
RequestDescription description,
IDataService dataService)
{
Debug.Assert(description != null, "description != null");
Debug.Assert(dataService.Host != null, "dataService.Host != null");
CachedRequestParams requestParams = dataService.RequestParams;
CheckVersion(dataService);
CheckETagValues(dataService.RequestParams);
ResourceContainer lastSegmentContainer = description.LastSegmentInfo.TargetContainer;
if (requestParams.AstoriaHttpVerb == AstoriaVerbs.GET)
{
if (lastSegmentContainer != null)
{
dataService.Configuration.CheckResourceRightsForRead(lastSegmentContainer, description.IsSingleResult);
}
}
else if (description.TargetKind == RequestTargetKind.ServiceDirectory)
{
throw DataServiceException.CreateMethodNotAllowed(
Strings.DataService_OnlyGetOperationSupportedOnServiceUrl,
XmlConstants.HttpMethodGet);
}
int statusCode = 200;
bool shouldWriteBody = true;
RequestDescription newDescription = description;
if (description.TargetSource != RequestTargetSource.ServiceOperation)
{
if (requestParams.AstoriaHttpVerb == AstoriaVerbs.POST)
{
newDescription = HandlePostOperation(description, dataService);
if (description.LinkUri)
{
statusCode = 204; // 204 - No Content
shouldWriteBody = false;
}
else
{
statusCode = 201; // 201 - Created.
}
}
else if (requestParams.AstoriaHttpVerb == AstoriaVerbs.PUT ||
requestParams.AstoriaHttpVerb == AstoriaVerbs.MERGE)
{
if (lastSegmentContainer != null)
{
if (requestParams.AstoriaHttpVerb == AstoriaVerbs.PUT)
{
dataService.Configuration.CheckResourceRights(lastSegmentContainer, EntitySetRights.WriteReplace);
}
else
{
dataService.Configuration.CheckResourceRights(lastSegmentContainer, EntitySetRights.WriteMerge);
}
}
// For PUT, the body itself shouldn't be written, but the etag should (unless it's just a link).
shouldWriteBody = !description.LinkUri;
newDescription = HandlePutOperation(description, dataService);
statusCode = 204; // 204 - No Content
}
else if (requestParams.AstoriaHttpVerb == AstoriaVerbs.DELETE)
{
if (lastSegmentContainer != null)
{
dataService.Configuration.CheckResourceRights(lastSegmentContainer, EntitySetRights.WriteDelete);
}
HandleDeleteOperation(description, dataService);
statusCode = 204; // 204 - No Content
shouldWriteBody = false;
}
}
else if (description.TargetKind == RequestTargetKind.VoidServiceOperation)
{
statusCode = 204; // No Content
shouldWriteBody = false;
}
// Set the caching policy appropriately - for the time being, we disable caching.
dataService.Host.ResponseCacheControl = XmlConstants.HttpCacheControlNoCache;
// Always set the version when a payload will be returned, in case other
// headers include links, which may need to be interpreted under version-specific rules.
dataService.Host.ResponseVersion = XmlConstants.DataServiceVersionCurrent;
dataService.Host.ResponseStatusCode = statusCode;
if (shouldWriteBody)
{
dataService.Host.ResponseVersion = XmlConstants.DataServiceVersionCurrent;
// return the description, only if response or something in the response header needs to be written
// for e.g. in PUT operations, we need to write etag to the response header, and
// we can compute the new etag only after we have called save changes.
return newDescription;
}
else
{
return null;
}
}
/// Serializes the results for a request into the body of a response message.
/// Description of the data requested.
/// data service to which the request was made.
/// A delegate that can serialize the body into an IEnumerable.
private static Action SerializeResponseBody(RequestDescription description, IDataService dataService)
{
Debug.Assert(dataService.Provider != null, "dataService.Provider != null");
Debug.Assert(dataService.Host != null, "dataService.Host != null");
CachedRequestParams requestParams = dataService.RequestParams;
// Handle internal system resources.
Action result = HandleInternalResources(description, dataService);
if (result != null)
{
return result;
}
// ETags are not supported if there are more than one resource expected in the response.
if (!description.IsSingleResult || (description.ExpandPaths != null && description.ExpandPaths.Count != 0))
{
if (!String.IsNullOrEmpty(requestParams.IfMatch) || !String.IsNullOrEmpty(requestParams.IfNoneMatch))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_ETagSpecifiedForCollection(requestParams.AbsoluteRequestUri));
}
}
if (requestParams.AstoriaHttpVerb == AstoriaVerbs.PUT ||
requestParams.AstoriaHttpVerb == AstoriaVerbs.MERGE)
{
ResourceContainer container;
object actualEntity = GetContainerAndActualEntityInstance(dataService.Provider, description, out container);
dataService.Host.ResponseETag = WebUtil.GetETagValue(dataService.Provider, actualEntity, container);
return EmptyStreamWriter;
}
// Pick the content format to be used to serialize the body.
Debug.Assert(description.RequestEnumerable != null, "description.RequestEnumerable != null");
ContentFormat responseFormat = SelectResponseFormatForType(
description.LinkUri ? RequestTargetKind.Link : description.TargetKind,
description.TargetElementType,
requestParams.Accept,
description.MimeType,
dataService.Host);
// check for etags first
// If no etag is specified, then do the normal stuff - run the query and serialize the result
if (description.TargetSource == RequestTargetSource.ServiceOperation ||
description.TargetSource == RequestTargetSource.None ||
!description.IsSingleResult)
{
Debug.Assert(
String.IsNullOrEmpty(requestParams.IfMatch) && String.IsNullOrEmpty(requestParams.IfNoneMatch),
"No etag can be specified for collection");
Encoding encoding = GetRequestAcceptEncoding(responseFormat, requestParams.AcceptCharset);
IEnumerator queryResults = WebUtil.GetRequestEnumerator(description.RequestEnumerable);
try
{
bool hasMoved = queryResults.MoveNext();
// If we had to wait until we got a value to determine the valid contents, try that now.
#if ASTORIA_OPEN_OBJECT
if (responseFormat == ContentFormat.Unknown)
{
responseFormat = ResolveUnknownFormat(description, queryResults.Current, dataService);
}
#else
Debug.Assert(responseFormat != ContentFormat.Unknown, "responseFormat != ContentFormat.Unknown");
#endif
dataService.Host.ResponseContentType = HttpProcessUtility.BuildContentType(dataService.Host.ResponseContentType, encoding);
return new ResponseBodyWriter(encoding, hasMoved, dataService, queryResults, description, responseFormat).Write;
}
catch
{
WebUtil.Dispose(queryResults);
throw;
}
}
else
{
return CompareETagAndWriteResponse(description, responseFormat, dataService);
}
}
/// Selects the correct content format for a given resource type.
/// Target resource to return.
/// CLR element type.
/// Accept header value.
/// Required MIME type.
/// Host implementation for this data service.
///
/// The content format for the resource; Unknown if it cannot be determined statically.
///
private static ContentFormat SelectResponseFormatForType(
RequestTargetKind targetKind,
Type elementType,
string acceptTypesText,
string mimeType,
IDataServiceHost host)
{
ContentFormat responseFormat;
if (targetKind == RequestTargetKind.PrimitiveValue)
{
responseFormat = SelectPrimitiveContentType(elementType, acceptTypesText, mimeType, host);
}
#if ASTORIA_OPEN_OBJECT
else if (targetKind != RequestTargetKind.OpenPropertyValue && targetKind != RequestTargetKind.OpenProperty)
#else
else
#endif
{
bool entityTarget = targetKind == RequestTargetKind.Resource;
responseFormat = SelectResponseFormat(host, acceptTypesText, entityTarget);
if (responseFormat == ContentFormat.Unsupported)
{
throw new DataServiceException(415, Strings.DataServiceException_UnsupportedMediaType);
}
}
#if ASTORIA_OPEN_OBJECT
else
{
// We cannot negotiate a format until we know what the value is for the object.
responseFormat = ContentFormat.Unknown;
}
#endif
return responseFormat;
}
/// Selects the correct content format for a primitive type.
/// CLR element type.
/// Accept header value.
/// Required MIME type, possibly null.
/// Host implementation for this data service.
/// The content format for the resource.
private static ContentFormat SelectPrimitiveContentType(Type targetElementType, string acceptTypesText, string requiredContentType, IDataServiceHost host)
{
Debug.Assert(targetElementType != null, "targetElementType != null");
string contentType;
ContentFormat responseFormat = WebUtil.GetResponseFormatForPrimitiveValue(targetElementType, out contentType);
requiredContentType = requiredContentType ?? contentType;
host.ResponseContentType = HttpProcessUtility.SelectRequiredMimeType(
acceptTypesText, // acceptTypesText
new string[] { requiredContentType }, // exactContentType
requiredContentType); // inexactContentType
return responseFormat;
}
/// Handles POST requests.
/// description about the target request
/// data service to which the request was made.
/// a new request description object, containing information about the response payload
private static RequestDescription HandlePostOperation(RequestDescription description, IDataService dataService)
{
Debug.Assert(
description.TargetSource != RequestTargetSource.ServiceOperation,
"TargetSource != ServiceOperation -- should have been handled in request URI processing");
CachedRequestParams requestParams = dataService.RequestParams;
if (!String.IsNullOrEmpty(requestParams.IfMatch) || !String.IsNullOrEmpty(requestParams.IfNoneMatch))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_ETagSpecifiedForPost);
}
if (description.IsSingleResult)
{
throw DataServiceException.CreateMethodNotAllowed(
Strings.BadRequest_InvalidUriForPostOperation(requestParams.AbsoluteRequestUri),
dataService.Configuration.GetAllowedMethods(description));
}
Debug.Assert(
description.TargetSource == RequestTargetSource.EntitySet ||
#if ASTORIA_OPEN_OBJECT
description.TargetKind == RequestTargetKind.OpenProperty ||
#endif
description.Property.Kind == ResourcePropertyKind.ResourceSetReference,
"Only ways to have collections of resources");
Stream requestStream = dataService.Host.RequestStream;
if (requestStream == null)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_NullRequestStream);
}
string mimeType;
System.Text.Encoding encoding;
HttpProcessUtility.ReadContentType(dataService.Host.RequestContentType, out mimeType, out encoding);
ContentFormat requestFormat = WebUtil.SelectRequestFormat(mimeType, description);
object entity = null;
Deserializer deserializer = null;
try
{
switch (requestFormat)
{
case ContentFormat.Json:
StreamReader streamReader = new StreamReader(requestStream, encoding);
deserializer = new JsonDeserializer(
streamReader,
false /*update*/,
dataService,
UpdateTracker.CreateUpdateTracker(description, dataService.Provider));
break;
case ContentFormat.Atom:
SyndicationFormatterFactory factory = new Atom10FormatterFactory();
deserializer = new SyndicationDeserializer(
requestStream, // stream
encoding, // encoding
dataService, // dataService
false, // update
factory,
UpdateTracker.CreateUpdateTracker(description, dataService.Provider)); // factory
break;
case ContentFormat.PlainXml:
deserializer = new PlainXmlDeserializer(
requestStream,
encoding,
dataService,
false /*update*/,
UpdateTracker.CreateUpdateTracker(description, dataService.Provider));
break;
default:
throw new DataServiceException(415, Strings.BadRequest_UnsupportedMediaForPost(mimeType));
}
Debug.Assert(deserializer != null, "deserializer != null");
entity = deserializer.HandlePostRequest(description);
Debug.Assert(entity != null, "entity != null");
if (deserializer.Tracker != null)
{
deserializer.Tracker.FireNotifications(dataService.Instance);
}
return RequestDescription.CreateSingleResultRequestDescription(
description, entity, description.LastSegmentInfo.TargetContainer);
}
finally
{
WebUtil.Dispose(deserializer);
}
}
/// Handles PUT requests.
/// description about the target request
/// data service to which the request was made.
/// new request description which contains the info about the entity resource getting modified.
private static RequestDescription HandlePutOperation(RequestDescription description, IDataService dataService)
{
Debug.Assert(description.TargetSource != RequestTargetSource.ServiceOperation, "description.TargetSource != RequestTargetSource.ServiceOperation");
if (!description.IsSingleResult)
{
throw DataServiceException.CreateMethodNotAllowed(
Strings.BadRequest_InvalidUriForPutOperation(dataService.RequestParams.AbsoluteRequestUri),
dataService.Configuration.GetAllowedMethods(description));
}
if (!String.IsNullOrEmpty(dataService.RequestParams.IfNoneMatch))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_IfNoneMatchHeaderNotSupportedInPut);
}
else if (description.LinkUri && !String.IsNullOrEmpty(dataService.RequestParams.IfMatch))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_ETagsNotAllowedForLinkOperations);
}
else if (description.Property != null && description.Property.IsOfKind(ResourcePropertyKind.Key))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_CannotUpdateKeyProperties(description.Property.Name));
}
Stream requestStream = dataService.Host.RequestStream;
if (requestStream == null)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_NullRequestStream);
}
return Deserializer.HandlePutRequest(description, dataService, requestStream);
}
/// Handles DELETE requests.
/// description about the target request
/// data service to which the request was made.
private static void HandleDeleteOperation(RequestDescription description, IDataService dataService)
{
Debug.Assert(description != null, "description != null");
Debug.Assert(description.TargetSource != RequestTargetSource.ServiceOperation, "description.TargetSource != RequestTargetSource.ServiceOperation");
Debug.Assert(dataService != null, "dataService != null");
Debug.Assert(dataService.Configuration != null, "dataService.Configuration != null");
Debug.Assert(dataService.RequestParams != null, "dataService.RequestParams != null");
// In general, deletes are only supported on resource referred via top level sets or collection properties.
// If its the open property case, the key must be specified
// or you can unbind relationships using delete
if (description.LinkUri)
{
HandleUnbindOperation(description, dataService);
}
else if (
#if ASTORIA_OPEN_OBJECT
(description.TargetKind == RequestTargetKind.OpenProperty) ||
#endif
(description.IsSingleResult && description.TargetKind == RequestTargetKind.Resource))
{
#if ASTORIA_OPEN_OBJECT
Debug.Assert(
description.LastSegmentInfo.TargetContainer != null || description.TargetKind == RequestTargetKind.OpenProperty,
"description.LastSegmentInfo.TargetContainer != null || TargetKind == OpenProperty");
#endif
if (description.RequestEnumerable == null)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_ResourceCanBeCrossReferencedOnlyForBindOperation);
}
// Get the single entity result
// We have to query for the delete case, since we don't know the type of the resource
object entity = Deserializer.GetResource(description.LastSegmentInfo, null, dataService, true /*checkForNull*/);
ResourceContainer container = description.LastSegmentInfo.TargetContainer;
//
object actualEntity = dataService.Provider.ResolveResource(entity);
// For open properties, we need to make sure that they refer to resource types
#if ASTORIA_OPEN_OBJECT
if (description.TargetKind == RequestTargetKind.OpenProperty)
{
// Verify that the resource is an entity type. Otherwise we need to throw
ResourceType resourceType = dataService.Provider.GetResourceType(actualEntity.GetType());
if (resourceType == null || resourceType.ResourceTypeKind != ResourceTypeKind.EntityType)
{
throw DataServiceException.CreateBadRequestError(
Strings.DataService_TypeNotValidForDeleteOperation(dataService.RequestParams.AbsoluteRequestUri));
}
container = dataService.Provider.GetContainerForResourceType(resourceType.Type);
}
#endif
if (description.Property != null)
{
Debug.Assert(container != null, "container != null");
dataService.Configuration.CheckResourceRights(container, EntitySetRights.WriteDelete);
}
CheckForETagInDeleteOperation(actualEntity, entity, container, dataService.RequestParams, dataService.Provider);
dataService.Provider.DeleteResource(entity);
//
#if ASTORIA_OPEN_OBJECT
if (description.TargetKind != RequestTargetKind.OpenProperty)
#endif
{
UpdateTracker.FireNotification(dataService.Instance, actualEntity, container, UpdateOperations.Delete);
}
}
else if (description.TargetKind == RequestTargetKind.PrimitiveValue)
{
Debug.Assert(description.TargetSource == RequestTargetSource.Property, "description.TargetSource == RequestTargetSource.Property");
Debug.Assert(description.IsSingleResult, "description.IsSingleResult");
if (description.Property != null && description.Property.IsOfKind(ResourcePropertyKind.Key))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_CannotUpdateKeyProperties(description.Property.Name));
}
else if (description.Property.Type.IsValueType)
{
// 403 - Forbidden
throw new DataServiceException(403, Strings.BadRequest_CannotNullifyValueTypeProperty);
}
// We have to issue the query to get the resource
object securityResource; // Resource on which security check can be made (possibly entity parent of 'resource').
ResourceContainer container; // Resource Container to which the parent entity belongs to.
object resource = Deserializer.GetResourceToModify(description, dataService, false /*allowCrossReference*/, out securityResource, out container);
//
object actualEntity = dataService.Provider.ResolveResource(securityResource);
CheckForETagInDeleteOperation(actualEntity, securityResource, container, dataService.RequestParams, dataService.Provider);
// Doesn't matter which content format we pass here, since the value we are setting to is null
Deserializer.ModifyResource(description, resource, null, ContentFormat.Text, dataService.Provider);
UpdateTracker.FireNotification(dataService.Instance, actualEntity, container, UpdateOperations.Change);
}
#if ASTORIA_OPEN_OBJECT
else if (description.TargetKind == RequestTargetKind.OpenPropertyValue)
{
object securityResource;
object resource = Deserializer.GetResourceToModify(description, dataService, out securityResource);
//
object actualEntity = dataService.Provider.ResolveResource(resource);
ResourceContainer container = dataService.Provider.GetContainerForResourceType(actualEntity.GetType());
CheckForETagInDeleteOperation(actualEntity, resource, container, dataService.RequestParams, dataService.Provider);
// Doesn't matter which content format we pass here, since the value we are setting to is null
Deserializer.ModifyResource(description, resource, null, ContentFormat.Text, dataService.Provider);
}
#endif
else
{
throw DataServiceException.CreateMethodNotAllowed(
Strings.BadRequest_InvalidUriForDeleteOperation(dataService.RequestParams.AbsoluteRequestUri),
dataService.Configuration.GetAllowedMethods(description));
}
}
/// Handles a request for an internal resource if applicable.
/// Request description.
/// data service to which the request was made.
///
/// An action that produces the resulting stream; null if the description isn't for an internal resource.
///
private static Action HandleInternalResources(RequestDescription description, IDataService dataService)
{
string[] exactContentType = null;
ContentFormat format = ContentFormat.Unknown;
string mime = null;
if (description.TargetKind == RequestTargetKind.Metadata)
{
exactContentType = new string[] { XmlConstants.MimeMetadata };
format = ContentFormat.MetadataDocument;
mime = HttpProcessUtility.SelectRequiredMimeType(
dataService.RequestParams.Accept, // acceptTypesText
exactContentType, // exactContentType
XmlConstants.MimeApplicationXml); // inexactContentType
}
else if (description.TargetKind == RequestTargetKind.ServiceDirectory)
{
exactContentType = new string[] { XmlConstants.MimeApplicationAtomService, XmlConstants.MimeApplicationJson, XmlConstants.MimeApplicationXml };
mime = HttpProcessUtility.SelectRequiredMimeType(
dataService.RequestParams.Accept, // acceptTypesText
exactContentType, // exactContentType
XmlConstants.MimeApplicationXml); // inexactContentType;
format = GetContentFormat(mime);
}
if (exactContentType != null)
{
Debug.Assert(
format != ContentFormat.Unknown,
"format(" + format + ") != ContentFormat.Unknown -- otherwise exactContentType should be null");
Encoding encoding = HttpProcessUtility.EncodingFromAcceptCharset(dataService.RequestParams.AcceptCharset);
dataService.Host.ResponseContentType = HttpProcessUtility.BuildContentType(mime, encoding);
return new ResponseBodyWriter(
encoding,
false, // hasMoved
dataService,
null, // queryResults
description,
format).Write;
}
return null;
}
///
/// Compare the ETag value and then serialize the value if required
///
/// Description of the uri requested.
/// Content format for response.
/// Data service to which the request was made.
/// A delegate that can serialize the result.
private static Action CompareETagAndWriteResponse(
RequestDescription description,
ContentFormat responseFormat,
IDataService dataService)
{
Debug.Assert(description != null, "description != null");
Debug.Assert(dataService != null, "dataService != null");
CachedRequestParams requestParams = dataService.RequestParams;
Debug.Assert(
String.IsNullOrEmpty(requestParams.IfMatch) || String.IsNullOrEmpty(requestParams.IfNoneMatch),
"Both If-Match and If-None-Match header cannot be specified");
IEnumerator queryResults = null;
try
{
if (requestParams.AstoriaHttpVerb == AstoriaVerbs.GET)
{
bool writeResponse = true;
// Get the index of the last resource in the request uri
int parentResourceIndex = description.GetIndexOfTargetEntityResource();
SegmentInfo parentEntitySegment = description.SegmentInfos[parentResourceIndex];
queryResults = RequestDescription.GetSingleResultFromEnumerable(parentEntitySegment);
object resource = queryResults.Current;
string etagValue = null;
if (description.LinkUri)
{
// No need to worry about etags while performing link operations
// No etags can be specified also while performing link operations
if (requestParams.IfMatch != null || requestParams.IfNoneMatch != null)
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_ETagsNotAllowedForLinkOperations);
}
if (resource == null)
{
throw DataServiceException.CreateResourceNotFound(description.LastSegmentInfo.Identifier);
}
}
else
{
ResourceContainer container = null;
if (resource != null)
{
container = WebUtil.GetResourceContainer(resource, parentEntitySegment, dataService.Provider);
}
etagValue = WebUtil.CompareAndGetETag(
resource, resource, container, dataService.Provider, requestParams, out writeResponse);
if (resource == null && description.TargetKind == RequestTargetKind.Resource)
{
Debug.Assert(description.Property != null, "non-open type property");
// If you are querying reference nav property and the value is null,
// return 204 - No Content e.g. /Customers(1)/BestFriend
dataService.Host.ResponseStatusCode = 204; // No Content
return EmptyStreamWriter;
}
WriteETagValueInResponseHeader(description, etagValue, dataService.Host);
}
if (writeResponse)
{
int lastResourceIndex = description.GetIndexOfTargetEntityResource();
return WriteSingleElementResponse(description, responseFormat, queryResults, lastResourceIndex, etagValue, dataService);
}
else
{
dataService.Host.ResponseStatusCode = 304; // Not Modified
return EmptyStreamWriter;
}
}
else
{
Debug.Assert(requestParams.AstoriaHttpVerb == AstoriaVerbs.POST, "Must be POST method");
ResourceContainer container;
object actualEntity = GetContainerAndActualEntityInstance(dataService.Provider, description, out container);
dataService.Host.ResponseLocation = Serializer.GetUri(actualEntity, dataService.Provider, container, requestParams.AbsoluteServiceUri).AbsoluteUri;
string etagValue = WebUtil.GetETagValue(dataService.Provider, actualEntity, container);
queryResults = RequestDescription.GetSingleResultFromEnumerable(description.LastSegmentInfo);
return WriteSingleElementResponse(description, responseFormat, queryResults, description.SegmentInfos.Length - 1, etagValue, dataService);
}
}
catch
{
WebUtil.Dispose(queryResults);
throw;
}
}
#if ASTORIA_OPEN_OBJECT
/// Resolves the content format required when it is statically unknown.
/// Request description.
/// Result target.
/// data service to which the request was made.
/// The format for the specified element.
private static ContentFormat ResolveUnknownFormat(RequestDescription description, object element, IDataService dataService)
{
Debug.Assert(
description.TargetKind == RequestTargetKind.OpenProperty ||
description.TargetKind == RequestTargetKind.OpenPropertyValue,
description.TargetKind + " is open property or open property value");
WebUtil.CheckResourceExists(element != null, description.LastSegmentInfo.Identifier);
Type elementType = element.GetType();
ResourceType resourceType = dataService.Provider.GetResourceType(elementType);
// This resource wouldn't be visible during serialization, so we treat is as 404.
if (resourceType == null)
{
throw new InvalidOperationException(Strings.DataService_InvalidResourceType(elementType.FullName));
}
// Determine the appropriate target type based on the kind of resource.
bool rawValue = description.TargetKind == RequestTargetKind.OpenPropertyValue;
RequestTargetKind targetKind;
switch (resourceType.ResourceTypeKind)
{
case ResourceTypeKind.ComplexType:
if (rawValue)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_ValuesCanBeReturnedForPrimitiveTypesOnly);
}
else
{
targetKind = RequestTargetKind.ComplexObject;
}
break;
case ResourceTypeKind.Primitive:
if (rawValue)
{
targetKind = RequestTargetKind.PrimitiveValue;
}
else
{
targetKind = RequestTargetKind.Primitive;
}
break;
default:
Debug.Assert(ResourceTypeKind.EntityType == resourceType.ResourceTypeKind, "ResourceTypeKind.EntityType == " + resourceType.ResourceTypeKind);
if (rawValue)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_ValuesCanBeReturnedForPrimitiveTypesOnly);
}
else
{
targetKind = RequestTargetKind.Resource;
}
break;
}
if (description.LinkUri)
{
targetKind = RequestTargetKind.Link;
}
return SelectResponseFormatForType(targetKind, elementType, dataService.RequestParams.Accept, null, dataService.Host);
}
#endif
///
/// Compare the ETag value and then serialize the value if required
///
/// Description of the uri requested.
/// format of the response
/// Enumerator whose current resource points to the resource which needs to be written
/// index of the segment info that represents the last resource
/// etag value for the resource specified in parent resource parameter
/// data service to which the request was made.
/// A delegate that can serialize the result.
private static Action WriteSingleElementResponse(
RequestDescription description,
ContentFormat responseFormat,
IEnumerator queryResults,
int parentResourceIndex,
string etagValue,
IDataService dataService)
{
try
{
if (parentResourceIndex != description.SegmentInfos.Length - 1)
{
// Dispose the old enumerator
WebUtil.Dispose(queryResults);
// get the resource which need to be written
queryResults = RequestDescription.GetSingleResultFromEnumerable(description.LastSegmentInfo);
}
// If we had to wait until we got a value to determine the valid contents, try that now.
#if ASTORIA_OPEN_OBJECT
if (responseFormat == ContentFormat.Unknown)
{
responseFormat = ResolveUnknownFormat(description, queryResults.Current, dataService);
}
#else
Debug.Assert(responseFormat != ContentFormat.Unknown, "responseFormat != ContentFormat.Unknown");
#endif
// Write the etag header
WriteETagValueInResponseHeader(description, etagValue, dataService.Host);
Encoding encoding = GetRequestAcceptEncoding(responseFormat, dataService.RequestParams.AcceptCharset);
dataService.Host.ResponseContentType = HttpProcessUtility.BuildContentType(dataService.Host.ResponseContentType, encoding);
return new ResponseBodyWriter(
encoding,
true /* hasMoved */,
dataService,
queryResults,
description,
responseFormat).Write;
}
catch
{
WebUtil.Dispose(queryResults);
throw;
}
}
///
/// Write the etag header value in the response
///
/// description about the request made
/// etag value that needs to be written.
/// Host implementation for this data service.
private static void WriteETagValueInResponseHeader(RequestDescription requestDescription, string etagValue, IDataServiceHost host)
{
Debug.Assert(requestDescription.IsSingleResult, "requestDescription.IsSingleResult");
if ((requestDescription.ExpandPaths == null || requestDescription.ExpandPaths.Count == 0)
&& !String.IsNullOrEmpty(etagValue))
{
host.ResponseETag = etagValue;
}
}
///
/// Returns the actual entity instance and its containers for the resource in the description results.
///
/// Data provider
/// description about the request made.
/// returns the container to which the result resource belongs to.
/// returns the actual entity instance for the given resource.
private static object GetContainerAndActualEntityInstance(
IDataServiceProvider provider, RequestDescription description, out ResourceContainer container)
{
// For POST operations, we need to resolve the entity only after save changes. Hence we need to do this at the serialization
// to make sure save changes has been called
object[] results = (object[])description.RequestEnumerable;
Debug.Assert(results != null && results.Length == 1, "results != null && results.Length == 1");
// Make a call to the provider to get the exact resource instance back
results[0] = provider.ResolveResource(results[0]);
container = description.LastSegmentInfo.TargetContainer;
#if ASTORIA_OPEN_OBJECT
if (container == null)
{
// Open types will not have TargetContainer set, but they don't support MEST either.
Debug.Assert(
RequestTargetKind.OpenProperty == description.LastSegmentInfo.TargetKind,
"RequestTargetKind.OpenProperty == description.LastSegmentInfo.TargetKind(" + description.LastSegmentInfo.TargetKind + " - otherwise, why is TargetContainer null for POST target?");
container = provider.GetContainerForResourceType(results[0].GetType());
Debug.Assert(
container != null,
"container != null -- otherwise results[0].GetType() (" + results[0].GetType() + ") didn't work.");
}
#else
Debug.Assert(container != null, "description.LastSegmentInfo.TargetContainer != null");
#endif
return results[0];
}
///
/// Check for etag values for the given resource in DeleteOperation
///
/// resource whose etag value needs to be compared to the one given in the request header
/// token as returned by the IUpdatable.GetResource method.
/// resource container to which the resource belongs to.
/// request headers
/// Data provider
private static void CheckForETagInDeleteOperation(
object actualEntityInstance,
object entityToken,
ResourceContainer container,
CachedRequestParams requestParams,
IDataServiceProvider provider)
{
Debug.Assert(actualEntityInstance != null, "actualEntityInstance != null");
Debug.Assert(entityToken != null, "entityToken != null");
// If this method is called for Update, we need to pass the token object as well as the actual instance.
// The actual instance is used to determine the type that's necessary to find out the etag properties.
// The token is required to pass back to IUpdatable interface, if we need to get the values for etag properties.
if (!String.IsNullOrEmpty(requestParams.IfNoneMatch))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_IfNoneMatchHeaderNotSupportedInDelete);
}
ICollection etagProperties = provider.GetETagProperties(container.Name, actualEntityInstance.GetType());
if (etagProperties.Count == 0)
{
if (requestParams.IfMatch != null)
{
throw DataServiceException.CreateBadRequestError(Strings.Serializer_NoETagPropertiesForType);
}
}
else if (String.IsNullOrEmpty(requestParams.IfMatch))
{
string typeName = WebUtil.GetTypeName(provider, actualEntityInstance.GetType());
throw DataServiceException.CreateBadRequestError(Strings.DataService_CannotPerformDeleteOperationWithoutETag(typeName));
}
else if (requestParams.IfMatch != XmlConstants.HttpAnyETag)
{
string etagValue = WebUtil.GetETagValue(entityToken, etagProperties, provider);
if (etagValue != requestParams.IfMatch)
{
throw DataServiceException.CreatePreConditionFailedError(Strings.Serializer_ETagValueDoesNotMatch);
}
}
}
/// No-op method for a stream-writing action.
/// Stream to write to.
private static void EmptyStreamWriter(Stream stream)
{
}
///
/// Handles the unbind operations
///
/// description about the request made.
/// data service to which the request was made.
private static void HandleUnbindOperation(RequestDescription description, IDataService dataService)
{
Debug.Assert(description.LinkUri, "This method must be called for link operations");
Debug.Assert(description.IsSingleResult, "Expecting this method to be called on single resource uris");
object parentEntity;
Deserializer.GetResourceToModify(description, dataService, out parentEntity);
if (description.Property != null)
{
if (description.Property.Kind == ResourcePropertyKind.ResourceReference)
{
dataService.Provider.SetReference(parentEntity, description.Property.Name, null);
}
else
{
Debug.Assert(description.Property.Kind == ResourcePropertyKind.ResourceSetReference, "expecting collection nav properties");
Debug.Assert(description.LastSegmentInfo.HasKeyValues, "expecting properties to have key value specified");
object childEntity = Deserializer.GetResource(description.LastSegmentInfo, null, dataService, true /*checkForNull*/);
dataService.Provider.RemoveReferenceFromCollection(parentEntity, description.Property.Name, childEntity);
}
}
else
{
if (description.LastSegmentInfo.HasKeyValues)
{
object childEntity = Deserializer.GetResource(description.LastSegmentInfo, null, dataService, true /*checkForNull*/);
dataService.Provider.RemoveReferenceFromCollection(parentEntity, description.ContainerName, childEntity);
}
else
{
dataService.Provider.SetReference(parentEntity, description.ContainerName, null);
}
}
}
///
/// Get the content format corresponding to the given mime type.
///
/// mime type for the request.
/// content format mapping to the given mime type.
private static ContentFormat GetContentFormat(string mime)
{
if (mime == XmlConstants.MimeApplicationJson)
{
return ContentFormat.Json;
}
else if (mime == XmlConstants.MimeApplicationAtom)
{
return ContentFormat.Atom;
}
else
{
Debug.Assert(
mime == XmlConstants.MimeApplicationXml || mime == XmlConstants.MimeTextXml,
"expecting application/xml or plain/xml, got " + mime);
return ContentFormat.PlainXml;
}
}
///
/// Handle the request - whether its a batch request or a non-batch request
///
/// Returns the delegate for writing the response
private Action HandleRequest()
{
Debug.Assert(this.host != null, "this.host != null");
Action writer;
try
{
if (this.host is HttpContextServiceHost)
{
((HttpContextServiceHost)this.host).VerifyQueryParameters();
}
RequestDescription description = this.ProcessIncomingRequestUriAndCacheHeaders();
this.OnStartProcessingRequest(new ProcessRequestArgs(this.requestParams.AbsoluteRequestUri, false /*isBatchOperation*/));
if (description.TargetKind != RequestTargetKind.Batch)
{
writer = this.HandleNonBatchRequest(description);
}
else
{
writer = this.HandleBatchRequest();
}
}
catch (Exception exception)
{
// Exception should be re-thrown if not handled.
if (!WebUtil.IsCatchableExceptionType(exception))
{
throw;
}
string accept = (this.requestParams != null) ? this.requestParams.Accept : null;
string acceptCharset = (this.requestParams != null) ? this.requestParams.AcceptCharset : null;
writer = ErrorHandler.HandleBeforeWritingException(exception, this, accept, acceptCharset);
}
Debug.Assert(writer != null, "writer != null");
return writer;
}
///
/// Handle non-batch requests
///
/// description about the request uri.
/// Returns the delegate which takes the response stream for writing the response.
private Action HandleNonBatchRequest(RequestDescription description)
{
Debug.Assert(description.TargetKind != RequestTargetKind.Batch, "description.TargetKind != RequestTargetKind.Batch");
description = ProcessIncomingRequest(description, this);
if (this.requestParams.AstoriaHttpVerb != AstoriaVerbs.GET)
{
this.provider.SaveChanges();
}
return (description == null) ? EmptyStreamWriter : SerializeResponseBody(description, this);
}
/// Handle the batch request.
/// Returns the delegate which takes the response stream for writing the response.
private Action HandleBatchRequest()
{
// Verify the HTTP method.
if (this.requestParams.AstoriaHttpVerb != AstoriaVerbs.POST)
{
throw DataServiceException.CreateMethodNotAllowed(
Strings.DataService_BatchResourceOnlySupportsPost,
XmlConstants.HttpMethodPost);
}
CheckVersion(this);
// Verify the content type and get the boundary string
Encoding encoding;
string boundary;
if (!BatchStream.GetBoundaryAndEncodingFromMultipartMixedContentType(this.requestParams.ContentType, out boundary, out encoding) ||
String.IsNullOrEmpty(boundary))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_InvalidContentTypeForBatchRequest);
}
// Write the response headers
this.host.ResponseStatusCode = 202; // OK
this.host.ResponseCacheControl = XmlConstants.HttpCacheControlNoCache;
string batchBoundary = XmlConstants.HttpMultipartBoundaryBatchResponse + '_' + Guid.NewGuid().ToString();
this.host.ResponseContentType = String.Format(
System.Globalization.CultureInfo.InvariantCulture,
"{0}; {1}={2}",
XmlConstants.MimeMultiPartMixed,
XmlConstants.HttpMultipartBoundary,
batchBoundary);
BatchStream batchStream = new BatchStream(this.host.RequestStream, boundary, encoding, true);
this.batchDataService = new BatchDataService(this, batchStream, batchBoundary);
return this.batchDataService.HandleBatchContent;
}
/// Creates the provider and configuration as necessary to be used for this request.
private void EnsureProviderAndConfigForRequest()
{
if (this.provider == null)
{
Type dataServiceType = this.GetType();
object dataSourceInstance = this.CreateDataSource();
if (dataSourceInstance == null)
{
throw new InvalidOperationException(Strings.DataService_CreateDataSourceNull);
}
this.provider = CreateProvider(dataServiceType, dataSourceInstance, out this.configuration);
}
else
{
Debug.Assert(this.configuration != null, "this.configuration != null -- otherwise this.provider was ----signed with no configuration");
}
}
///
/// Processes the incoming request and cache all the request headers
///
/// description about the request uri.
private RequestDescription ProcessIncomingRequestUriAndCacheHeaders()
{
this.requestParams = new CachedRequestParams(
this.host.RequestAccept,
this.host.RequestAcceptCharSet,
this.host.RequestContentType,
this.host.RequestHttpMethod,
this.host.RequestIfMatch,
this.host.RequestIfNoneMatch,
this.host.RequestVersion,
this.host.RequestMaxVersion,
RequestUriProcessor.GetAbsoluteRequestUri(this.host),
RequestUriProcessor.GetServiceUri(this.host));
ValidateRequest(this.requestParams);
return RequestUriProcessor.ProcessRequestUri(this.requestParams.AbsoluteRequestUri, this);
}
#endregion Private methods.
///
/// Dummy data service for batch requests
///
private class BatchDataService : IDataService
{
#region Private fields.
/// Original data service instance.
private readonly IDataService dataService;
/// batch stream which reads the content of the batch from the underlying request stream.
private readonly BatchStream batchRequestStream;
/// batch response seperator string.
private readonly string batchBoundary;
/// Hashset to make sure that the content ids specified in the batch are all unique.
private readonly HashSet contentIds = new HashSet(new Int32EqualityComparer());
/// Dictionary to track objects represented by each content id within a changeset.
private readonly Dictionary contentIdsToSegmentInfoMapping = new Dictionary(StringComparer.Ordinal);
/// Number of changset/query operations encountered in the current batch.
private int batchElementCount;
/// Whether the batch limit has been exceeded (implies no further processing should take place).
private bool batchLimitExceeded;
/// List of the all request description within a changeset.
private List batchRequestDescription = new List();
/// List of the all response headers and results of each operation within a changeset.
private List batchRequestHost = new List();
/// Number of CUD operations encountered in the current changeset.
private int changeSetElementCount;
/// Batch Host which caches the request headers and response headers per operation within a changeset.
private IDataServiceHost host;
#endregion Private fields.
///
/// Creates an instance of the batch data service which keeps track of the
/// request and response headers per operation in the batch
///
/// original data service to which the batch request was made
/// batch stream which read batch content from the request stream
/// batch response seperator string.
internal BatchDataService(IDataService dataService, BatchStream batchRequestStream, string batchBoundary)
{
Debug.Assert(dataService != null, "dataService != null");
Debug.Assert(batchRequestStream != null, "batchRequestStream != null");
Debug.Assert(batchBoundary != null, "batchBoundary != null");
this.dataService = dataService;
this.batchRequestStream = batchRequestStream;
this.batchBoundary = batchBoundary;
}
#region IDataService Members
/// Service configuration information.
DataServiceConfiguration IDataService.Configuration
{
get { return this.dataService.Configuration; }
}
/// Host implementation for the batch data service.
IDataServiceHost IDataService.Host
{
get { return this.host; }
}
/// Data provider for this data service.
IDataServiceProvider IDataService.Provider
{
get { return this.dataService.Provider; }
}
/// Instance of the data provider.
IDataService IDataService.Instance
{
get { return this.dataService.Instance; }
}
/// Gets the cached request headers.
CachedRequestParams IDataService.RequestParams
{
get { return ((BatchServiceHost)this.host).RequestParams; }
}
///
/// This method is called during query processing to validate and customize
/// paths for the $expand options are applied by the provider.
///
/// Query which will be composed.
/// Collection of segment paths to be expanded.
void IDataService.InternalApplyingExpansions(IQueryable queryable, ICollection expandPaths)
{
this.dataService.InternalApplyingExpansions(queryable, expandPaths);
}
/// Processes a catchable exception.
/// The arguments describing how to handle the exception.
void IDataService.InternalHandleException(HandleExceptionArgs args)
{
this.dataService.InternalHandleException(args);
}
///
/// Returns the segmentInfo of the resource referred by the given content Id;
///
/// content id for a operation in the batch request.
/// segmentInfo for the resource referred by the given content id.
SegmentInfo IDataService.GetSegmentForContentId(string contentId)
{
if (contentId.StartsWith("$", StringComparison.Ordinal))
{
SegmentInfo segmentInfo;
this.contentIdsToSegmentInfoMapping.TryGetValue(contentId.Substring(1), out segmentInfo);
return segmentInfo;
}
return null;
}
///
/// Get the resource referred by the segment in the request with the given index
///
/// description about the request url.
/// index of the segment that refers to the resource that needs to be returned.
/// typename of the resource.
/// the resource as returned by the provider.
object IDataService.GetResource(RequestDescription description, int segmentIndex, string typeFullName)
{
if (description.SegmentInfos[0].Identifier.StartsWith("$", StringComparison.Ordinal))
{
Debug.Assert(segmentIndex >= 0 && segmentIndex < description.SegmentInfos.Length, "segment index must be a valid one");
if (description.SegmentInfos[segmentIndex].RequestEnumerable == null)
{
object resource = GetResourceFromSegmentEnumerable(description.SegmentInfos[0]);
for (int i = 1; i <= segmentIndex; i++)
{
resource = ((IDataService)this).Provider.GetValue(resource, description.SegmentInfos[i].Identifier);
if (resource == null)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_DereferencingNullPropertyValue(description.SegmentInfos[i].Identifier));
}
description.SegmentInfos[i].RequestEnumerable = new object[] { resource };
}
return resource;
}
else
{
return GetResourceFromSegmentEnumerable(description.SegmentInfos[segmentIndex]);
}
}
return Deserializer.GetResource(description.SegmentInfos[segmentIndex], typeFullName, ((IDataService)this), false /*checkForNull*/);
}
///
/// Dispose the data source instance
///
void IDataService.DisposeDataSource()
{
this.dataService.DisposeDataSource();
}
///
/// This method is called before a request is processed.
///
/// Information about the request that is going to be processed.
void IDataService.InternalOnStartProcessingRequest(ProcessRequestArgs args)
{
this.dataService.InternalOnStartProcessingRequest(args);
}
#endregion
///
/// Handle the batch content
///
/// response stream for writing batch response
internal void HandleBatchContent(Stream responseStream)
{
BatchServiceHost batchHost = null;
RequestDescription description;
string changesetBoundary = null;
Exception exceptionEncountered = null;
try
{
StreamWriter writer = new StreamWriter(responseStream, HttpProcessUtility.FallbackEncoding);
while (!this.batchLimitExceeded && this.batchRequestStream.State != BatchStreamState.EndBatch)
{
// clear the host from the last operation
this.host = null;
// If we encounter any error while reading the batch request,
// we write out the exception message and return. We do not try
// and read the request further.
try
{
this.batchRequestStream.MoveNext();
}
catch (Exception exception)
{
if (!WebUtil.IsCatchableExceptionType(exception))
{
throw;
}
ErrorHandler.HandleBatchRequestException(this, exception, writer);
break;
}
try
{
switch (this.batchRequestStream.State)
{
case BatchStreamState.BeginChangeSet:
this.IncreaseBatchCount();
changesetBoundary = XmlConstants.HttpMultipartBoundaryChangesetResponse + '_' + Guid.NewGuid().ToString();
BatchWriter.WriteStartBatchBoundary(writer, this.batchBoundary, changesetBoundary);
break;
case BatchStreamState.EndChangeSet:
#region EndChangeSet
this.changeSetElementCount = 0;
this.contentIdsToSegmentInfoMapping.Clear();
// In case of exception, the changeset boundary will be set to null.
// for that case, just write the end boundary and continue
if (exceptionEncountered == null)
{
Debug.Assert(!String.IsNullOrEmpty(changesetBoundary), "!String.IsNullOrEmpty(changesetBoundary)");
// Save all the changes and write the response
this.dataService.Provider.SaveChanges();
Debug.Assert(this.batchRequestHost.Count == this.batchRequestDescription.Count, "counts must be the same");
for (int i = 0; i < this.batchRequestDescription.Count; i++)
{
this.host = this.batchRequestHost[i];
this.WriteRequest(this.batchRequestDescription[i], this.batchRequestHost[i]);
}
BatchWriter.WriteEndBoundary(writer, changesetBoundary);
}
else
{
this.HandleChangesetException(exceptionEncountered, this.batchRequestHost, changesetBoundary, writer);
}
break;
#endregion //EndChangeSet
case BatchStreamState.Get:
#region GET Operation
this.IncreaseBatchCount();
batchHost = CreateHostFromHeaders(
this.dataService.Host,
this.batchRequestStream,
this.contentIds,
this.batchBoundary,
writer);
this.host = batchHost;
// it must be GET operation
Debug.Assert(this.host.RequestHttpMethod == XmlConstants.HttpMethodGet, "this.host.RequestHttpMethod == XmlConstants.HttpMethodGet");
Debug.Assert(this.batchRequestDescription.Count == 0, "this.batchRequestDescription.Count == 0");
Debug.Assert(this.batchRequestHost.Count == 0, "this.batchRequestHost.Count == 0");
this.dataService.InternalOnStartProcessingRequest(new ProcessRequestArgs(this.host.AbsoluteRequestUri, true /*isBatchOperation*/));
description = RequestUriProcessor.ProcessRequestUri(this.host.AbsoluteRequestUri, this);
description = ProcessIncomingRequest(description, this);
this.WriteRequest(description, batchHost);
break;
#endregion // GET Operation
case BatchStreamState.Post:
case BatchStreamState.Put:
case BatchStreamState.Delete:
case BatchStreamState.Merge:
#region CUD Operation
// if we encounter an error, we ignore rest of the operations
// within a changeset.
this.IncreaseChangeSetCount();
batchHost = CreateHostFromHeaders(this.dataService.Host, this.batchRequestStream, this.contentIds, changesetBoundary, writer);
if (exceptionEncountered == null)
{
this.batchRequestHost.Add(batchHost);
this.host = batchHost;
this.dataService.InternalOnStartProcessingRequest(new ProcessRequestArgs(this.host.AbsoluteRequestUri, true /*isBatchOperation*/));
description = RequestUriProcessor.ProcessRequestUri(this.host.AbsoluteRequestUri, this);
description = ProcessIncomingRequest(description, this);
this.batchRequestDescription.Add(description);
// In Link case, we do not write any response out. hence the description will be null
if (this.batchRequestStream.State == BatchStreamState.Post && description != null)
{
Debug.Assert(description.TargetKind == RequestTargetKind.Resource, "The target must be a resource, since otherwise cross-referencing doesn't make sense");
// if the content id is specified, only then add it to the collection
if (batchHost.ContentId != null)
{
this.contentIdsToSegmentInfoMapping.Add(batchHost.ContentId, description.LastSegmentInfo);
}
}
}
break;
#endregion // CUD Operation
default:
Debug.Assert(this.batchRequestStream.State == BatchStreamState.EndBatch, "expecting end batch state");
break;
}
}
catch (Exception exception)
{
if (!WebUtil.IsCatchableExceptionType(exception))
{
throw;
}
if (this.batchRequestStream.State == BatchStreamState.EndChangeSet)
{
this.HandleChangesetException(exception, this.batchRequestHost, changesetBoundary, writer);
}
else if (this.batchRequestStream.State == BatchStreamState.Post ||
this.batchRequestStream.State == BatchStreamState.Put ||
this.batchRequestStream.State == BatchStreamState.Delete ||
this.batchRequestStream.State == BatchStreamState.Merge)
{
// Store the exception if its in the middle of the changeset,
// we need to write the same exception for every
exceptionEncountered = exception;
}
else
{
BatchServiceHost currentHost = (BatchServiceHost)this.host;
if (currentHost == null)
{
// For error cases (like we encounter an error while parsing request headers
// and were not able to create the host), we need to create a dummy host
currentHost = new BatchServiceHost(this.batchBoundary, writer);
}
ErrorHandler.HandleBatchProcessException(this, currentHost, exception, writer);
}
}
finally
{
// Once the end of the changeset is reached, clear the error state
if (this.batchRequestStream.State == BatchStreamState.EndChangeSet)
{
exceptionEncountered = null;
changesetBoundary = null;
this.batchRequestDescription.Clear();
this.batchRequestHost.Clear();
}
}
}
BatchWriter.WriteEndBoundary(writer, this.batchBoundary);
writer.Flush();
}
finally
{
this.batchRequestStream.Dispose();
}
}
#region Private methods.
///
/// Gets the value of the given header from the given header collection
///
/// Dictionary with header names and values.
/// name of the header whose value needs to be returned.
/// value of the given header.
private static string GetValue(Dictionary headers, string headerName)
{
string headerValue;
headers.TryGetValue(headerName, out headerValue);
return headerValue;
}
///
/// Creates a batch host from the given headers
///
/// IDataServiceHost implementation host for this data service.
/// batch stream which contains the header information.
/// content ids that are defined in the batch.
/// Part separator for host.
/// Output writer.
/// instance of the batch host which represents the current operation.
private static BatchServiceHost CreateHostFromHeaders(IDataServiceHost host, BatchStream batchStream, HashSet contentIds, string boundary, StreamWriter writer)
{
Debug.Assert(batchStream != null, "batchStream != null");
Debug.Assert(boundary != null, "boundary != null");
// If the Content-ID header is defined, it should be unique.
string contentIdValue = GetValue(batchStream.ContentHeaders, XmlConstants.HttpContentID);
if (!String.IsNullOrEmpty(contentIdValue))
{
int contentId;
if (!Int32.TryParse(contentIdValue, System.Globalization.NumberStyles.Integer, System.Globalization.NumberFormatInfo.InvariantInfo, out contentId))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_ContentIdMustBeAnInteger(contentId));
}
if (!contentIds.Add(contentId))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_ContentIdMustBeUniqueInBatch(contentId));
}
}
CachedRequestParams requestParams = CreateRequestParams(host, batchStream);
return new BatchServiceHost(requestParams, batchStream.GetContentStream(), contentIdValue, boundary, writer);
}
///
/// Creates a new instance of CachedRequestParams given the header information
///
/// IDataServiceHost implementation host for this data service.
/// batch stream which contains the header information.
/// instance of the CachedRequestParams with all request header information.
private static CachedRequestParams CreateRequestParams(IDataServiceHost host, BatchStream batchStream)
{
string accept = GetValue(batchStream.ContentHeaders, XmlConstants.HttpRequestAccept);
string acceptCharset = GetValue(batchStream.ContentHeaders, XmlConstants.HttpRequestAcceptCharset);
string contentType = GetValue(batchStream.ContentHeaders, XmlConstants.HttpContentType);
string headerIfMatch = GetValue(batchStream.ContentHeaders, XmlConstants.HttpRequestIfMatch);
string headerIfNoneMatch = GetValue(batchStream.ContentHeaders, XmlConstants.HttpRequestIfNoneMatch);
string version = GetValue(batchStream.ContentHeaders, XmlConstants.HttpDataServiceVersion);
string maxVersion = GetValue(batchStream.ContentHeaders, XmlConstants.HttpMaxDataServiceVersion);
Uri absoluteServiceUri = RequestUriProcessor.GetServiceUri(host);
Uri contentUri = RequestUriProcessor.GetAbsoluteUriFromReference(
batchStream.ContentUri, // reference
absoluteServiceUri); // absoluteServiceUri
return new CachedRequestParams(
accept,
acceptCharset,
contentType,
GetHttpMethodName(batchStream.State),
headerIfMatch,
headerIfNoneMatch,
version,
maxVersion,
contentUri,
absoluteServiceUri);
}
///
/// Returns the http method name given the batch stream state
///
/// state of the batch stream.
/// returns the http method name
private static string GetHttpMethodName(BatchStreamState state)
{
Debug.Assert(
state == BatchStreamState.Get ||
state == BatchStreamState.Post ||
state == BatchStreamState.Put ||
state == BatchStreamState.Delete ||
state == BatchStreamState.Merge,
"Expecting BatchStreamState (" + state + ") to be Delete, Get, Post or Put");
switch (state)
{
case BatchStreamState.Delete:
return XmlConstants.HttpMethodDelete;
case BatchStreamState.Get:
return XmlConstants.HttpMethodGet;
case BatchStreamState.Post:
return XmlConstants.HttpMethodPost;
case BatchStreamState.Merge:
return XmlConstants.HttpMethodMerge;
default:
Debug.Assert(BatchStreamState.Put == state, "BatchStreamState.Put == state");
return XmlConstants.HttpMethodPut;
}
}
///
/// Gets the resource from the segment enumerable.
///
/// segment from which resource needs to be returned.
/// returns the resource contained in the request enumerable.
private static object GetResourceFromSegmentEnumerable(SegmentInfo segmentInfo)
{
Debug.Assert(segmentInfo.RequestEnumerable != null, "The segment should always have the result");
object[] results = (object[])segmentInfo.RequestEnumerable;
Debug.Assert(results != null && results.Length == 1, "results != null && results.Length == 1");
Debug.Assert(results[0] != null, "results[0] != null");
return results[0];
}
///
/// Write the exception encountered in the middle of the changeset to the response
///
/// exception encountered
/// list of hosts
/// changeset boundary for the current processing changeset
/// writer to which the response needs to be written
private void HandleChangesetException(
Exception exception,
List changesetHosts,
string changesetBoundary,
StreamWriter writer)
{
Debug.Assert(exception != null, "exception != null");
Debug.Assert(changesetHosts != null, "changesetHosts != null");
Debug.Assert(WebUtil.IsCatchableExceptionType(exception), "WebUtil.IsCatchableExceptionType(exception)");
// For a changeset, we need to write the exception only once. Since we ignore all the changesets
// after we encounter an error, its the last changeset which had error. For cases, which we don't
// know, (like something in save changes, etc), we will still right the last operation information.
// If there are no host, then just pass null.
BatchServiceHost currentHost = null;
if (changesetHosts.Count == 0)
{
currentHost = new BatchServiceHost(changesetBoundary, writer);
}
else
{
currentHost = changesetHosts[changesetHosts.Count - 1];
}
ErrorHandler.HandleBatchProcessException(this, currentHost, exception, writer);
// Write end boundary for the changeset
BatchWriter.WriteEndBoundary(writer, changesetBoundary);
this.dataService.Provider.ClearChanges();
}
/// Increases the count of batch changsets/queries found, and checks it is within limits.
private void IncreaseBatchCount()
{
checked
{
this.batchElementCount++;
}
if (this.batchElementCount > this.dataService.Configuration.MaxBatchCount)
{
this.batchLimitExceeded = true;
throw new DataServiceException(400, Strings.DataService_BatchExceedMaxBatchCount(this.dataService.Configuration.MaxBatchCount));
}
}
/// Increases the count of changeset CUD operations found, and checks it is within limits.
private void IncreaseChangeSetCount()
{
checked
{
this.changeSetElementCount++;
}
if (this.changeSetElementCount > this.dataService.Configuration.MaxChangesetCount)
{
throw new DataServiceException(400, Strings.DataService_BatchExceedMaxChangeSetCount(this.dataService.Configuration.MaxChangesetCount));
}
}
///
/// Write the response for the given request, if required.
///
/// description of the request uri. If this is null, means that no response needs to be written
/// Batch host for which the request should be written.
private void WriteRequest(RequestDescription description, BatchServiceHost batchHost)
{
Debug.Assert(batchHost != null, "host != null");
// For DELETE operations, description will be null
if (description == null)
{
BatchWriter.WriteBoundaryAndHeaders(batchHost.Writer, this.host, batchHost.BoundaryString);
}
else
{
Action responseWriter = DataService.SerializeResponseBody(description, this);
if (responseWriter != null)
{
BatchWriter.WriteBoundaryAndHeaders(batchHost.Writer, this.host, batchHost.BoundaryString);
batchHost.Writer.Flush();
responseWriter(batchHost.Writer.BaseStream);
batchHost.Writer.WriteLine();
}
else
{
BatchWriter.WriteBoundaryAndHeaders(batchHost.Writer, this.host, batchHost.BoundaryString);
}
}
}
#endregion Private methods.
}
}
}
// File provided for Reference Use Only by Microsoft Corporation (c) 2007.
//----------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
//
// Provides a base class for DataWeb services.
//
//
// @owner [....]
//---------------------------------------------------------------------
namespace System.Data.Services
{
#region Namespaces.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Objects;
using System.Data.Services.Caching;
using System.Data.Services.Providers;
using System.Data.Services.Serializers;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.ServiceModel;
using System.ServiceModel.Activation;
using System.ServiceModel.Channels;
using System.Text;
#endregion Namespaces.
///
/// Represents a strongly typed service that can process data-oriented
/// resource requests.
///
/// The type of the store to provide resources.
///
/// will typically be a subtype of
/// System.Data.Object.ObjectContext or another class that provides IQueryable
/// properties.
///
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
public class DataService : IRequestHandler, IDataService
{
#region Private fields.
/// A delegate used to create an instance of the data context.
private static Func cachedConstructor;
/// Service configuration information.
private DataServiceConfiguration configuration;
/// Host implementation for this data service.
private IDataServiceHost host;
/// Data provider for this data service.
private IDataServiceProvider provider;
/// Cached request headers.
private CachedRequestParams requestParams;
/// dummy data service for batch requests.
private BatchDataService batchDataService;
#endregion Private fields.
#region Properties.
/// Service configuration information.
DataServiceConfiguration IDataService.Configuration
{
[DebuggerStepThrough]
get { return this.configuration; }
}
/// Host implementation for this data service
IDataServiceHost IDataService.Host
{
[DebuggerStepThrough]
get { return this.host; }
}
/// Data provider for this data service
IDataServiceProvider IDataService.Provider
{
[DebuggerStepThrough]
get
{
Debug.Assert(this.provider != null, "this.provider != null -- otherwise EnsureProviderAndConfigForRequest didn't");
return this.provider;
}
}
/// Returns the instance of data service.
IDataService IDataService.Instance
{
[DebuggerStepThrough]
get { return this; }
}
/// Cached request headers.
CachedRequestParams IDataService.RequestParams
{
[DebuggerStepThrough]
get { return this.requestParams; }
}
/// The data source used in the current request processing.
protected T CurrentDataSource
{
get { return (T)this.provider.CurrentDataSource; }
}
#endregion Properties.
#region Public / interface methods.
///
/// This method is called during query processing to validate and customize
/// paths for the $expand options are applied by the provider.
///
/// Query which will be composed.
/// Collection of segment paths to be expanded.
void IDataService.InternalApplyingExpansions(IQueryable queryable, ICollection expandPaths)
{
Debug.Assert(queryable != null, "queryable != null");
Debug.Assert(expandPaths != null, "expandPaths != null");
Debug.Assert(this.configuration != null, "this.configuration != null");
// Check the expand depth and count.
int actualExpandDepth = 0;
int actualExpandCount = 0;
foreach (ExpandSegmentCollection collection in expandPaths)
{
int segmentDepth = collection.Count;
if (segmentDepth > actualExpandDepth)
{
actualExpandDepth = segmentDepth;
}
actualExpandCount += segmentDepth;
}
if (this.configuration.MaxExpandDepth < actualExpandDepth)
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_ExpandDepthExceeded(actualExpandDepth, this.configuration.MaxExpandDepth));
}
if (this.configuration.MaxExpandCount < actualExpandCount)
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_ExpandCountExceeded(actualExpandCount, this.configuration.MaxExpandCount));
}
}
/// Processes a catchable exception.
/// The arguments describing how to handle the exception.
void IDataService.InternalHandleException(HandleExceptionArgs args)
{
Debug.Assert(args != null, "args != null");
try
{
this.HandleException(args);
}
catch (Exception handlingException)
{
if (!WebUtil.IsCatchableExceptionType(handlingException))
{
throw;
}
args.Exception = handlingException;
}
}
///
/// Returns the segmentInfo of the resource referred by the given content Id;
///
/// content id for a operation in the batch request.
/// segmentInfo for the resource referred by the given content id.
SegmentInfo IDataService.GetSegmentForContentId(string contentId)
{
return null;
}
///
/// Get the resource referred by the segment in the request with the given index
///
/// description about the request url.
/// index of the segment that refers to the resource that needs to be returned.
/// typename of the resource.
/// the resource as returned by the provider.
object IDataService.GetResource(RequestDescription description, int segmentIndex, string typeFullName)
{
Debug.Assert(description.SegmentInfos[segmentIndex].RequestEnumerable != null, "requestDescription.SegmentInfos[segmentIndex].RequestEnumerable != null");
return Deserializer.GetResource(description.SegmentInfos[segmentIndex], typeFullName, ((IDataService)this), false /*checkForNull*/);
}
/// Disposes the data source of the current if necessary.
///
/// Because the provider has affinity with a specific data source
/// (which is created and set by the DataService), we set
/// the provider to null so we remember to re-create it if the
/// service gets reused for a different request.
///
void IDataService.DisposeDataSource()
{
if (this.provider != null)
{
this.provider.DisposeDataSource();
this.provider = null;
}
}
///
/// This method is called before a request is processed.
///
/// Information about the request that is going to be processed.
void IDataService.InternalOnStartProcessingRequest(ProcessRequestArgs args)
{
this.OnStartProcessingRequest(args);
}
/// Attaches the specified host to this service.
/// Host for service to interact with.
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1500:VariableNamesShouldNotMatchFieldNames", MessageId = "host", Justification = "Makes 1:1 argument-to-field correspondence obvious.")]
public void AttachHost(IDataServiceHost host)
{
WebUtil.CheckArgumentNull(host, "host");
this.host = host;
}
/// Processes the specified .
/// with message body to process.
/// The response .
public Message ProcessRequestForMessage(Stream messageBody)
{
WebUtil.CheckArgumentNull(messageBody, "messageBody");
HttpContextServiceHost httpHost = new HttpContextServiceHost(messageBody);
this.AttachHost(httpHost);
bool shouldDispose = true;
try
{
this.EnsureProviderAndConfigForRequest();
Action writer = this.HandleRequest();
Debug.Assert(writer != null, "writer != null");
Message result = CreateMessage(MessageVersion.None, "", ((IDataServiceHost)httpHost).ResponseContentType, writer, this);
shouldDispose = false;
return result;
}
finally
{
if (shouldDispose)
{
((IDataService)this).DisposeDataSource();
}
}
}
/// Provides a host-agnostic entry point for request processing.
public void ProcessRequest()
{
if (this.host == null)
{
throw new InvalidOperationException(Strings.DataService_HostNotAttached);
}
try
{
this.EnsureProviderAndConfigForRequest();
Action writer = this.HandleRequest();
if (writer != null)
{
writer(this.host.ResponseStream);
}
}
finally
{
((IDataService)this).DisposeDataSource();
}
}
#endregion Public / interface methods.
#region Protected methods.
/// Initializes a new data source instance.
/// A new data source instance.
///
/// The default implementation uses a constructor with no parameters
/// to create a new instance.
///
/// The instance will only be used for the duration of a single
/// request, and will be disposed after the request has been
/// handled.
///
protected virtual T CreateDataSource()
{
if (cachedConstructor == null)
{
Type dataContextType = typeof(T);
if (dataContextType.IsAbstract)
{
throw new InvalidOperationException(
Strings.DataService_ContextTypeIsAbstract(dataContextType, this.GetType()));
}
cachedConstructor = (Func)WebUtil.CreateNewInstanceConstructor(dataContextType, null, dataContextType);
}
return cachedConstructor();
}
/// Handles an exception thrown while processing a request.
/// Arguments to the exception.
protected virtual void HandleException(HandleExceptionArgs args)
{
WebUtil.CheckArgumentNull(args, "arg");
Debug.Assert(args.Exception != null, "args.Exception != null -- .ctor should have checked");
}
///
/// This method is called before processing each request. For batch requests
/// it is called once for the top batch request and once for each operation
/// in the batch.
///
/// args containing information about the request.
protected virtual void OnStartProcessingRequest(ProcessRequestArgs args)
{
// Do nothing. Application writers can override this and look
// at the request args and do some processing.
}
#endregion Protected methods.
#region Private methods.
/// Checks that the specified has a known version.
/// Service to check.
private static void CheckVersion(IDataService service)
{
Debug.Assert(service != null, "service != null");
Debug.Assert(service.RequestParams != null, "service.RequestParams != null");
// Check that the request/payload version is understood.
string versionText = service.RequestParams.Version;
if (!String.IsNullOrEmpty(versionText))
{
KeyValuePair version;
if (!HttpProcessUtility.TryReadVersion(versionText, out version))
{
throw DataServiceException.CreateBadRequestError(
Strings.DataService_VersionCannotBeParsed(versionText));
}
// Currently we only recognize an exact match. In future versions
// we may choose to allow different major/minor combinations.
if (version.Key.Major != XmlConstants.DataServiceVersionCurrentMajor ||
version.Key.Minor != XmlConstants.DataServiceVersionCurrentMinor)
{
string message = Strings.DataService_VersionNotSupported(
version.Key.ToString(2),
XmlConstants.DataServiceVersionCurrentMajor,
XmlConstants.DataServiceVersionCurrentMinor);
throw DataServiceException.CreateBadRequestError(message);
}
}
// Check that the maximum version for the client will understand our response.
versionText = service.RequestParams.MaxVersion;
if (!String.IsNullOrEmpty(versionText))
{
KeyValuePair version;
if (!HttpProcessUtility.TryReadVersion(versionText, out version))
{
throw DataServiceException.CreateBadRequestError(
Strings.DataService_VersionCannotBeParsed(versionText));
}
if (version.Key.Major < XmlConstants.DataServiceVersionCurrentMajor ||
(version.Key.Major == XmlConstants.DataServiceVersionCurrentMajor &&
version.Key.Minor < XmlConstants.DataServiceVersionCurrentMinor))
{
string message = Strings.DataService_VersionTooLow(
version.Key.ToString(2),
XmlConstants.DataServiceVersionCurrentMajor,
XmlConstants.DataServiceVersionCurrentMinor);
throw DataServiceException.CreateBadRequestError(message);
}
}
}
///
/// Checks that if etag values are specified in the header, they must be valid.
///
/// header values.
private static void CheckETagValues(CachedRequestParams requestParams)
{
Debug.Assert(requestParams != null, "requestParams != null");
if (!IsETagValueValid(requestParams.IfMatch))
{
throw DataServiceException.CreateBadRequestError(Strings.DataServiceException_GeneralError);
}
if (!IsETagValueValid(requestParams.IfNoneMatch))
{
throw DataServiceException.CreateBadRequestError(Strings.DataServiceException_GeneralError);
}
}
///
/// Returns false if the given etag value is not valid.
/// Look in http://www.ietf.org/rfc/rfc2616.txt?number=2616 (Section 14.26) for more information
///
/// etag value to be checked.
/// returns true if the etag value is valid, otherwise returns false.
private static bool IsETagValueValid(string etag)
{
if (String.IsNullOrEmpty(etag) || etag == XmlConstants.HttpAnyETag)
{
return true;
}
if (etag.Length <= 4 || etag[0] != 'W' || etag[1] != '/' || etag[2] != '"' || etag[etag.Length - 1] != '"')
{
return false;
}
for (int i = 3; i < etag.Length - 1; i++)
{
// Format of etag looks something like: W/"etag property values"
// according to HTTP RFC 2616, if someone wants to specify more than 1 etag value,
// then need to specify something like this: W/"etag values", W/"etag values", ...
// To make sure only one etag is specified, we need to ensure that
// only the third and last characters are quotes.
// If " is part of the key value, it needs to be escaped.
if (etag[i] == '"')
{
return false;
}
}
return true;
}
///
/// Creates a that invokes the specified
/// callback to write its body.
///
/// Version for message.
/// Action for message.
/// MIME content type for body.
/// Callback.
/// Service with context to dispose once the response has been written.
/// A new .
private static Message CreateMessage(MessageVersion version, string action, string contentType, Action writer, IDataService service)
{
Debug.Assert(version != null, "version != null");
Debug.Assert(writer != null, "writer != null");
Debug.Assert(service != null, "service != null");
DelegateBodyWriter bodyWriter = new DelegateBodyWriter(writer, service);
Message message = Message.CreateMessage(version, action, bodyWriter);
message.Properties.Add(WebBodyFormatMessageProperty.Name, new WebBodyFormatMessageProperty(WebContentFormat.Raw));
HttpResponseMessageProperty response = new HttpResponseMessageProperty();
response.Headers[System.Net.HttpResponseHeader.ContentType] = contentType;
message.Properties.Add(HttpResponseMessageProperty.Name, response);
return message;
}
/// Creates a configuration for the specified service.
/// Type of DataService with authorization methods.
/// Provider with metadata.
/// Instance of the data source for the provider.
///
/// A (possibly shared) configuration implementation for the specified service.
///
private static DataServiceConfiguration CreateConfiguration(Type dataServiceType, IDataServiceProvider provider, object dataSourceInstance)
{
Debug.Assert(dataServiceType != null, "dataServiceType != null");
DataServiceConfiguration result = new DataServiceConfiguration(provider);
result.InvokeStaticInitialization(dataServiceType);
result.RegisterCallbacks(dataServiceType);
result.ApplyToProvider(dataSourceInstance);
return result;
}
///
/// Creates a provider implementation that wraps the T type.
///
/// Type of DataService with service operations.
/// Instance of the data source for the provider.
/// Service configuration information.
///
/// A (possibly shared) provider implementation that wraps the T type.
///
private static IDataServiceProvider CreateProvider(Type dataServiceType, object dataSourceInstance, out DataServiceConfiguration configuration)
{
Debug.Assert(dataServiceType != null, "dataServiceType != null");
Debug.Assert(dataSourceInstance != null, "dataSourceInstance != null");
Type dataContextType = typeof(T);
Debug.Assert(
dataContextType.IsAssignableFrom(dataSourceInstance.GetType()),
"dataContextType.IsAssignableFrom(dataSourceInstance.GetType()) -- otherwise the wrong data source instance was created.");
MetadataCacheItem metadata = MetadataCache.TryLookup(dataServiceType, dataSourceInstance);
bool metadataRequiresInitialization = metadata == null;
if (metadataRequiresInitialization)
{
metadata = new MetadataCacheItem(dataContextType);
}
BaseServiceProvider result;
if (typeof(ObjectContext).IsAssignableFrom(dataContextType))
{
result = new ObjectContextServiceProvider(metadata, dataSourceInstance);
}
else
{
result = new ReflectionServiceProvider(metadata, dataSourceInstance);
}
if (metadataRequiresInitialization)
{
// Populate metadata in provider.
result.PopulateMetadata();
result.AddOperationsFromType(dataServiceType);
// Create and cache configuration, which goes hand-in-hand with metadata.
metadata.Configuration = CreateConfiguration(dataServiceType, result, dataSourceInstance);
metadata.Seal();
MetadataCache.AddCacheItem(dataServiceType, dataSourceInstance, metadata);
}
configuration = metadata.Configuration;
return (IDataServiceProvider)result;
}
///
/// Gets the appropriate encoding specified by the request, taking
/// the format into consideration.
///
/// Content format for response.
/// Accept-Charset header as specified in request.
/// The requested encoding, possibly null.
private static Encoding GetRequestAcceptEncoding(ContentFormat responseFormat, string acceptCharset)
{
if (responseFormat == ContentFormat.Binary)
{
return null;
}
else
{
return HttpProcessUtility.EncodingFromAcceptCharset(acceptCharset);
}
}
///
/// Selects a response format for the host's request and sets the
/// appropriate response header.
///
/// Host with request.
/// An comma-delimited list of client-supported MIME accept types.
/// Whether the target is an entity.
/// The selected response format.
private static ContentFormat SelectResponseFormat(IDataServiceHost host, string acceptTypesText, bool entityTarget)
{
Debug.Assert(host != null, "host != null");
string[] availableTypes;
if (entityTarget)
{
availableTypes = new string[]
{
XmlConstants.MimeApplicationAtom,
XmlConstants.MimeApplicationJson
};
}
else
{
availableTypes = new string[]
{
XmlConstants.MimeApplicationXml,
XmlConstants.MimeTextXml,
XmlConstants.MimeApplicationJson
};
}
string mime = HttpProcessUtility.SelectMimeType(acceptTypesText, availableTypes);
if (mime == null)
{
return ContentFormat.Unsupported;
}
else
{
host.ResponseContentType = mime;
return GetContentFormat(mime);
}
}
/// Validate the given request.
/// Request parameters.
private static void ValidateRequest(CachedRequestParams requestParams)
{
if (!String.IsNullOrEmpty(requestParams.IfMatch) && !String.IsNullOrEmpty(requestParams.IfNoneMatch))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_BothIfMatchAndIfNoneMatchHeaderSpecified);
}
}
///
/// Processes the incoming request, without writing anything to the response body.
///
/// description about the request uri
/// data service to which the request was made.
///
/// A delegate to be called to write the body; null if no body should be written out.
///
private static RequestDescription ProcessIncomingRequest(
RequestDescription description,
IDataService dataService)
{
Debug.Assert(description != null, "description != null");
Debug.Assert(dataService.Host != null, "dataService.Host != null");
CachedRequestParams requestParams = dataService.RequestParams;
CheckVersion(dataService);
CheckETagValues(dataService.RequestParams);
ResourceContainer lastSegmentContainer = description.LastSegmentInfo.TargetContainer;
if (requestParams.AstoriaHttpVerb == AstoriaVerbs.GET)
{
if (lastSegmentContainer != null)
{
dataService.Configuration.CheckResourceRightsForRead(lastSegmentContainer, description.IsSingleResult);
}
}
else if (description.TargetKind == RequestTargetKind.ServiceDirectory)
{
throw DataServiceException.CreateMethodNotAllowed(
Strings.DataService_OnlyGetOperationSupportedOnServiceUrl,
XmlConstants.HttpMethodGet);
}
int statusCode = 200;
bool shouldWriteBody = true;
RequestDescription newDescription = description;
if (description.TargetSource != RequestTargetSource.ServiceOperation)
{
if (requestParams.AstoriaHttpVerb == AstoriaVerbs.POST)
{
newDescription = HandlePostOperation(description, dataService);
if (description.LinkUri)
{
statusCode = 204; // 204 - No Content
shouldWriteBody = false;
}
else
{
statusCode = 201; // 201 - Created.
}
}
else if (requestParams.AstoriaHttpVerb == AstoriaVerbs.PUT ||
requestParams.AstoriaHttpVerb == AstoriaVerbs.MERGE)
{
if (lastSegmentContainer != null)
{
if (requestParams.AstoriaHttpVerb == AstoriaVerbs.PUT)
{
dataService.Configuration.CheckResourceRights(lastSegmentContainer, EntitySetRights.WriteReplace);
}
else
{
dataService.Configuration.CheckResourceRights(lastSegmentContainer, EntitySetRights.WriteMerge);
}
}
// For PUT, the body itself shouldn't be written, but the etag should (unless it's just a link).
shouldWriteBody = !description.LinkUri;
newDescription = HandlePutOperation(description, dataService);
statusCode = 204; // 204 - No Content
}
else if (requestParams.AstoriaHttpVerb == AstoriaVerbs.DELETE)
{
if (lastSegmentContainer != null)
{
dataService.Configuration.CheckResourceRights(lastSegmentContainer, EntitySetRights.WriteDelete);
}
HandleDeleteOperation(description, dataService);
statusCode = 204; // 204 - No Content
shouldWriteBody = false;
}
}
else if (description.TargetKind == RequestTargetKind.VoidServiceOperation)
{
statusCode = 204; // No Content
shouldWriteBody = false;
}
// Set the caching policy appropriately - for the time being, we disable caching.
dataService.Host.ResponseCacheControl = XmlConstants.HttpCacheControlNoCache;
// Always set the version when a payload will be returned, in case other
// headers include links, which may need to be interpreted under version-specific rules.
dataService.Host.ResponseVersion = XmlConstants.DataServiceVersionCurrent;
dataService.Host.ResponseStatusCode = statusCode;
if (shouldWriteBody)
{
dataService.Host.ResponseVersion = XmlConstants.DataServiceVersionCurrent;
// return the description, only if response or something in the response header needs to be written
// for e.g. in PUT operations, we need to write etag to the response header, and
// we can compute the new etag only after we have called save changes.
return newDescription;
}
else
{
return null;
}
}
/// Serializes the results for a request into the body of a response message.
/// Description of the data requested.
/// data service to which the request was made.
/// A delegate that can serialize the body into an IEnumerable.
private static Action SerializeResponseBody(RequestDescription description, IDataService dataService)
{
Debug.Assert(dataService.Provider != null, "dataService.Provider != null");
Debug.Assert(dataService.Host != null, "dataService.Host != null");
CachedRequestParams requestParams = dataService.RequestParams;
// Handle internal system resources.
Action result = HandleInternalResources(description, dataService);
if (result != null)
{
return result;
}
// ETags are not supported if there are more than one resource expected in the response.
if (!description.IsSingleResult || (description.ExpandPaths != null && description.ExpandPaths.Count != 0))
{
if (!String.IsNullOrEmpty(requestParams.IfMatch) || !String.IsNullOrEmpty(requestParams.IfNoneMatch))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_ETagSpecifiedForCollection(requestParams.AbsoluteRequestUri));
}
}
if (requestParams.AstoriaHttpVerb == AstoriaVerbs.PUT ||
requestParams.AstoriaHttpVerb == AstoriaVerbs.MERGE)
{
ResourceContainer container;
object actualEntity = GetContainerAndActualEntityInstance(dataService.Provider, description, out container);
dataService.Host.ResponseETag = WebUtil.GetETagValue(dataService.Provider, actualEntity, container);
return EmptyStreamWriter;
}
// Pick the content format to be used to serialize the body.
Debug.Assert(description.RequestEnumerable != null, "description.RequestEnumerable != null");
ContentFormat responseFormat = SelectResponseFormatForType(
description.LinkUri ? RequestTargetKind.Link : description.TargetKind,
description.TargetElementType,
requestParams.Accept,
description.MimeType,
dataService.Host);
// check for etags first
// If no etag is specified, then do the normal stuff - run the query and serialize the result
if (description.TargetSource == RequestTargetSource.ServiceOperation ||
description.TargetSource == RequestTargetSource.None ||
!description.IsSingleResult)
{
Debug.Assert(
String.IsNullOrEmpty(requestParams.IfMatch) && String.IsNullOrEmpty(requestParams.IfNoneMatch),
"No etag can be specified for collection");
Encoding encoding = GetRequestAcceptEncoding(responseFormat, requestParams.AcceptCharset);
IEnumerator queryResults = WebUtil.GetRequestEnumerator(description.RequestEnumerable);
try
{
bool hasMoved = queryResults.MoveNext();
// If we had to wait until we got a value to determine the valid contents, try that now.
#if ASTORIA_OPEN_OBJECT
if (responseFormat == ContentFormat.Unknown)
{
responseFormat = ResolveUnknownFormat(description, queryResults.Current, dataService);
}
#else
Debug.Assert(responseFormat != ContentFormat.Unknown, "responseFormat != ContentFormat.Unknown");
#endif
dataService.Host.ResponseContentType = HttpProcessUtility.BuildContentType(dataService.Host.ResponseContentType, encoding);
return new ResponseBodyWriter(encoding, hasMoved, dataService, queryResults, description, responseFormat).Write;
}
catch
{
WebUtil.Dispose(queryResults);
throw;
}
}
else
{
return CompareETagAndWriteResponse(description, responseFormat, dataService);
}
}
/// Selects the correct content format for a given resource type.
/// Target resource to return.
/// CLR element type.
/// Accept header value.
/// Required MIME type.
/// Host implementation for this data service.
///
/// The content format for the resource; Unknown if it cannot be determined statically.
///
private static ContentFormat SelectResponseFormatForType(
RequestTargetKind targetKind,
Type elementType,
string acceptTypesText,
string mimeType,
IDataServiceHost host)
{
ContentFormat responseFormat;
if (targetKind == RequestTargetKind.PrimitiveValue)
{
responseFormat = SelectPrimitiveContentType(elementType, acceptTypesText, mimeType, host);
}
#if ASTORIA_OPEN_OBJECT
else if (targetKind != RequestTargetKind.OpenPropertyValue && targetKind != RequestTargetKind.OpenProperty)
#else
else
#endif
{
bool entityTarget = targetKind == RequestTargetKind.Resource;
responseFormat = SelectResponseFormat(host, acceptTypesText, entityTarget);
if (responseFormat == ContentFormat.Unsupported)
{
throw new DataServiceException(415, Strings.DataServiceException_UnsupportedMediaType);
}
}
#if ASTORIA_OPEN_OBJECT
else
{
// We cannot negotiate a format until we know what the value is for the object.
responseFormat = ContentFormat.Unknown;
}
#endif
return responseFormat;
}
/// Selects the correct content format for a primitive type.
/// CLR element type.
/// Accept header value.
/// Required MIME type, possibly null.
/// Host implementation for this data service.
/// The content format for the resource.
private static ContentFormat SelectPrimitiveContentType(Type targetElementType, string acceptTypesText, string requiredContentType, IDataServiceHost host)
{
Debug.Assert(targetElementType != null, "targetElementType != null");
string contentType;
ContentFormat responseFormat = WebUtil.GetResponseFormatForPrimitiveValue(targetElementType, out contentType);
requiredContentType = requiredContentType ?? contentType;
host.ResponseContentType = HttpProcessUtility.SelectRequiredMimeType(
acceptTypesText, // acceptTypesText
new string[] { requiredContentType }, // exactContentType
requiredContentType); // inexactContentType
return responseFormat;
}
/// Handles POST requests.
/// description about the target request
/// data service to which the request was made.
/// a new request description object, containing information about the response payload
private static RequestDescription HandlePostOperation(RequestDescription description, IDataService dataService)
{
Debug.Assert(
description.TargetSource != RequestTargetSource.ServiceOperation,
"TargetSource != ServiceOperation -- should have been handled in request URI processing");
CachedRequestParams requestParams = dataService.RequestParams;
if (!String.IsNullOrEmpty(requestParams.IfMatch) || !String.IsNullOrEmpty(requestParams.IfNoneMatch))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_ETagSpecifiedForPost);
}
if (description.IsSingleResult)
{
throw DataServiceException.CreateMethodNotAllowed(
Strings.BadRequest_InvalidUriForPostOperation(requestParams.AbsoluteRequestUri),
dataService.Configuration.GetAllowedMethods(description));
}
Debug.Assert(
description.TargetSource == RequestTargetSource.EntitySet ||
#if ASTORIA_OPEN_OBJECT
description.TargetKind == RequestTargetKind.OpenProperty ||
#endif
description.Property.Kind == ResourcePropertyKind.ResourceSetReference,
"Only ways to have collections of resources");
Stream requestStream = dataService.Host.RequestStream;
if (requestStream == null)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_NullRequestStream);
}
string mimeType;
System.Text.Encoding encoding;
HttpProcessUtility.ReadContentType(dataService.Host.RequestContentType, out mimeType, out encoding);
ContentFormat requestFormat = WebUtil.SelectRequestFormat(mimeType, description);
object entity = null;
Deserializer deserializer = null;
try
{
switch (requestFormat)
{
case ContentFormat.Json:
StreamReader streamReader = new StreamReader(requestStream, encoding);
deserializer = new JsonDeserializer(
streamReader,
false /*update*/,
dataService,
UpdateTracker.CreateUpdateTracker(description, dataService.Provider));
break;
case ContentFormat.Atom:
SyndicationFormatterFactory factory = new Atom10FormatterFactory();
deserializer = new SyndicationDeserializer(
requestStream, // stream
encoding, // encoding
dataService, // dataService
false, // update
factory,
UpdateTracker.CreateUpdateTracker(description, dataService.Provider)); // factory
break;
case ContentFormat.PlainXml:
deserializer = new PlainXmlDeserializer(
requestStream,
encoding,
dataService,
false /*update*/,
UpdateTracker.CreateUpdateTracker(description, dataService.Provider));
break;
default:
throw new DataServiceException(415, Strings.BadRequest_UnsupportedMediaForPost(mimeType));
}
Debug.Assert(deserializer != null, "deserializer != null");
entity = deserializer.HandlePostRequest(description);
Debug.Assert(entity != null, "entity != null");
if (deserializer.Tracker != null)
{
deserializer.Tracker.FireNotifications(dataService.Instance);
}
return RequestDescription.CreateSingleResultRequestDescription(
description, entity, description.LastSegmentInfo.TargetContainer);
}
finally
{
WebUtil.Dispose(deserializer);
}
}
/// Handles PUT requests.
/// description about the target request
/// data service to which the request was made.
/// new request description which contains the info about the entity resource getting modified.
private static RequestDescription HandlePutOperation(RequestDescription description, IDataService dataService)
{
Debug.Assert(description.TargetSource != RequestTargetSource.ServiceOperation, "description.TargetSource != RequestTargetSource.ServiceOperation");
if (!description.IsSingleResult)
{
throw DataServiceException.CreateMethodNotAllowed(
Strings.BadRequest_InvalidUriForPutOperation(dataService.RequestParams.AbsoluteRequestUri),
dataService.Configuration.GetAllowedMethods(description));
}
if (!String.IsNullOrEmpty(dataService.RequestParams.IfNoneMatch))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_IfNoneMatchHeaderNotSupportedInPut);
}
else if (description.LinkUri && !String.IsNullOrEmpty(dataService.RequestParams.IfMatch))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_ETagsNotAllowedForLinkOperations);
}
else if (description.Property != null && description.Property.IsOfKind(ResourcePropertyKind.Key))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_CannotUpdateKeyProperties(description.Property.Name));
}
Stream requestStream = dataService.Host.RequestStream;
if (requestStream == null)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_NullRequestStream);
}
return Deserializer.HandlePutRequest(description, dataService, requestStream);
}
/// Handles DELETE requests.
/// description about the target request
/// data service to which the request was made.
private static void HandleDeleteOperation(RequestDescription description, IDataService dataService)
{
Debug.Assert(description != null, "description != null");
Debug.Assert(description.TargetSource != RequestTargetSource.ServiceOperation, "description.TargetSource != RequestTargetSource.ServiceOperation");
Debug.Assert(dataService != null, "dataService != null");
Debug.Assert(dataService.Configuration != null, "dataService.Configuration != null");
Debug.Assert(dataService.RequestParams != null, "dataService.RequestParams != null");
// In general, deletes are only supported on resource referred via top level sets or collection properties.
// If its the open property case, the key must be specified
// or you can unbind relationships using delete
if (description.LinkUri)
{
HandleUnbindOperation(description, dataService);
}
else if (
#if ASTORIA_OPEN_OBJECT
(description.TargetKind == RequestTargetKind.OpenProperty) ||
#endif
(description.IsSingleResult && description.TargetKind == RequestTargetKind.Resource))
{
#if ASTORIA_OPEN_OBJECT
Debug.Assert(
description.LastSegmentInfo.TargetContainer != null || description.TargetKind == RequestTargetKind.OpenProperty,
"description.LastSegmentInfo.TargetContainer != null || TargetKind == OpenProperty");
#endif
if (description.RequestEnumerable == null)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_ResourceCanBeCrossReferencedOnlyForBindOperation);
}
// Get the single entity result
// We have to query for the delete case, since we don't know the type of the resource
object entity = Deserializer.GetResource(description.LastSegmentInfo, null, dataService, true /*checkForNull*/);
ResourceContainer container = description.LastSegmentInfo.TargetContainer;
//
object actualEntity = dataService.Provider.ResolveResource(entity);
// For open properties, we need to make sure that they refer to resource types
#if ASTORIA_OPEN_OBJECT
if (description.TargetKind == RequestTargetKind.OpenProperty)
{
// Verify that the resource is an entity type. Otherwise we need to throw
ResourceType resourceType = dataService.Provider.GetResourceType(actualEntity.GetType());
if (resourceType == null || resourceType.ResourceTypeKind != ResourceTypeKind.EntityType)
{
throw DataServiceException.CreateBadRequestError(
Strings.DataService_TypeNotValidForDeleteOperation(dataService.RequestParams.AbsoluteRequestUri));
}
container = dataService.Provider.GetContainerForResourceType(resourceType.Type);
}
#endif
if (description.Property != null)
{
Debug.Assert(container != null, "container != null");
dataService.Configuration.CheckResourceRights(container, EntitySetRights.WriteDelete);
}
CheckForETagInDeleteOperation(actualEntity, entity, container, dataService.RequestParams, dataService.Provider);
dataService.Provider.DeleteResource(entity);
//
#if ASTORIA_OPEN_OBJECT
if (description.TargetKind != RequestTargetKind.OpenProperty)
#endif
{
UpdateTracker.FireNotification(dataService.Instance, actualEntity, container, UpdateOperations.Delete);
}
}
else if (description.TargetKind == RequestTargetKind.PrimitiveValue)
{
Debug.Assert(description.TargetSource == RequestTargetSource.Property, "description.TargetSource == RequestTargetSource.Property");
Debug.Assert(description.IsSingleResult, "description.IsSingleResult");
if (description.Property != null && description.Property.IsOfKind(ResourcePropertyKind.Key))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_CannotUpdateKeyProperties(description.Property.Name));
}
else if (description.Property.Type.IsValueType)
{
// 403 - Forbidden
throw new DataServiceException(403, Strings.BadRequest_CannotNullifyValueTypeProperty);
}
// We have to issue the query to get the resource
object securityResource; // Resource on which security check can be made (possibly entity parent of 'resource').
ResourceContainer container; // Resource Container to which the parent entity belongs to.
object resource = Deserializer.GetResourceToModify(description, dataService, false /*allowCrossReference*/, out securityResource, out container);
//
object actualEntity = dataService.Provider.ResolveResource(securityResource);
CheckForETagInDeleteOperation(actualEntity, securityResource, container, dataService.RequestParams, dataService.Provider);
// Doesn't matter which content format we pass here, since the value we are setting to is null
Deserializer.ModifyResource(description, resource, null, ContentFormat.Text, dataService.Provider);
UpdateTracker.FireNotification(dataService.Instance, actualEntity, container, UpdateOperations.Change);
}
#if ASTORIA_OPEN_OBJECT
else if (description.TargetKind == RequestTargetKind.OpenPropertyValue)
{
object securityResource;
object resource = Deserializer.GetResourceToModify(description, dataService, out securityResource);
//
object actualEntity = dataService.Provider.ResolveResource(resource);
ResourceContainer container = dataService.Provider.GetContainerForResourceType(actualEntity.GetType());
CheckForETagInDeleteOperation(actualEntity, resource, container, dataService.RequestParams, dataService.Provider);
// Doesn't matter which content format we pass here, since the value we are setting to is null
Deserializer.ModifyResource(description, resource, null, ContentFormat.Text, dataService.Provider);
}
#endif
else
{
throw DataServiceException.CreateMethodNotAllowed(
Strings.BadRequest_InvalidUriForDeleteOperation(dataService.RequestParams.AbsoluteRequestUri),
dataService.Configuration.GetAllowedMethods(description));
}
}
/// Handles a request for an internal resource if applicable.
/// Request description.
/// data service to which the request was made.
///
/// An action that produces the resulting stream; null if the description isn't for an internal resource.
///
private static Action HandleInternalResources(RequestDescription description, IDataService dataService)
{
string[] exactContentType = null;
ContentFormat format = ContentFormat.Unknown;
string mime = null;
if (description.TargetKind == RequestTargetKind.Metadata)
{
exactContentType = new string[] { XmlConstants.MimeMetadata };
format = ContentFormat.MetadataDocument;
mime = HttpProcessUtility.SelectRequiredMimeType(
dataService.RequestParams.Accept, // acceptTypesText
exactContentType, // exactContentType
XmlConstants.MimeApplicationXml); // inexactContentType
}
else if (description.TargetKind == RequestTargetKind.ServiceDirectory)
{
exactContentType = new string[] { XmlConstants.MimeApplicationAtomService, XmlConstants.MimeApplicationJson, XmlConstants.MimeApplicationXml };
mime = HttpProcessUtility.SelectRequiredMimeType(
dataService.RequestParams.Accept, // acceptTypesText
exactContentType, // exactContentType
XmlConstants.MimeApplicationXml); // inexactContentType;
format = GetContentFormat(mime);
}
if (exactContentType != null)
{
Debug.Assert(
format != ContentFormat.Unknown,
"format(" + format + ") != ContentFormat.Unknown -- otherwise exactContentType should be null");
Encoding encoding = HttpProcessUtility.EncodingFromAcceptCharset(dataService.RequestParams.AcceptCharset);
dataService.Host.ResponseContentType = HttpProcessUtility.BuildContentType(mime, encoding);
return new ResponseBodyWriter(
encoding,
false, // hasMoved
dataService,
null, // queryResults
description,
format).Write;
}
return null;
}
///
/// Compare the ETag value and then serialize the value if required
///
/// Description of the uri requested.
/// Content format for response.
/// Data service to which the request was made.
/// A delegate that can serialize the result.
private static Action CompareETagAndWriteResponse(
RequestDescription description,
ContentFormat responseFormat,
IDataService dataService)
{
Debug.Assert(description != null, "description != null");
Debug.Assert(dataService != null, "dataService != null");
CachedRequestParams requestParams = dataService.RequestParams;
Debug.Assert(
String.IsNullOrEmpty(requestParams.IfMatch) || String.IsNullOrEmpty(requestParams.IfNoneMatch),
"Both If-Match and If-None-Match header cannot be specified");
IEnumerator queryResults = null;
try
{
if (requestParams.AstoriaHttpVerb == AstoriaVerbs.GET)
{
bool writeResponse = true;
// Get the index of the last resource in the request uri
int parentResourceIndex = description.GetIndexOfTargetEntityResource();
SegmentInfo parentEntitySegment = description.SegmentInfos[parentResourceIndex];
queryResults = RequestDescription.GetSingleResultFromEnumerable(parentEntitySegment);
object resource = queryResults.Current;
string etagValue = null;
if (description.LinkUri)
{
// No need to worry about etags while performing link operations
// No etags can be specified also while performing link operations
if (requestParams.IfMatch != null || requestParams.IfNoneMatch != null)
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_ETagsNotAllowedForLinkOperations);
}
if (resource == null)
{
throw DataServiceException.CreateResourceNotFound(description.LastSegmentInfo.Identifier);
}
}
else
{
ResourceContainer container = null;
if (resource != null)
{
container = WebUtil.GetResourceContainer(resource, parentEntitySegment, dataService.Provider);
}
etagValue = WebUtil.CompareAndGetETag(
resource, resource, container, dataService.Provider, requestParams, out writeResponse);
if (resource == null && description.TargetKind == RequestTargetKind.Resource)
{
Debug.Assert(description.Property != null, "non-open type property");
// If you are querying reference nav property and the value is null,
// return 204 - No Content e.g. /Customers(1)/BestFriend
dataService.Host.ResponseStatusCode = 204; // No Content
return EmptyStreamWriter;
}
WriteETagValueInResponseHeader(description, etagValue, dataService.Host);
}
if (writeResponse)
{
int lastResourceIndex = description.GetIndexOfTargetEntityResource();
return WriteSingleElementResponse(description, responseFormat, queryResults, lastResourceIndex, etagValue, dataService);
}
else
{
dataService.Host.ResponseStatusCode = 304; // Not Modified
return EmptyStreamWriter;
}
}
else
{
Debug.Assert(requestParams.AstoriaHttpVerb == AstoriaVerbs.POST, "Must be POST method");
ResourceContainer container;
object actualEntity = GetContainerAndActualEntityInstance(dataService.Provider, description, out container);
dataService.Host.ResponseLocation = Serializer.GetUri(actualEntity, dataService.Provider, container, requestParams.AbsoluteServiceUri).AbsoluteUri;
string etagValue = WebUtil.GetETagValue(dataService.Provider, actualEntity, container);
queryResults = RequestDescription.GetSingleResultFromEnumerable(description.LastSegmentInfo);
return WriteSingleElementResponse(description, responseFormat, queryResults, description.SegmentInfos.Length - 1, etagValue, dataService);
}
}
catch
{
WebUtil.Dispose(queryResults);
throw;
}
}
#if ASTORIA_OPEN_OBJECT
/// Resolves the content format required when it is statically unknown.
/// Request description.
/// Result target.
/// data service to which the request was made.
/// The format for the specified element.
private static ContentFormat ResolveUnknownFormat(RequestDescription description, object element, IDataService dataService)
{
Debug.Assert(
description.TargetKind == RequestTargetKind.OpenProperty ||
description.TargetKind == RequestTargetKind.OpenPropertyValue,
description.TargetKind + " is open property or open property value");
WebUtil.CheckResourceExists(element != null, description.LastSegmentInfo.Identifier);
Type elementType = element.GetType();
ResourceType resourceType = dataService.Provider.GetResourceType(elementType);
// This resource wouldn't be visible during serialization, so we treat is as 404.
if (resourceType == null)
{
throw new InvalidOperationException(Strings.DataService_InvalidResourceType(elementType.FullName));
}
// Determine the appropriate target type based on the kind of resource.
bool rawValue = description.TargetKind == RequestTargetKind.OpenPropertyValue;
RequestTargetKind targetKind;
switch (resourceType.ResourceTypeKind)
{
case ResourceTypeKind.ComplexType:
if (rawValue)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_ValuesCanBeReturnedForPrimitiveTypesOnly);
}
else
{
targetKind = RequestTargetKind.ComplexObject;
}
break;
case ResourceTypeKind.Primitive:
if (rawValue)
{
targetKind = RequestTargetKind.PrimitiveValue;
}
else
{
targetKind = RequestTargetKind.Primitive;
}
break;
default:
Debug.Assert(ResourceTypeKind.EntityType == resourceType.ResourceTypeKind, "ResourceTypeKind.EntityType == " + resourceType.ResourceTypeKind);
if (rawValue)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_ValuesCanBeReturnedForPrimitiveTypesOnly);
}
else
{
targetKind = RequestTargetKind.Resource;
}
break;
}
if (description.LinkUri)
{
targetKind = RequestTargetKind.Link;
}
return SelectResponseFormatForType(targetKind, elementType, dataService.RequestParams.Accept, null, dataService.Host);
}
#endif
///
/// Compare the ETag value and then serialize the value if required
///
/// Description of the uri requested.
/// format of the response
/// Enumerator whose current resource points to the resource which needs to be written
/// index of the segment info that represents the last resource
/// etag value for the resource specified in parent resource parameter
/// data service to which the request was made.
/// A delegate that can serialize the result.
private static Action WriteSingleElementResponse(
RequestDescription description,
ContentFormat responseFormat,
IEnumerator queryResults,
int parentResourceIndex,
string etagValue,
IDataService dataService)
{
try
{
if (parentResourceIndex != description.SegmentInfos.Length - 1)
{
// Dispose the old enumerator
WebUtil.Dispose(queryResults);
// get the resource which need to be written
queryResults = RequestDescription.GetSingleResultFromEnumerable(description.LastSegmentInfo);
}
// If we had to wait until we got a value to determine the valid contents, try that now.
#if ASTORIA_OPEN_OBJECT
if (responseFormat == ContentFormat.Unknown)
{
responseFormat = ResolveUnknownFormat(description, queryResults.Current, dataService);
}
#else
Debug.Assert(responseFormat != ContentFormat.Unknown, "responseFormat != ContentFormat.Unknown");
#endif
// Write the etag header
WriteETagValueInResponseHeader(description, etagValue, dataService.Host);
Encoding encoding = GetRequestAcceptEncoding(responseFormat, dataService.RequestParams.AcceptCharset);
dataService.Host.ResponseContentType = HttpProcessUtility.BuildContentType(dataService.Host.ResponseContentType, encoding);
return new ResponseBodyWriter(
encoding,
true /* hasMoved */,
dataService,
queryResults,
description,
responseFormat).Write;
}
catch
{
WebUtil.Dispose(queryResults);
throw;
}
}
///
/// Write the etag header value in the response
///
/// description about the request made
/// etag value that needs to be written.
/// Host implementation for this data service.
private static void WriteETagValueInResponseHeader(RequestDescription requestDescription, string etagValue, IDataServiceHost host)
{
Debug.Assert(requestDescription.IsSingleResult, "requestDescription.IsSingleResult");
if ((requestDescription.ExpandPaths == null || requestDescription.ExpandPaths.Count == 0)
&& !String.IsNullOrEmpty(etagValue))
{
host.ResponseETag = etagValue;
}
}
///
/// Returns the actual entity instance and its containers for the resource in the description results.
///
/// Data provider
/// description about the request made.
/// returns the container to which the result resource belongs to.
/// returns the actual entity instance for the given resource.
private static object GetContainerAndActualEntityInstance(
IDataServiceProvider provider, RequestDescription description, out ResourceContainer container)
{
// For POST operations, we need to resolve the entity only after save changes. Hence we need to do this at the serialization
// to make sure save changes has been called
object[] results = (object[])description.RequestEnumerable;
Debug.Assert(results != null && results.Length == 1, "results != null && results.Length == 1");
// Make a call to the provider to get the exact resource instance back
results[0] = provider.ResolveResource(results[0]);
container = description.LastSegmentInfo.TargetContainer;
#if ASTORIA_OPEN_OBJECT
if (container == null)
{
// Open types will not have TargetContainer set, but they don't support MEST either.
Debug.Assert(
RequestTargetKind.OpenProperty == description.LastSegmentInfo.TargetKind,
"RequestTargetKind.OpenProperty == description.LastSegmentInfo.TargetKind(" + description.LastSegmentInfo.TargetKind + " - otherwise, why is TargetContainer null for POST target?");
container = provider.GetContainerForResourceType(results[0].GetType());
Debug.Assert(
container != null,
"container != null -- otherwise results[0].GetType() (" + results[0].GetType() + ") didn't work.");
}
#else
Debug.Assert(container != null, "description.LastSegmentInfo.TargetContainer != null");
#endif
return results[0];
}
///
/// Check for etag values for the given resource in DeleteOperation
///
/// resource whose etag value needs to be compared to the one given in the request header
/// token as returned by the IUpdatable.GetResource method.
/// resource container to which the resource belongs to.
/// request headers
/// Data provider
private static void CheckForETagInDeleteOperation(
object actualEntityInstance,
object entityToken,
ResourceContainer container,
CachedRequestParams requestParams,
IDataServiceProvider provider)
{
Debug.Assert(actualEntityInstance != null, "actualEntityInstance != null");
Debug.Assert(entityToken != null, "entityToken != null");
// If this method is called for Update, we need to pass the token object as well as the actual instance.
// The actual instance is used to determine the type that's necessary to find out the etag properties.
// The token is required to pass back to IUpdatable interface, if we need to get the values for etag properties.
if (!String.IsNullOrEmpty(requestParams.IfNoneMatch))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_IfNoneMatchHeaderNotSupportedInDelete);
}
ICollection etagProperties = provider.GetETagProperties(container.Name, actualEntityInstance.GetType());
if (etagProperties.Count == 0)
{
if (requestParams.IfMatch != null)
{
throw DataServiceException.CreateBadRequestError(Strings.Serializer_NoETagPropertiesForType);
}
}
else if (String.IsNullOrEmpty(requestParams.IfMatch))
{
string typeName = WebUtil.GetTypeName(provider, actualEntityInstance.GetType());
throw DataServiceException.CreateBadRequestError(Strings.DataService_CannotPerformDeleteOperationWithoutETag(typeName));
}
else if (requestParams.IfMatch != XmlConstants.HttpAnyETag)
{
string etagValue = WebUtil.GetETagValue(entityToken, etagProperties, provider);
if (etagValue != requestParams.IfMatch)
{
throw DataServiceException.CreatePreConditionFailedError(Strings.Serializer_ETagValueDoesNotMatch);
}
}
}
/// No-op method for a stream-writing action.
/// Stream to write to.
private static void EmptyStreamWriter(Stream stream)
{
}
///
/// Handles the unbind operations
///
/// description about the request made.
/// data service to which the request was made.
private static void HandleUnbindOperation(RequestDescription description, IDataService dataService)
{
Debug.Assert(description.LinkUri, "This method must be called for link operations");
Debug.Assert(description.IsSingleResult, "Expecting this method to be called on single resource uris");
object parentEntity;
Deserializer.GetResourceToModify(description, dataService, out parentEntity);
if (description.Property != null)
{
if (description.Property.Kind == ResourcePropertyKind.ResourceReference)
{
dataService.Provider.SetReference(parentEntity, description.Property.Name, null);
}
else
{
Debug.Assert(description.Property.Kind == ResourcePropertyKind.ResourceSetReference, "expecting collection nav properties");
Debug.Assert(description.LastSegmentInfo.HasKeyValues, "expecting properties to have key value specified");
object childEntity = Deserializer.GetResource(description.LastSegmentInfo, null, dataService, true /*checkForNull*/);
dataService.Provider.RemoveReferenceFromCollection(parentEntity, description.Property.Name, childEntity);
}
}
else
{
if (description.LastSegmentInfo.HasKeyValues)
{
object childEntity = Deserializer.GetResource(description.LastSegmentInfo, null, dataService, true /*checkForNull*/);
dataService.Provider.RemoveReferenceFromCollection(parentEntity, description.ContainerName, childEntity);
}
else
{
dataService.Provider.SetReference(parentEntity, description.ContainerName, null);
}
}
}
///
/// Get the content format corresponding to the given mime type.
///
/// mime type for the request.
/// content format mapping to the given mime type.
private static ContentFormat GetContentFormat(string mime)
{
if (mime == XmlConstants.MimeApplicationJson)
{
return ContentFormat.Json;
}
else if (mime == XmlConstants.MimeApplicationAtom)
{
return ContentFormat.Atom;
}
else
{
Debug.Assert(
mime == XmlConstants.MimeApplicationXml || mime == XmlConstants.MimeTextXml,
"expecting application/xml or plain/xml, got " + mime);
return ContentFormat.PlainXml;
}
}
///
/// Handle the request - whether its a batch request or a non-batch request
///
/// Returns the delegate for writing the response
private Action HandleRequest()
{
Debug.Assert(this.host != null, "this.host != null");
Action writer;
try
{
if (this.host is HttpContextServiceHost)
{
((HttpContextServiceHost)this.host).VerifyQueryParameters();
}
RequestDescription description = this.ProcessIncomingRequestUriAndCacheHeaders();
this.OnStartProcessingRequest(new ProcessRequestArgs(this.requestParams.AbsoluteRequestUri, false /*isBatchOperation*/));
if (description.TargetKind != RequestTargetKind.Batch)
{
writer = this.HandleNonBatchRequest(description);
}
else
{
writer = this.HandleBatchRequest();
}
}
catch (Exception exception)
{
// Exception should be re-thrown if not handled.
if (!WebUtil.IsCatchableExceptionType(exception))
{
throw;
}
string accept = (this.requestParams != null) ? this.requestParams.Accept : null;
string acceptCharset = (this.requestParams != null) ? this.requestParams.AcceptCharset : null;
writer = ErrorHandler.HandleBeforeWritingException(exception, this, accept, acceptCharset);
}
Debug.Assert(writer != null, "writer != null");
return writer;
}
///
/// Handle non-batch requests
///
/// description about the request uri.
/// Returns the delegate which takes the response stream for writing the response.
private Action HandleNonBatchRequest(RequestDescription description)
{
Debug.Assert(description.TargetKind != RequestTargetKind.Batch, "description.TargetKind != RequestTargetKind.Batch");
description = ProcessIncomingRequest(description, this);
if (this.requestParams.AstoriaHttpVerb != AstoriaVerbs.GET)
{
this.provider.SaveChanges();
}
return (description == null) ? EmptyStreamWriter : SerializeResponseBody(description, this);
}
/// Handle the batch request.
/// Returns the delegate which takes the response stream for writing the response.
private Action HandleBatchRequest()
{
// Verify the HTTP method.
if (this.requestParams.AstoriaHttpVerb != AstoriaVerbs.POST)
{
throw DataServiceException.CreateMethodNotAllowed(
Strings.DataService_BatchResourceOnlySupportsPost,
XmlConstants.HttpMethodPost);
}
CheckVersion(this);
// Verify the content type and get the boundary string
Encoding encoding;
string boundary;
if (!BatchStream.GetBoundaryAndEncodingFromMultipartMixedContentType(this.requestParams.ContentType, out boundary, out encoding) ||
String.IsNullOrEmpty(boundary))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_InvalidContentTypeForBatchRequest);
}
// Write the response headers
this.host.ResponseStatusCode = 202; // OK
this.host.ResponseCacheControl = XmlConstants.HttpCacheControlNoCache;
string batchBoundary = XmlConstants.HttpMultipartBoundaryBatchResponse + '_' + Guid.NewGuid().ToString();
this.host.ResponseContentType = String.Format(
System.Globalization.CultureInfo.InvariantCulture,
"{0}; {1}={2}",
XmlConstants.MimeMultiPartMixed,
XmlConstants.HttpMultipartBoundary,
batchBoundary);
BatchStream batchStream = new BatchStream(this.host.RequestStream, boundary, encoding, true);
this.batchDataService = new BatchDataService(this, batchStream, batchBoundary);
return this.batchDataService.HandleBatchContent;
}
/// Creates the provider and configuration as necessary to be used for this request.
private void EnsureProviderAndConfigForRequest()
{
if (this.provider == null)
{
Type dataServiceType = this.GetType();
object dataSourceInstance = this.CreateDataSource();
if (dataSourceInstance == null)
{
throw new InvalidOperationException(Strings.DataService_CreateDataSourceNull);
}
this.provider = CreateProvider(dataServiceType, dataSourceInstance, out this.configuration);
}
else
{
Debug.Assert(this.configuration != null, "this.configuration != null -- otherwise this.provider was ----signed with no configuration");
}
}
///
/// Processes the incoming request and cache all the request headers
///
/// description about the request uri.
private RequestDescription ProcessIncomingRequestUriAndCacheHeaders()
{
this.requestParams = new CachedRequestParams(
this.host.RequestAccept,
this.host.RequestAcceptCharSet,
this.host.RequestContentType,
this.host.RequestHttpMethod,
this.host.RequestIfMatch,
this.host.RequestIfNoneMatch,
this.host.RequestVersion,
this.host.RequestMaxVersion,
RequestUriProcessor.GetAbsoluteRequestUri(this.host),
RequestUriProcessor.GetServiceUri(this.host));
ValidateRequest(this.requestParams);
return RequestUriProcessor.ProcessRequestUri(this.requestParams.AbsoluteRequestUri, this);
}
#endregion Private methods.
///
/// Dummy data service for batch requests
///
private class BatchDataService : IDataService
{
#region Private fields.
/// Original data service instance.
private readonly IDataService dataService;
/// batch stream which reads the content of the batch from the underlying request stream.
private readonly BatchStream batchRequestStream;
/// batch response seperator string.
private readonly string batchBoundary;
/// Hashset to make sure that the content ids specified in the batch are all unique.
private readonly HashSet contentIds = new HashSet(new Int32EqualityComparer());
/// Dictionary to track objects represented by each content id within a changeset.
private readonly Dictionary contentIdsToSegmentInfoMapping = new Dictionary(StringComparer.Ordinal);
/// Number of changset/query operations encountered in the current batch.
private int batchElementCount;
/// Whether the batch limit has been exceeded (implies no further processing should take place).
private bool batchLimitExceeded;
/// List of the all request description within a changeset.
private List batchRequestDescription = new List();
/// List of the all response headers and results of each operation within a changeset.
private List batchRequestHost = new List();
/// Number of CUD operations encountered in the current changeset.
private int changeSetElementCount;
/// Batch Host which caches the request headers and response headers per operation within a changeset.
private IDataServiceHost host;
#endregion Private fields.
///
/// Creates an instance of the batch data service which keeps track of the
/// request and response headers per operation in the batch
///
/// original data service to which the batch request was made
/// batch stream which read batch content from the request stream
/// batch response seperator string.
internal BatchDataService(IDataService dataService, BatchStream batchRequestStream, string batchBoundary)
{
Debug.Assert(dataService != null, "dataService != null");
Debug.Assert(batchRequestStream != null, "batchRequestStream != null");
Debug.Assert(batchBoundary != null, "batchBoundary != null");
this.dataService = dataService;
this.batchRequestStream = batchRequestStream;
this.batchBoundary = batchBoundary;
}
#region IDataService Members
/// Service configuration information.
DataServiceConfiguration IDataService.Configuration
{
get { return this.dataService.Configuration; }
}
/// Host implementation for the batch data service.
IDataServiceHost IDataService.Host
{
get { return this.host; }
}
/// Data provider for this data service.
IDataServiceProvider IDataService.Provider
{
get { return this.dataService.Provider; }
}
/// Instance of the data provider.
IDataService IDataService.Instance
{
get { return this.dataService.Instance; }
}
/// Gets the cached request headers.
CachedRequestParams IDataService.RequestParams
{
get { return ((BatchServiceHost)this.host).RequestParams; }
}
///
/// This method is called during query processing to validate and customize
/// paths for the $expand options are applied by the provider.
///
/// Query which will be composed.
/// Collection of segment paths to be expanded.
void IDataService.InternalApplyingExpansions(IQueryable queryable, ICollection expandPaths)
{
this.dataService.InternalApplyingExpansions(queryable, expandPaths);
}
/// Processes a catchable exception.
/// The arguments describing how to handle the exception.
void IDataService.InternalHandleException(HandleExceptionArgs args)
{
this.dataService.InternalHandleException(args);
}
///
/// Returns the segmentInfo of the resource referred by the given content Id;
///
/// content id for a operation in the batch request.
/// segmentInfo for the resource referred by the given content id.
SegmentInfo IDataService.GetSegmentForContentId(string contentId)
{
if (contentId.StartsWith("$", StringComparison.Ordinal))
{
SegmentInfo segmentInfo;
this.contentIdsToSegmentInfoMapping.TryGetValue(contentId.Substring(1), out segmentInfo);
return segmentInfo;
}
return null;
}
///
/// Get the resource referred by the segment in the request with the given index
///
/// description about the request url.
/// index of the segment that refers to the resource that needs to be returned.
/// typename of the resource.
/// the resource as returned by the provider.
object IDataService.GetResource(RequestDescription description, int segmentIndex, string typeFullName)
{
if (description.SegmentInfos[0].Identifier.StartsWith("$", StringComparison.Ordinal))
{
Debug.Assert(segmentIndex >= 0 && segmentIndex < description.SegmentInfos.Length, "segment index must be a valid one");
if (description.SegmentInfos[segmentIndex].RequestEnumerable == null)
{
object resource = GetResourceFromSegmentEnumerable(description.SegmentInfos[0]);
for (int i = 1; i <= segmentIndex; i++)
{
resource = ((IDataService)this).Provider.GetValue(resource, description.SegmentInfos[i].Identifier);
if (resource == null)
{
throw DataServiceException.CreateBadRequestError(Strings.BadRequest_DereferencingNullPropertyValue(description.SegmentInfos[i].Identifier));
}
description.SegmentInfos[i].RequestEnumerable = new object[] { resource };
}
return resource;
}
else
{
return GetResourceFromSegmentEnumerable(description.SegmentInfos[segmentIndex]);
}
}
return Deserializer.GetResource(description.SegmentInfos[segmentIndex], typeFullName, ((IDataService)this), false /*checkForNull*/);
}
///
/// Dispose the data source instance
///
void IDataService.DisposeDataSource()
{
this.dataService.DisposeDataSource();
}
///
/// This method is called before a request is processed.
///
/// Information about the request that is going to be processed.
void IDataService.InternalOnStartProcessingRequest(ProcessRequestArgs args)
{
this.dataService.InternalOnStartProcessingRequest(args);
}
#endregion
///
/// Handle the batch content
///
/// response stream for writing batch response
internal void HandleBatchContent(Stream responseStream)
{
BatchServiceHost batchHost = null;
RequestDescription description;
string changesetBoundary = null;
Exception exceptionEncountered = null;
try
{
StreamWriter writer = new StreamWriter(responseStream, HttpProcessUtility.FallbackEncoding);
while (!this.batchLimitExceeded && this.batchRequestStream.State != BatchStreamState.EndBatch)
{
// clear the host from the last operation
this.host = null;
// If we encounter any error while reading the batch request,
// we write out the exception message and return. We do not try
// and read the request further.
try
{
this.batchRequestStream.MoveNext();
}
catch (Exception exception)
{
if (!WebUtil.IsCatchableExceptionType(exception))
{
throw;
}
ErrorHandler.HandleBatchRequestException(this, exception, writer);
break;
}
try
{
switch (this.batchRequestStream.State)
{
case BatchStreamState.BeginChangeSet:
this.IncreaseBatchCount();
changesetBoundary = XmlConstants.HttpMultipartBoundaryChangesetResponse + '_' + Guid.NewGuid().ToString();
BatchWriter.WriteStartBatchBoundary(writer, this.batchBoundary, changesetBoundary);
break;
case BatchStreamState.EndChangeSet:
#region EndChangeSet
this.changeSetElementCount = 0;
this.contentIdsToSegmentInfoMapping.Clear();
// In case of exception, the changeset boundary will be set to null.
// for that case, just write the end boundary and continue
if (exceptionEncountered == null)
{
Debug.Assert(!String.IsNullOrEmpty(changesetBoundary), "!String.IsNullOrEmpty(changesetBoundary)");
// Save all the changes and write the response
this.dataService.Provider.SaveChanges();
Debug.Assert(this.batchRequestHost.Count == this.batchRequestDescription.Count, "counts must be the same");
for (int i = 0; i < this.batchRequestDescription.Count; i++)
{
this.host = this.batchRequestHost[i];
this.WriteRequest(this.batchRequestDescription[i], this.batchRequestHost[i]);
}
BatchWriter.WriteEndBoundary(writer, changesetBoundary);
}
else
{
this.HandleChangesetException(exceptionEncountered, this.batchRequestHost, changesetBoundary, writer);
}
break;
#endregion //EndChangeSet
case BatchStreamState.Get:
#region GET Operation
this.IncreaseBatchCount();
batchHost = CreateHostFromHeaders(
this.dataService.Host,
this.batchRequestStream,
this.contentIds,
this.batchBoundary,
writer);
this.host = batchHost;
// it must be GET operation
Debug.Assert(this.host.RequestHttpMethod == XmlConstants.HttpMethodGet, "this.host.RequestHttpMethod == XmlConstants.HttpMethodGet");
Debug.Assert(this.batchRequestDescription.Count == 0, "this.batchRequestDescription.Count == 0");
Debug.Assert(this.batchRequestHost.Count == 0, "this.batchRequestHost.Count == 0");
this.dataService.InternalOnStartProcessingRequest(new ProcessRequestArgs(this.host.AbsoluteRequestUri, true /*isBatchOperation*/));
description = RequestUriProcessor.ProcessRequestUri(this.host.AbsoluteRequestUri, this);
description = ProcessIncomingRequest(description, this);
this.WriteRequest(description, batchHost);
break;
#endregion // GET Operation
case BatchStreamState.Post:
case BatchStreamState.Put:
case BatchStreamState.Delete:
case BatchStreamState.Merge:
#region CUD Operation
// if we encounter an error, we ignore rest of the operations
// within a changeset.
this.IncreaseChangeSetCount();
batchHost = CreateHostFromHeaders(this.dataService.Host, this.batchRequestStream, this.contentIds, changesetBoundary, writer);
if (exceptionEncountered == null)
{
this.batchRequestHost.Add(batchHost);
this.host = batchHost;
this.dataService.InternalOnStartProcessingRequest(new ProcessRequestArgs(this.host.AbsoluteRequestUri, true /*isBatchOperation*/));
description = RequestUriProcessor.ProcessRequestUri(this.host.AbsoluteRequestUri, this);
description = ProcessIncomingRequest(description, this);
this.batchRequestDescription.Add(description);
// In Link case, we do not write any response out. hence the description will be null
if (this.batchRequestStream.State == BatchStreamState.Post && description != null)
{
Debug.Assert(description.TargetKind == RequestTargetKind.Resource, "The target must be a resource, since otherwise cross-referencing doesn't make sense");
// if the content id is specified, only then add it to the collection
if (batchHost.ContentId != null)
{
this.contentIdsToSegmentInfoMapping.Add(batchHost.ContentId, description.LastSegmentInfo);
}
}
}
break;
#endregion // CUD Operation
default:
Debug.Assert(this.batchRequestStream.State == BatchStreamState.EndBatch, "expecting end batch state");
break;
}
}
catch (Exception exception)
{
if (!WebUtil.IsCatchableExceptionType(exception))
{
throw;
}
if (this.batchRequestStream.State == BatchStreamState.EndChangeSet)
{
this.HandleChangesetException(exception, this.batchRequestHost, changesetBoundary, writer);
}
else if (this.batchRequestStream.State == BatchStreamState.Post ||
this.batchRequestStream.State == BatchStreamState.Put ||
this.batchRequestStream.State == BatchStreamState.Delete ||
this.batchRequestStream.State == BatchStreamState.Merge)
{
// Store the exception if its in the middle of the changeset,
// we need to write the same exception for every
exceptionEncountered = exception;
}
else
{
BatchServiceHost currentHost = (BatchServiceHost)this.host;
if (currentHost == null)
{
// For error cases (like we encounter an error while parsing request headers
// and were not able to create the host), we need to create a dummy host
currentHost = new BatchServiceHost(this.batchBoundary, writer);
}
ErrorHandler.HandleBatchProcessException(this, currentHost, exception, writer);
}
}
finally
{
// Once the end of the changeset is reached, clear the error state
if (this.batchRequestStream.State == BatchStreamState.EndChangeSet)
{
exceptionEncountered = null;
changesetBoundary = null;
this.batchRequestDescription.Clear();
this.batchRequestHost.Clear();
}
}
}
BatchWriter.WriteEndBoundary(writer, this.batchBoundary);
writer.Flush();
}
finally
{
this.batchRequestStream.Dispose();
}
}
#region Private methods.
///
/// Gets the value of the given header from the given header collection
///
/// Dictionary with header names and values.
/// name of the header whose value needs to be returned.
/// value of the given header.
private static string GetValue(Dictionary headers, string headerName)
{
string headerValue;
headers.TryGetValue(headerName, out headerValue);
return headerValue;
}
///
/// Creates a batch host from the given headers
///
/// IDataServiceHost implementation host for this data service.
/// batch stream which contains the header information.
/// content ids that are defined in the batch.
/// Part separator for host.
/// Output writer.
/// instance of the batch host which represents the current operation.
private static BatchServiceHost CreateHostFromHeaders(IDataServiceHost host, BatchStream batchStream, HashSet contentIds, string boundary, StreamWriter writer)
{
Debug.Assert(batchStream != null, "batchStream != null");
Debug.Assert(boundary != null, "boundary != null");
// If the Content-ID header is defined, it should be unique.
string contentIdValue = GetValue(batchStream.ContentHeaders, XmlConstants.HttpContentID);
if (!String.IsNullOrEmpty(contentIdValue))
{
int contentId;
if (!Int32.TryParse(contentIdValue, System.Globalization.NumberStyles.Integer, System.Globalization.NumberFormatInfo.InvariantInfo, out contentId))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_ContentIdMustBeAnInteger(contentId));
}
if (!contentIds.Add(contentId))
{
throw DataServiceException.CreateBadRequestError(Strings.DataService_ContentIdMustBeUniqueInBatch(contentId));
}
}
CachedRequestParams requestParams = CreateRequestParams(host, batchStream);
return new BatchServiceHost(requestParams, batchStream.GetContentStream(), contentIdValue, boundary, writer);
}
///
/// Creates a new instance of CachedRequestParams given the header information
///
/// IDataServiceHost implementation host for this data service.
/// batch stream which contains the header information.
/// instance of the CachedRequestParams with all request header information.
private static CachedRequestParams CreateRequestParams(IDataServiceHost host, BatchStream batchStream)
{
string accept = GetValue(batchStream.ContentHeaders, XmlConstants.HttpRequestAccept);
string acceptCharset = GetValue(batchStream.ContentHeaders, XmlConstants.HttpRequestAcceptCharset);
string contentType = GetValue(batchStream.ContentHeaders, XmlConstants.HttpContentType);
string headerIfMatch = GetValue(batchStream.ContentHeaders, XmlConstants.HttpRequestIfMatch);
string headerIfNoneMatch = GetValue(batchStream.ContentHeaders, XmlConstants.HttpRequestIfNoneMatch);
string version = GetValue(batchStream.ContentHeaders, XmlConstants.HttpDataServiceVersion);
string maxVersion = GetValue(batchStream.ContentHeaders, XmlConstants.HttpMaxDataServiceVersion);
Uri absoluteServiceUri = RequestUriProcessor.GetServiceUri(host);
Uri contentUri = RequestUriProcessor.GetAbsoluteUriFromReference(
batchStream.ContentUri, // reference
absoluteServiceUri); // absoluteServiceUri
return new CachedRequestParams(
accept,
acceptCharset,
contentType,
GetHttpMethodName(batchStream.State),
headerIfMatch,
headerIfNoneMatch,
version,
maxVersion,
contentUri,
absoluteServiceUri);
}
///
/// Returns the http method name given the batch stream state
///
/// state of the batch stream.
/// returns the http method name
private static string GetHttpMethodName(BatchStreamState state)
{
Debug.Assert(
state == BatchStreamState.Get ||
state == BatchStreamState.Post ||
state == BatchStreamState.Put ||
state == BatchStreamState.Delete ||
state == BatchStreamState.Merge,
"Expecting BatchStreamState (" + state + ") to be Delete, Get, Post or Put");
switch (state)
{
case BatchStreamState.Delete:
return XmlConstants.HttpMethodDelete;
case BatchStreamState.Get:
return XmlConstants.HttpMethodGet;
case BatchStreamState.Post:
return XmlConstants.HttpMethodPost;
case BatchStreamState.Merge:
return XmlConstants.HttpMethodMerge;
default:
Debug.Assert(BatchStreamState.Put == state, "BatchStreamState.Put == state");
return XmlConstants.HttpMethodPut;
}
}
///
/// Gets the resource from the segment enumerable.
///
/// segment from which resource needs to be returned.
/// returns the resource contained in the request enumerable.
private static object GetResourceFromSegmentEnumerable(SegmentInfo segmentInfo)
{
Debug.Assert(segmentInfo.RequestEnumerable != null, "The segment should always have the result");
object[] results = (object[])segmentInfo.RequestEnumerable;
Debug.Assert(results != null && results.Length == 1, "results != null && results.Length == 1");
Debug.Assert(results[0] != null, "results[0] != null");
return results[0];
}
///
/// Write the exception encountered in the middle of the changeset to the response
///
/// exception encountered
/// list of hosts
/// changeset boundary for the current processing changeset
/// writer to which the response needs to be written
private void HandleChangesetException(
Exception exception,
List changesetHosts,
string changesetBoundary,
StreamWriter writer)
{
Debug.Assert(exception != null, "exception != null");
Debug.Assert(changesetHosts != null, "changesetHosts != null");
Debug.Assert(WebUtil.IsCatchableExceptionType(exception), "WebUtil.IsCatchableExceptionType(exception)");
// For a changeset, we need to write the exception only once. Since we ignore all the changesets
// after we encounter an error, its the last changeset which had error. For cases, which we don't
// know, (like something in save changes, etc), we will still right the last operation information.
// If there are no host, then just pass null.
BatchServiceHost currentHost = null;
if (changesetHosts.Count == 0)
{
currentHost = new BatchServiceHost(changesetBoundary, writer);
}
else
{
currentHost = changesetHosts[changesetHosts.Count - 1];
}
ErrorHandler.HandleBatchProcessException(this, currentHost, exception, writer);
// Write end boundary for the changeset
BatchWriter.WriteEndBoundary(writer, changesetBoundary);
this.dataService.Provider.ClearChanges();
}
/// Increases the count of batch changsets/queries found, and checks it is within limits.
private void IncreaseBatchCount()
{
checked
{
this.batchElementCount++;
}
if (this.batchElementCount > this.dataService.Configuration.MaxBatchCount)
{
this.batchLimitExceeded = true;
throw new DataServiceException(400, Strings.DataService_BatchExceedMaxBatchCount(this.dataService.Configuration.MaxBatchCount));
}
}
/// Increases the count of changeset CUD operations found, and checks it is within limits.
private void IncreaseChangeSetCount()
{
checked
{
this.changeSetElementCount++;
}
if (this.changeSetElementCount > this.dataService.Configuration.MaxChangesetCount)
{
throw new DataServiceException(400, Strings.DataService_BatchExceedMaxChangeSetCount(this.dataService.Configuration.MaxChangesetCount));
}
}
///
/// Write the response for the given request, if required.
///
/// description of the request uri. If this is null, means that no response needs to be written
/// Batch host for which the request should be written.
private void WriteRequest(RequestDescription description, BatchServiceHost batchHost)
{
Debug.Assert(batchHost != null, "host != null");
// For DELETE operations, description will be null
if (description == null)
{
BatchWriter.WriteBoundaryAndHeaders(batchHost.Writer, this.host, batchHost.BoundaryString);
}
else
{
Action responseWriter = DataService.SerializeResponseBody(description, this);
if (responseWriter != null)
{
BatchWriter.WriteBoundaryAndHeaders(batchHost.Writer, this.host, batchHost.BoundaryString);
batchHost.Writer.Flush();
responseWriter(batchHost.Writer.BaseStream);
batchHost.Writer.WriteLine();
}
else
{
BatchWriter.WriteBoundaryAndHeaders(batchHost.Writer, this.host, batchHost.BoundaryString);
}
}
}
#endregion Private methods.
}
}
}
// File provided for Reference Use Only by Microsoft Corporation (c) 2007.
Link Menu

This book is available now!
Buy at Amazon US or
Buy at Amazon UK
- SemanticAnalyzer.cs
- ResolveNameEventArgs.cs
- FormParameter.cs
- CompiledIdentityConstraint.cs
- IncomingWebRequestContext.cs
- SpinLock.cs
- XmlFormatReaderGenerator.cs
- DataFormats.cs
- UserPersonalizationStateInfo.cs
- SettingsProperty.cs
- Menu.cs
- SmtpFailedRecipientException.cs
- TextAutomationPeer.cs
- IWorkflowDebuggerService.cs
- ActivationProxy.cs
- WorkflowView.cs
- SecurityBindingElementImporter.cs
- TimelineClockCollection.cs
- ProcessModelInfo.cs
- DataListCommandEventArgs.cs
- PageAsyncTaskManager.cs
- DbModificationClause.cs
- ImmutablePropertyDescriptorGridEntry.cs
- WorkflowEventArgs.cs
- Utility.cs
- Int32Collection.cs
- DocumentPageTextView.cs
- XmlSchemaObject.cs
- Mouse.cs
- BinHexEncoder.cs
- ConfigUtil.cs
- SelectedDatesCollection.cs
- EntityCollection.cs
- PersonalizationStateInfo.cs
- DesignerTextWriter.cs
- Constraint.cs
- DbTransaction.cs
- HtmlInputReset.cs
- SystemFonts.cs
- BindingSource.cs
- AsyncOperation.cs
- SQLMoney.cs
- ListViewItemEventArgs.cs
- XD.cs
- DeleteMemberBinder.cs
- InlineUIContainer.cs
- SourceElementsCollection.cs
- HTMLTextWriter.cs
- KnownIds.cs
- BinaryFormatter.cs
- Page.cs
- CodeVariableDeclarationStatement.cs
- DependentList.cs
- UiaCoreApi.cs
- MembershipUser.cs
- DemultiplexingClientMessageFormatter.cs
- TriState.cs
- MimeTypeAttribute.cs
- FamilyMap.cs
- ListViewInsertEventArgs.cs
- CodeTypeMemberCollection.cs
- WindowsTitleBar.cs
- WebPartChrome.cs
- CollectionViewGroupInternal.cs
- EntitySetBase.cs
- DataGridViewComboBoxEditingControl.cs
- DataError.cs
- DocumentReferenceCollection.cs
- LayoutEngine.cs
- CatalogZone.cs
- SymbolEqualComparer.cs
- ClientSponsor.cs
- OnOperation.cs
- HwndHostAutomationPeer.cs
- __ComObject.cs
- SqlDataSourceCustomCommandEditor.cs
- PrinterUnitConvert.cs
- OleDbMetaDataFactory.cs
- TreeNodeClickEventArgs.cs
- CoordinationService.cs
- MaterializeFromAtom.cs
- FlowDocumentFormatter.cs
- PrintPreviewControl.cs
- OrderedParallelQuery.cs
- Context.cs
- PriorityBindingExpression.cs
- EmbossBitmapEffect.cs
- AlphabetConverter.cs
- SmtpFailedRecipientException.cs
- WmlImageAdapter.cs
- UMPAttributes.cs
- DocumentViewer.cs
- EmbeddedObject.cs
- prompt.cs
- WebBrowserNavigatedEventHandler.cs
- IsolatedStorageFilePermission.cs
- HttpRequestCacheValidator.cs
- RegisteredScript.cs
- EncoderNLS.cs
- WindowsListBox.cs