Code:
/ 4.0 / 4.0 / DEVDIV_TFS / Dev10 / Releases / RTMRel / ndp / fx / src / DataWeb / Server / System / Data / Services / DataService.cs / 1458001 / 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.Data.Services.Common; using System.Diagnostics; using System.IO; using System.Linq; using System.ServiceModel; using System.ServiceModel.Activation; using System.ServiceModel.Channels; #if ASTORIA_FF_CALLBACKS using System.ServiceModel.Syndication; #endif 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. ////// [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class DataServicewill typically be a subtype of /// or another class that provides /// properties. /// : IRequestHandler, IDataService { #region Private fields. /// A delegate used to create an instance of the data context. private static FunccachedConstructor; /// Service configuration information. private DataServiceConfiguration configuration; ///Data provider for this data service. private DataServiceProviderWrapper provider; ///IUpdatable interface for this datasource's provider private UpdatableWrapper updatable; ///Custom paging provider interface exposed by the service. private DataServicePagingProviderWrapper pagingProvider; ///Context for the current operation. private DataServiceOperationContext operationContext; ///Reference to IDataServiceStreamProvider interface. private DataServiceStreamProviderWrapper streamProvider; ///Events for the data service processing pipeline. private DataServiceProcessingPipeline processingPipeline = new DataServiceProcessingPipeline(); #if DEBUG #pragma warning disable 0169 [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1823")] private ActionrequestQueryableConstructed; #pragma warning restore 0169 #endif #endregion Private fields. #region Properties. /// Events for the data service processing pipeline. public DataServiceProcessingPipeline ProcessingPipeline { [DebuggerStepThrough] get { return this.processingPipeline; } } ///Service configuration information. DataServiceConfiguration IDataService.Configuration { [DebuggerStepThrough] get { return this.configuration; } } ///Data provider for this data service DataServiceProviderWrapper IDataService.Provider { [DebuggerStepThrough] get { return this.provider; } } ///Paging provider for this data service. DataServicePagingProviderWrapper IDataService.PagingProvider { [DebuggerStepThrough] get { Debug.Assert(this.provider != null, "this.provider != null"); if (this.pagingProvider == null) { this.pagingProvider = new DataServicePagingProviderWrapper(this); } return this.pagingProvider; } } ///Returns the instance of data service. object IDataService.Instance { [DebuggerStepThrough] get { return this; } } ///Cached request headers. DataServiceOperationContext IDataService.OperationContext { [DebuggerStepThrough] get { return this.operationContext; } } ///Processing pipeline events DataServiceProcessingPipeline IDataService.ProcessingPipeline { [DebuggerStepThrough] get { return this.processingPipeline; } } ///IUpdatable interface for this provider UpdatableWrapper IDataService.Updatable { [DebuggerStepThrough] get { Debug.Assert(this.provider != null, "this.provider != null"); return this.updatable; } } ///Reference to IDataServiceStreamProvider interface. DataServiceStreamProviderWrapper IDataService.StreamProvider { [DebuggerStepThrough] get { Debug.Assert(this.provider != null, "this.provider != null"); return this.streamProvider ?? (this.streamProvider = new DataServiceStreamProviderWrapper(this)); } } ///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, ICollectionexpandPaths) { 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 DEBUG // We've hit an error, some of the processing pipeline events will not get fired. Setting this flag to skip validation. this.ProcessingPipeline.SkipDebugAssert = true; #endif if (!WebUtil.IsCatchableExceptionType(handlingException)) { throw; } args.Exception = handlingException; } } #if ASTORIA_FF_CALLBACKS ////// Invoked once feed has been written to override the feed elements /// /// Feed being written void IDataService.InternalOnWriteFeed(SyndicationFeed feed) { this.OnWriteFeed(feed); } ////// Invoked once an element has been written to override the element /// /// Item that has been written /// Object with content for thevoid IDataService.InternalOnWriteItem(SyndicationItem item, object obj) { this.OnWriteItem(item, obj); } #endif /// /// 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 DEBUG this.processingPipeline.AssertDebugStateAtDispose(); this.processingPipeline.HasDisposedProviderInterfaces = true; #endif if (this.updatable != null) { this.updatable.DisposeProvider(); this.updatable = null; } if (this.streamProvider != null) { this.streamProvider.DisposeProvider(); this.streamProvider = null; } if (this.pagingProvider != null) { this.pagingProvider.DisposeProvider(); this.pagingProvider = null; } 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) { #if DEBUG this.processingPipeline.AssertDebugStateAtOnStartProcessingRequest(); this.processingPipeline.OnStartProcessingRequestInvokeCount++; #endif 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.operationContext = new DataServiceOperationContext(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 { Action. writer = this.HandleRequest(); Debug.Assert(writer != null, "writer != null"); Message result = CreateMessage(MessageVersion.None, "", ((IDataServiceHost)httpHost).ResponseContentType, writer, this); // If SuppressEntityBody is false, WCF will call DelegateBodyWriter.OnWriteBodyContent(), which // will dispose the data source and stream provider. Otherwise we need to dispose them in the // finally block below. if (!System.ServiceModel.Web.WebOperationContext.Current.OutgoingResponse.SuppressEntityBody) { shouldDispose = false; } return result; } #if DEBUG catch { this.processingPipeline.SkipDebugAssert = true; throw; } #endif finally { if (shouldDispose) { ((IDataService)this).DisposeDataSource(); } } } /// Provides a host-agnostic entry point for request processing. public void ProcessRequest() { if (this.operationContext == null) { throw new InvalidOperationException(Strings.DataService_HostNotAttached); } try { Actionwriter = this.HandleRequest(); if (writer != null) { writer(this.operationContext.Host.ResponseStream); } } #if DEBUG catch { this.processingPipeline.SkipDebugAssert = true; throw; } #endif finally { ((IDataService)this).DisposeDataSource(); #if DEBUG // Need to reset the states since the caller can reuse the same service instance. this.processingPipeline.ResetDebugState(); #endif } } #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"); #if DEBUG this.processingPipeline.SkipDebugAssert = true; #endif } ////// 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. } #if ASTORIA_FF_CALLBACKS ////// Invoked once feed has been written to override the feed elements /// /// Feed being written protected virtual void OnWriteFeed(SyndicationFeed feed) { } ////// Invoked once an element has been written to override the element /// /// Item that has been written /// Object with content for theprotected virtual void OnWriteItem(SyndicationItem item, object obj) { } #endif #endregion Protected methods. #region Private methods. /// /// Checks that if etag values are specified in the header, they must be valid. /// /// header values. /// request description. private static void CheckETagValues(DataServiceHostWrapper host, RequestDescription description) { Debug.Assert(host != null, "host != null"); // Media Resource ETags can be strong bool allowStrongEtag = description.TargetKind == RequestTargetKind.MediaResource; if (!WebUtil.IsETagValueValid(host.RequestIfMatch, allowStrongEtag)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_ETagValueNotValid(host.RequestIfMatch)); } if (!WebUtil.IsETagValueValid(host.RequestIfNoneMatch, allowStrongEtag)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_ETagValueNotValid(host.RequestIfNoneMatch)); } } ////// Creates a /// Version for message. /// Action for message. /// MIME content type for body. /// Callback. /// Service with context to dispose once the response has been written. ///that invokes the specified /// callback to write its body. /// 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 new data service configuration instance /// /// data service type /// provider instance ///data service configuration instance private static DataServiceConfiguration CreateConfiguration(Type dataServiceType, IDataServiceMetadataProvider provider) { Debug.Assert(dataServiceType != null, "dataServiceType != null"); Debug.Assert(provider != null, "provider != null"); DataServiceConfiguration configuration = new DataServiceConfiguration(provider); configuration.Initialize(dataServiceType); if (!(provider is BaseServiceProvider) && configuration.GetKnownTypes().Any()) { throw new InvalidOperationException(Strings.DataService_RegisterKnownTypeNotAllowedForIDSP); } configuration.Seal(); return configuration; } ////// 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(DataServiceHostWrapper 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. /// Context for current operation. private static void ValidateRequest(DataServiceOperationContext operationContext) { if (!String.IsNullOrEmpty(operationContext.Host.RequestIfMatch) && !String.IsNullOrEmpty(operationContext.Host.RequestIfNoneMatch)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_BothIfMatchAndIfNoneMatchHeaderSpecified); } } ////// Raises the response version header if necessary for the $metadata path. /// WARNING!!! This property can only be called for the $metadata path because it enumerates through all resource types. /// Calling it from outside of the $metadata path would break our IDSP contract. /// /// description about the request uri /// data service to which the request was made private static void RaiseResponseVersionForMetadata(RequestDescription description, IDataService dataService) { Debug.Assert(description.TargetKind == RequestTargetKind.Metadata, "This method can only be called from the $metadata path because it enumerates through all resource types."); if (dataService.Provider.IsV1Provider) { if (!dataService.Provider.GetEpmCompatiblityForV1Provider()) { description.RaiseResponseVersion(2, 0); } } else { foreach (ResourceType rt in dataService.Provider.Types) { if (!rt.EpmIsV1Compatible) { description.RaiseResponseVersion(2, 0); break; } } } } ////// 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.OperationContext.Host != null, "dataService.OperationContext.Host != null"); DataServiceHostWrapper host = dataService.OperationContext.Host; // Make a decision about metadata response version if (description.TargetKind == RequestTargetKind.Metadata) { RaiseResponseVersionForMetadata(description, dataService); } dataService.Configuration.ValidateMaxProtocolVersion(description); WebUtil.CheckVersion(dataService, description); CheckETagValues(host, description); ResourceSetWrapper lastSegmentContainer = description.LastSegmentInfo.TargetContainer; if (host.AstoriaHttpVerb == AstoriaVerbs.GET) { // This if expression was missing from V1.0, but is a breaking change to add it // without also checking for the new OverrideEntitySetRights if (description.LastSegmentInfo.Operation != null && (0 != (dataService.Configuration.GetServiceOperationRights(description.LastSegmentInfo.Operation.ServiceOperation) & ServiceOperationRights.OverrideEntitySetRights))) { DataServiceConfiguration.CheckServiceRights(description.LastSegmentInfo.Operation, description.IsSingleResult); } else { // For $count, the rights is already checked in the RequestUriProcessor and hence we don't need to check here. // Also, checking for ReadSingle right is wrong, since we need to only check for ReadMultiple rights. if (lastSegmentContainer != null && description.LastSegmentInfo.Identifier != XmlConstants.UriCountSegment) { DataServiceConfiguration.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 (host.AstoriaHttpVerb == AstoriaVerbs.POST) { newDescription = HandlePostOperation(description, dataService); if (description.LinkUri) { statusCode = 204; // 204 - No Content shouldWriteBody = false; } else { statusCode = 201; // 201 - Created. } } else if (host.AstoriaHttpVerb == AstoriaVerbs.PUT || host.AstoriaHttpVerb == AstoriaVerbs.MERGE) { if (lastSegmentContainer != null && !description.LinkUri) { if (host.AstoriaHttpVerb == AstoriaVerbs.PUT) { DataServiceConfiguration.CheckResourceRights(lastSegmentContainer, EntitySetRights.WriteReplace); } else { DataServiceConfiguration.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); Debug.Assert(description.ResponseVersion == RequestDescription.DataServiceDefaultResponseVersion, "description.ResponseVersion == RequestDescription.DataServiceDefaultResponseVersion"); statusCode = 204; // 204 - No Content } else if (host.AstoriaHttpVerb == AstoriaVerbs.DELETE) { if (lastSegmentContainer != null && !description.LinkUri) { DataServiceConfiguration.CheckResourceRights(lastSegmentContainer, EntitySetRights.WriteDelete); } HandleDeleteOperation(description, dataService); Debug.Assert(description.RequireMinimumVersion == new Version(1, 0), "description.RequireMinimumVersion == new Version(1, 0)"); Debug.Assert(description.ResponseVersion == RequestDescription.DataServiceDefaultResponseVersion, "description.ResponseVersion == RequestDescription.DataServiceDefaultResponseVersion"); 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. 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. Debug.Assert(description.ResponseVersion == newDescription.ResponseVersion, "description.ResponseVersion == newDescription.ResponseVersion"); host.ResponseVersion = newDescription.ResponseVersion.ToString() + ";"; host.ResponseStatusCode = statusCode; if (shouldWriteBody) { // 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 ActionSerializeResponseBody(RequestDescription description, IDataService dataService) { Debug.Assert(dataService.Provider != null, "dataService.Provider != null"); Debug.Assert(dataService.OperationContext.Host != null, "dataService.OperationContext.Host != null"); DataServiceHostWrapper host = dataService.OperationContext.Host; // 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 (!RequestDescription.IsETagHeaderAllowed(description)) { if (!String.IsNullOrEmpty(host.RequestIfMatch) || !String.IsNullOrEmpty(host.RequestIfNoneMatch)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_ETagCannotBeSpecified(host.AbsoluteRequestUri)); } } if (host.AstoriaHttpVerb == AstoriaVerbs.PUT || host.AstoriaHttpVerb == AstoriaVerbs.MERGE) { ResourceSetWrapper container; object actualEntity = GetContainerAndActualEntityInstance(dataService, description, out container); // We should only write etag in the response, if the type has one or more etag properties defined. // WriteETagValueInResponseHeader checks for null etag value (which means that no etag properties are defined) // that before calling the host. string etag; if (description.TargetKind == RequestTargetKind.MediaResource) { etag = dataService.StreamProvider.GetStreamETag(actualEntity, dataService.OperationContext); } else { etag = WebUtil.GetETagValue(dataService, actualEntity, container); } #if DEBUG WebUtil.WriteETagValueInResponseHeader(description, etag, host); #else WebUtil.WriteETagValueInResponseHeader(etag, host); #endif return WebUtil.GetEmptyStreamWriter(); } // 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.TargetResourceType, host.RequestAccept, description.MimeType, dataService); // This is the code path for service operations and GET requests returning multiple results if (description.TargetSource == RequestTargetSource.ServiceOperation || description.TargetSource == RequestTargetSource.None || !description.IsSingleResult) { // For service operations returning single result, etag checks must be performed by the service operation itself. Debug.Assert( (String.IsNullOrEmpty(host.RequestIfMatch) && String.IsNullOrEmpty(host.RequestIfNoneMatch)) || description.TargetSource == RequestTargetSource.ServiceOperation, "No etag can be specified for collection or it must be a service operation"); Encoding encoding = GetRequestAcceptEncoding(responseFormat, host.RequestAcceptCharSet); IEnumerator queryResults = WebUtil.GetRequestEnumerator(description.RequestEnumerable); try { bool hasMoved = queryResults.MoveNext(); if (description.IsSingleResult) { if (!hasMoved || queryResults.Current == null) { throw DataServiceException.CreateResourceNotFound(description.LastSegmentInfo.Identifier); } } // If we had to wait until we got a value to determine the valid contents, try that now. if (responseFormat == ContentFormat.Unknown) { responseFormat = ResolveUnknownFormat(description, queryResults.Current, dataService); } Debug.Assert(responseFormat != ContentFormat.Unknown, "responseFormat != ContentFormat.Unknown"); host.ResponseContentType = HttpProcessUtility.BuildContentType(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. /// resource type. /// Accept header value. /// Required MIME type. /// Data service. ////// The content format for the resource; Unknown if it cannot be determined statically. /// private static ContentFormat SelectResponseFormatForType( RequestTargetKind targetKind, ResourceType resourceType, string acceptTypesText, string mimeType, IDataService service) { ContentFormat responseFormat; if (targetKind == RequestTargetKind.PrimitiveValue) { responseFormat = SelectPrimitiveContentType(resourceType, acceptTypesText, mimeType, service.OperationContext.Host); } else if (targetKind == RequestTargetKind.MediaResource) { // We need the MLE instance to get the response format for the MediaResource. // We will resolve the response format in ResolveUnknownFormat() where we have the MLE instance. responseFormat = ContentFormat.Unknown; } else if (targetKind != RequestTargetKind.OpenPropertyValue) { bool entityTarget = targetKind == RequestTargetKind.Resource; responseFormat = SelectResponseFormat(service.OperationContext.Host, acceptTypesText, entityTarget); if (responseFormat == ContentFormat.Unsupported) { throw new DataServiceException(415, Strings.DataServiceException_UnsupportedMediaType); } } else { // We cannot negotiate a format until we know what the value is for the object. responseFormat = ContentFormat.Unknown; } return responseFormat; } ///Selects the correct content format for a primitive type. /// resource 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(ResourceType targetResourceType, string acceptTypesText, string requiredContentType, DataServiceHostWrapper host) { // Debug.Assert( // targetResourceType != null && // targetResourceType.ResourceTypeKind == ResourceTypeKind.Primitive, // "targetElementType != null && targetResourceType.ResourceTypeKind == ResourceTypeKind.Primitive"); string contentType; ContentFormat responseFormat = WebUtil.GetResponseFormatForPrimitiveValue(targetResourceType, out contentType); requiredContentType = requiredContentType ?? contentType; host.ResponseContentType = HttpProcessUtility.SelectRequiredMimeType( acceptTypesText, // acceptTypesText new string[] { requiredContentType }, // exactContentType requiredContentType); // inexactContentType return responseFormat; } ///Selects the correct content format for a media resource. /// The media link entry. /// Accept header value. /// Data service instance. ///The content format for the resource. private static ContentFormat SelectMediaResourceContentType(object mediaLinkEntry, string acceptTypesText, IDataService service) { Debug.Assert(mediaLinkEntry != null, "mediaLinkEntry != null"); Debug.Assert(service != null, "service != null"); string contentType = service.StreamProvider.GetStreamContentType(mediaLinkEntry, service.OperationContext); service.OperationContext.Host.ResponseContentType = HttpProcessUtility.SelectRequiredMimeType( acceptTypesText, // acceptTypesText new string[] { contentType }, // exactContentType contentType); // inexactContentType return ContentFormat.Binary; } ///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"); DataServiceHostWrapper host = dataService.OperationContext.Host; if (!String.IsNullOrEmpty(host.RequestIfMatch) || !String.IsNullOrEmpty(host.RequestIfNoneMatch)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_ETagSpecifiedForPost); } if (description.IsSingleResult) { throw DataServiceException.CreateMethodNotAllowed( Strings.BadRequest_InvalidUriForPostOperation(host.AbsoluteRequestUri), DataServiceConfiguration.GetAllowedMethods(dataService.Configuration, description)); } Debug.Assert( description.TargetSource == RequestTargetSource.EntitySet || description.Property.Kind == ResourcePropertyKind.ResourceSetReference, "Only ways to have collections of resources"); Stream requestStream = host.RequestStream; Debug.Assert(requestStream != null, "requestStream != null"); object entity = null; ResourceType targetResourceType = description.TargetResourceType; Debug.Assert(targetResourceType != null, "targetResourceType != null"); if (!description.LinkUri && dataService.Provider.HasDerivedTypes(targetResourceType) && WebUtil.HasMediaLinkEntryInHierarchy(targetResourceType, dataService.Provider)) { ResourceSetWrapper targetResourceSet = description.LastSegmentInfo.TargetContainer; Debug.Assert(targetResourceSet != null, "targetResourceSet != null"); targetResourceType = dataService.StreamProvider.ResolveType(targetResourceSet.Name, dataService); Debug.Assert(targetResourceType != null, "targetResourceType != null"); } UpdateTracker tracker = UpdateTracker.CreateUpdateTracker(dataService); if (!description.LinkUri && targetResourceType.IsMediaLinkEntry) { // Verify that the user has rights to add to the target container Debug.Assert(description.LastSegmentInfo.TargetContainer != null, "description.LastSegmentInfo.TargetContainer != null"); DataServiceConfiguration.CheckResourceRights(description.LastSegmentInfo.TargetContainer, EntitySetRights.WriteAppend); entity = Deserializer.CreateMediaLinkEntry(targetResourceType.FullName, requestStream, dataService, description, tracker); if (description.TargetSource == RequestTargetSource.Property) { Debug.Assert(description.Property.Kind == ResourcePropertyKind.ResourceSetReference, "Expecting POST resource set property"); Deserializer.HandleBindOperation(description, entity, dataService, tracker); } } else { using (Deserializer deserializer = Deserializer.CreateDeserializer(description, dataService, false /*update*/, tracker)) { entity = deserializer.HandlePostRequest(description); Debug.Assert(entity != null, "entity != null"); } } tracker.FireNotifications(); return RequestDescription.CreateSingleResultRequestDescription( description, entity, description.LastSegmentInfo.TargetContainer); } ///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"); DataServiceHostWrapper host = dataService.OperationContext.Host; if (!description.IsSingleResult) { throw DataServiceException.CreateMethodNotAllowed( Strings.BadRequest_InvalidUriForPutOperation(host.AbsoluteRequestUri), DataServiceConfiguration.GetAllowedMethods(dataService.Configuration, description)); } else if (description.LinkUri && description.Property.Kind != ResourcePropertyKind.ResourceReference) { throw DataServiceException.CreateMethodNotAllowed(Strings.DataService_CannotUpdateSetReferenceLinks, XmlConstants.HttpMethodDelete); } // Note that for Media Resources, we let the Stream Provider decide whether or not to support If-None-Match for PUT if (!String.IsNullOrEmpty(host.RequestIfNoneMatch) && description.TargetKind != RequestTargetKind.MediaResource) { throw DataServiceException.CreateBadRequestError(Strings.DataService_IfNoneMatchHeaderNotSupportedInPut); } else if (!RequestDescription.IsETagHeaderAllowed(description) && !String.IsNullOrEmpty(host.RequestIfMatch)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_ETagCannotBeSpecified(host.AbsoluteRequestUri)); } else if (description.Property != null && description.Property.IsOfKind(ResourcePropertyKind.Key)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_CannotUpdateKeyProperties(description.Property.Name)); } Stream requestStream = host.RequestStream; Debug.Assert(requestStream != null, "requestStream != null"); 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.OperationContext.Host != null, "dataService.OperationContext.Host != null"); DataServiceHostWrapper host = dataService.OperationContext.Host; // 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.IsSingleResult && description.LinkUri) { HandleUnbindOperation(description, dataService); } else if (description.IsSingleResult && description.TargetKind == RequestTargetKind.Resource) { Debug.Assert(description.LastSegmentInfo.TargetContainer != null, "description.LastSegmentInfo.TargetContainer != null"); if (description.RequestEnumerable == null) { throw DataServiceException.CreateBadRequestError(Strings.BadRequest_ResourceCanBeCrossReferencedOnlyForBindOperation); } // if (!String.IsNullOrEmpty(host.RequestIfNoneMatch)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_IfNoneMatchHeaderNotSupportedInDelete); } // 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*/); ResourceSetWrapper container = description.LastSegmentInfo.TargetContainer; // Need to check etag for DELETE operation dataService.Updatable.SetETagValues(entity, container); // object actualEntity = dataService.Updatable.ResolveResource(entity); ResourceType resourceType = dataService.Provider.GetResourceType(actualEntity); if (description.Property != null) { Debug.Assert(container != null, "container != null"); DataServiceConfiguration.CheckResourceRights(container, EntitySetRights.WriteDelete); } dataService.Updatable.DeleteResource(entity); if (resourceType != null && resourceType.IsMediaLinkEntry) { dataService.StreamProvider.DeleteStream(actualEntity, dataService.OperationContext); } UpdateTracker.FireNotification(dataService, 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 (!String.IsNullOrEmpty(host.RequestIfNoneMatch)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_IfNoneMatchHeaderNotSupportedInDelete); } 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'). ResourceSetWrapper container; // resource set to which the parent entity belongs to. object resource = Deserializer.GetResourceToModify(description, dataService, false /*allowCrossReference*/, out securityResource, out container, true /*checkETag*/); object actualEntity = dataService.Updatable.ResolveResource(securityResource); // 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); UpdateTracker.FireNotification(dataService, actualEntity, container, UpdateOperations.Change); } else if (description.TargetKind == RequestTargetKind.OpenProperty) { // Open navigation properties are not supported on OpenTypes. throw DataServiceException.CreateBadRequestError(Strings.OpenNavigationPropertiesNotSupportedOnOpenTypes(description.LastSegmentInfo.Identifier)); } else if (description.TargetKind == RequestTargetKind.OpenPropertyValue) { // if (!String.IsNullOrEmpty(host.RequestIfNoneMatch)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_IfNoneMatchHeaderNotSupportedInDelete); } object securityResource; ResourceSetWrapper container; object resource = Deserializer.GetResourceToModify(description, dataService, false /*allowCrossReference*/, out securityResource, out container, true /*checkETag*/); object actualEntity = dataService.Updatable.ResolveResource(securityResource); // 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); UpdateTracker.FireNotification(dataService, actualEntity, container, UpdateOperations.Change); } else { throw DataServiceException.CreateMethodNotAllowed( Strings.BadRequest_InvalidUriForDeleteOperation(host.AbsoluteRequestUri), DataServiceConfiguration.GetAllowedMethods(dataService.Configuration, 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 ActionHandleInternalResources(RequestDescription description, IDataService dataService) { string[] exactContentType = null; ContentFormat format = ContentFormat.Unknown; string mime = null; DataServiceHostWrapper host = dataService.OperationContext.Host; if (description.TargetKind == RequestTargetKind.Metadata) { exactContentType = new string[] { XmlConstants.MimeMetadata }; format = ContentFormat.MetadataDocument; mime = HttpProcessUtility.SelectRequiredMimeType( host.RequestAccept, // acceptTypesText exactContentType, // exactContentType XmlConstants.MimeApplicationXml); // inexactContentType } else if (description.TargetKind == RequestTargetKind.ServiceDirectory) { exactContentType = new string[] { XmlConstants.MimeApplicationAtomService, XmlConstants.MimeApplicationJson, XmlConstants.MimeApplicationXml }; mime = HttpProcessUtility.SelectRequiredMimeType( host.RequestAccept, // 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(host.RequestAcceptCharSet); 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 ActionCompareETagAndWriteResponse( RequestDescription description, ContentFormat responseFormat, IDataService dataService) { Debug.Assert(description != null, "description != null"); Debug.Assert(dataService != null, "dataService != null"); Debug.Assert(dataService.OperationContext != null && dataService.OperationContext.Host != null, "dataService.OperationContext != null && dataService.OperationContext.Host != null"); DataServiceHostWrapper host = dataService.OperationContext.Host; Debug.Assert( String.IsNullOrEmpty(host.RequestIfMatch) || String.IsNullOrEmpty(host.RequestIfNoneMatch), "Both If-Match and If-None-Match header cannot be specified"); IEnumerator queryResults = null; try { if (host.AstoriaHttpVerb == AstoriaVerbs.GET) { bool writeResponse = true; // Get the index of the last resource in the request uri int parentResourceIndex = description.GetIndexOfTargetEntityResource(); Debug.Assert(parentResourceIndex >= 0 && parentResourceIndex < description.SegmentInfos.Length, "parentResourceIndex >= 0 && parentResourceIndex < description.SegmentInfos.Length"); SegmentInfo parentEntitySegment = description.SegmentInfos[parentResourceIndex]; queryResults = RequestDescription.GetSingleResultFromEnumerable(parentEntitySegment); object resource = queryResults.Current; string etagValue = null; if (description.LinkUri) { // This must be already checked in SerializeResponseBody method. Debug.Assert(String.IsNullOrEmpty(host.RequestIfMatch) && String.IsNullOrEmpty(host.RequestIfNoneMatch), "ETag cannot be specified for $link requests"); if (resource == null) { throw DataServiceException.CreateResourceNotFound(description.LastSegmentInfo.Identifier); } } else if (RequestDescription.IsETagHeaderAllowed(description) && description.TargetKind != RequestTargetKind.MediaResource) { // Media Resources have their own ETags, we let the Stream Provider handle it. No need to compare the MLE ETag here. ResourceSetWrapper container = parentEntitySegment.TargetContainer; Debug.Assert(container != null, "container != null"); etagValue = WebUtil.CompareAndGetETag(resource, resource, container, dataService, out writeResponse); } if (resource == null && description.TargetKind == RequestTargetKind.Resource) { Debug.Assert(description.Property != null, "non-open type property"); WebUtil.Dispose(queryResults); queryResults = null; // If you are querying reference nav property and the value is null, // return 204 - No Content e.g. /Customers(1)/BestFriend host.ResponseStatusCode = 204; // No Content return WebUtil.GetEmptyStreamWriter(); } if (writeResponse) { return WriteSingleElementResponse(description, responseFormat, queryResults, parentResourceIndex, etagValue, dataService); } else { WebUtil.Dispose(queryResults); queryResults = null; #if DEBUG WebUtil.WriteETagValueInResponseHeader(description, etagValue, host); #else WebUtil.WriteETagValueInResponseHeader(etagValue, host); #endif host.ResponseStatusCode = 304; // Not Modified return WebUtil.GetEmptyStreamWriter(); } } else { Debug.Assert(host.AstoriaHttpVerb == AstoriaVerbs.POST, "Must be POST method"); ResourceSetWrapper container; object actualEntity = GetContainerAndActualEntityInstance(dataService, description, out container); host.ResponseLocation = Serializer.GetUri(actualEntity, dataService.Provider, container, host.AbsoluteServiceUri).AbsoluteUri; string etagValue = WebUtil.GetETagValue(dataService, actualEntity, container); queryResults = RequestDescription.GetSingleResultFromEnumerable(description.LastSegmentInfo); return WriteSingleElementResponse(description, responseFormat, queryResults, description.SegmentInfos.Length - 1, etagValue, dataService); } } catch { WebUtil.Dispose(queryResults); throw; } } /// 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 == RequestTargetKind.MediaResource, description.TargetKind + " is open property, open property value, or MediaResource."); WebUtil.CheckResourceExists(element != null, description.LastSegmentInfo.Identifier); ResourceType resourceType = WebUtil.GetResourceType(dataService.Provider, element); Debug.Assert(resourceType != null, "resourceType != null, WebUtil.GetResourceType() should throw if it fails to resolve the resource type."); DataServiceHostWrapper host = dataService.OperationContext.Host; // Determine the appropriate target type based on the kind of resource. bool rawValue = description.TargetKind == RequestTargetKind.OpenPropertyValue || description.TargetKind == RequestTargetKind.MediaResource; 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) { if (resourceType.IsMediaLinkEntry) { return SelectMediaResourceContentType(element, host.RequestAccept, dataService); } else { throw DataServiceException.CreateBadRequestError(Strings.BadRequest_InvalidUriForMediaResource(host.AbsoluteRequestUri)); } } else { targetKind = RequestTargetKind.Resource; } break; } if (description.LinkUri) { targetKind = RequestTargetKind.Link; } return SelectResponseFormatForType(targetKind, resourceType, host.RequestAccept, null, dataService); } ////// 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 ActionWriteSingleElementResponse( RequestDescription description, ContentFormat responseFormat, IEnumerator queryResults, int parentResourceIndex, string etagValue, IDataService dataService) { try { // The queryResults parameter contains the enumerator of the parent resource. If the parent resource's RequestEnumerable is not // the same instance as that of the last segment, we need to get the enumerator for the last segment. // Take MediaResource for example, the MLE is its parent resource, which is what we want to write out and we don't want to // query for another instance of the enumerator. if (description.SegmentInfos[parentResourceIndex].RequestEnumerable != description.LastSegmentInfo.RequestEnumerable) { object resource = queryResults.Current; for (int segmentIdx = parentResourceIndex + 1; segmentIdx < description.SegmentInfos.Length; segmentIdx++) { SegmentInfo parentSegment = description.SegmentInfos[segmentIdx - 1]; SegmentInfo currentSegment = description.SegmentInfos[segmentIdx]; WebUtil.CheckResourceExists(resource != null, parentSegment.Identifier); // $value has the same query as the preceding segment. if (currentSegment.TargetKind == RequestTargetKind.PrimitiveValue || currentSegment.TargetKind == RequestTargetKind.OpenPropertyValue) { Debug.Assert(segmentIdx == description.SegmentInfos.Length - 1, "$value has to be the last segment."); break; } if (currentSegment.TargetKind == RequestTargetKind.OpenProperty) { ResourceType openTypeParentResourceType = WebUtil.GetResourceType(dataService.Provider, resource); if (openTypeParentResourceType.ResourceTypeKind == ResourceTypeKind.ComplexType) { ResourceProperty resProperty = openTypeParentResourceType.Properties.First(p => p.Name == currentSegment.Identifier); resource = WebUtil.GetPropertyValue(dataService.Provider, resource, openTypeParentResourceType, resProperty, null); } else { Debug.Assert(openTypeParentResourceType.ResourceTypeKind == ResourceTypeKind.EntityType, "Entity Type expected"); resource = WebUtil.GetPropertyValue(dataService.Provider, resource, openTypeParentResourceType, null, currentSegment.Identifier); } } else { resource = WebUtil.GetPropertyValue(dataService.Provider, resource, parentSegment.TargetResourceType, currentSegment.ProjectedProperty, null); } } RequestDescription.CheckQueryResult(resource, description.LastSegmentInfo); queryResults = new QueryResultsWrapper((new object[] { resource }).GetEnumerator(), queryResults); queryResults.MoveNext(); } // If we had to wait until we got a value to determine the valid contents, try that now. if (responseFormat == ContentFormat.Unknown) { responseFormat = ResolveUnknownFormat(description, queryResults.Current, dataService); } Debug.Assert(responseFormat != ContentFormat.Unknown, "responseFormat != ContentFormat.Unknown"); DataServiceHostWrapper host = dataService.OperationContext.Host; // Write the etag header #if DEBUG WebUtil.WriteETagValueInResponseHeader(description, etagValue, host); #else WebUtil.WriteETagValueInResponseHeader(etagValue, host); #endif Encoding encoding = GetRequestAcceptEncoding(responseFormat, host.RequestAcceptCharSet); host.ResponseContentType = HttpProcessUtility.BuildContentType(host.ResponseContentType, encoding); return new ResponseBodyWriter( encoding, true /* hasMoved */, dataService, queryResults, description, responseFormat).Write; } catch { WebUtil.Dispose(queryResults); throw; } } /// /// Returns the actual entity instance and its containers for the resource in the description results. /// /// Data service /// 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( IDataService service, RequestDescription description, out ResourceSetWrapper 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] = service.Updatable.ResolveResource(results[0]); container = description.LastSegmentInfo.TargetContainer; if (container == null) { // Open navigation properties are not supported on OpenTypes. throw DataServiceException.CreateBadRequestError(Strings.OpenNavigationPropertiesNotSupportedOnOpenTypes(description.LastSegmentInfo.Identifier)); } Debug.Assert(container != null, "description.LastSegmentInfo.TargetContainer != null"); return results[0]; } ////// 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"); if (!String.IsNullOrEmpty(dataService.OperationContext.Host.RequestIfMatch) || !String.IsNullOrEmpty(dataService.OperationContext.Host.RequestIfNoneMatch)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_ETagNotSupportedInUnbind); } object parentEntity; ResourceSetWrapper parentEntityResourceSet; Deserializer.GetResourceToModify(description, dataService, out parentEntity, out parentEntityResourceSet); Debug.Assert(description.Property != null, "description.Property != null"); if (description.Property.Kind == ResourcePropertyKind.ResourceReference) { dataService.Updatable.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.Updatable.RemoveReferenceFromCollection(parentEntity, description.Property.Name, childEntity); } if (dataService.Configuration.DataServiceBehavior.InvokeInterceptorsOnLinkDelete) { object actualParentEntity = dataService.Updatable.ResolveResource(parentEntity); UpdateTracker.FireNotification(dataService, actualParentEntity, parentEntityResourceSet, UpdateOperations.Change); } } ////// 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 (WebUtil.CompareMimeType(mime, XmlConstants.MimeApplicationJson)) { return ContentFormat.Json; } else if (WebUtil.CompareMimeType(mime, XmlConstants.MimeApplicationAtom)) { return ContentFormat.Atom; } else { Debug.Assert( WebUtil.CompareMimeType(mime, XmlConstants.MimeApplicationXml) || WebUtil.CompareMimeType(mime, XmlConstants.MimeApplicationAtomService) || WebUtil.CompareMimeType(mime, XmlConstants.MimeTextXml), "expecting application/xml, application/atomsvc+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 ActionHandleRequest() { Debug.Assert(this.operationContext != null, "this.operationContext != null"); // Need to cache the request headers for every request. Note that the while the underlying // host instance may stay the same across requests, the request headers can change between // requests. We have to refresh the cache for every request. this.operationContext.InitializeAndCacheHeaders(); Action writer = null; try { this.EnsureProviderAndConfigForRequest(); } catch (Exception ex) { #if DEBUG // We've hit an error, some of the processing pipeline events will not get fired. Setting this flag to skip validation. this.ProcessingPipeline.SkipDebugAssert = true; #endif int responseStatusCode = 500; if (!WebUtil.IsCatchableExceptionType(ex)) { throw; } // if Exception been thrown is DSE, we keep the exception's status code // otherwise, the status code is 500. DataServiceException dse = ex as DataServiceException; if (dse != null) { responseStatusCode = dse.StatusCode; } // safe handling of initialization time error DataServiceHostWrapper host = this.operationContext.Host; host.ResponseStatusCode = responseStatusCode; host.ResponseVersion = XmlConstants.DataServiceVersion1Dot0 + ";"; throw; } try { RequestDescription description = this.ProcessIncomingRequestUri(); if (description.TargetKind != RequestTargetKind.Batch) { writer = this.HandleNonBatchRequest(description); // Query Processing Pipeline - Request end event // Note 1 we only invoke the event handler for ALL operations // Note 2 we invoke this event before serialization is complete // Note 3 we invoke this event before any provider interface held by the data service runtime is released/disposed DataServiceProcessingPipelineEventArgs eventArg = new DataServiceProcessingPipelineEventArgs(this.operationContext); this.processingPipeline.InvokeProcessedRequest(this, eventArg); } else { writer = this.HandleBatchRequest(); } } catch (Exception exception) { #if DEBUG // We've hit an error, some of the processing pipeline events will not get fired. Setting this flag to skip validation. this.ProcessingPipeline.SkipDebugAssert = true; #endif // Exception should be re-thrown if not handled. if (!WebUtil.IsCatchableExceptionType(exception)) { throw; } string accept = (this.operationContext != null) ? this.operationContext.Host.RequestAccept : null; string acceptCharset = (this.operationContext != null) ? this.operationContext.Host.RequestAcceptCharSet : 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 ActionHandleNonBatchRequest(RequestDescription description) { Debug.Assert(description.TargetKind != RequestTargetKind.Batch, "description.TargetKind != RequestTargetKind.Batch"); bool serviceOperationRequest = (description.TargetSource == RequestTargetSource.ServiceOperation); // The reason to create UpdatableWrapper here is to make sure that the right data service instance is // passed to the UpdatableWrapper. Earlier this line used to live in EnsureProviderAndConfigForRequest // method, which means that same data service instance was passed to the UpdatableWrapper, irrespective // of whether this was a batch request or not. The issue with that was in UpdatableWrapper.SetConcurrencyValues // method, if someone tried to access the service.RequestParams, this will give you the headers for the // top level batch request, not the part of the batch request we are processing. this.updatable = new UpdatableWrapper(this); description = ProcessIncomingRequest(description, this); if (this.operationContext.Host.AstoriaHttpVerb != AstoriaVerbs.GET) { // Bug 470090: Since we used to call SaveChanges() for service operations in V1, we need to // keep doing that for V1 providers that implement IUpdatable. In other words, for ObjectContextServiceProvider // we will always do this, and for reflection service provider, we will have to check. if (serviceOperationRequest) { if (this.provider.IsV1ProviderAndImplementsUpdatable()) { this.updatable.SaveChanges(); } } else { this.updatable.SaveChanges(); } // Query Processing Pipeline - Changeset end event // Note 1 we only invoke the event handler for CUD operations // Note 2 we invoke this event immediately after SaveChanges() // Note 3 we invoke this event before serialization happens this.processingPipeline.InvokeProcessedChangeset(this, new EventArgs()); } return (description == null) ? WebUtil.GetEmptyStreamWriter() : SerializeResponseBody(description, this); } /// Handle the batch request. ///Returns the delegate which takes the response stream for writing the response. private ActionHandleBatchRequest() { Debug.Assert(this.operationContext != null && this.operationContext.Host != null, "this.operationContext != null && this.operationContext.Host != null"); DataServiceHostWrapper host = this.operationContext.Host; // Verify the HTTP method. if (host.AstoriaHttpVerb != AstoriaVerbs.POST) { throw DataServiceException.CreateMethodNotAllowed( Strings.DataService_BatchResourceOnlySupportsPost, XmlConstants.HttpMethodPost); } WebUtil.CheckVersion(this, null); // Verify the content type and get the boundary string Encoding encoding; string boundary; if (!BatchStream.GetBoundaryAndEncodingFromMultipartMixedContentType(host.RequestContentType, out boundary, out encoding) || String.IsNullOrEmpty(boundary)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_InvalidContentTypeForBatchRequest); } // Write the response headers host.ResponseStatusCode = 202; // OK host.ResponseCacheControl = XmlConstants.HttpCacheControlNoCache; string batchBoundary = XmlConstants.HttpMultipartBoundaryBatchResponse + '_' + Guid.NewGuid().ToString(); host.ResponseContentType = String.Format( System.Globalization.CultureInfo.InvariantCulture, "{0}; {1}={2}", XmlConstants.MimeMultiPartMixed, XmlConstants.HttpMultipartBoundary, batchBoundary); // DEVNOTE([....]): // Added for V2+ services // The batch response format should be 1.0 until we update the format itself // Each individual batch response will set its version to the batch host. host.ResponseVersion = XmlConstants.DataServiceVersion1Dot0 + ";"; BatchStream batchStream = new BatchStream(host.RequestStream, boundary, encoding, true); BatchDataService batchDataService = new BatchDataService(this, batchStream, batchBoundary); return batchDataService.HandleBatchContent; } /// Creates the provider and configuration as necessary to be used for this request. private void EnsureProviderAndConfigForRequest() { if (this.provider == null) { this.CreateProvider(); } else { Debug.Assert(this.configuration != null, "this.configuration != null -- otherwise this.provider was ----signed with no configuration"); } #if DEBUG // No event should be fired before this point. // No provider interfaces except IDSP should be created before this point. this.processingPipeline.AssertInitialDebugState(); #endif } ////// Creates a provider implementation that wraps the T type. /// private void CreateProvider() { // From the IDSP Spec: // If the class derived from DataServiceimplements IServiceProvider then: // a. Invoke // IDataServiceMetadataProvider provider = IServiceProvider.GetService(TypeOf(IDataMetadataServiceProvider)) // // b. If provider != null, then the service is using a Custom provider (ie. custom implementation of IDSP) // Note: DataService .CreateDataSource is NOT invoked for custom data service providers // // c. If provider == null, then: // i. Create an instance of T by invoking DataService .CreateDataSource (this method may be overridden by a service author) // ii. If T implements IDataServiceMetadataProvider then the service is using a custom data service provider (skip step iii. & iv.) // iii. If typeof(T) == typeof(System.Data.Objects.ObjectContext) then the service will use the built-in IDSP implementation for EF (ie. the “EF provider”) // iv. If typeof(T) != typeof(System.Data.Objects.ObjectContext) then the service will use the built-in reflection for arbitrary .NET classes (ie. the “reflection provider”) Type dataServiceType = this.GetType(); Type dataContextType = typeof(T); bool friendlyFeedsV1Compatible; // If the GetService call returns a provider, that means there is a custom implementation of the provider IDataServiceMetadataProvider metadataProviderInstance = WebUtil.GetService (this); IDataServiceQueryProvider queryProviderInstance = null; object dataSourceInstance = null; if (metadataProviderInstance != null) { queryProviderInstance = WebUtil.GetService (this); if (queryProviderInstance == null) { throw new InvalidOperationException(Strings.DataService_IDataServiceQueryProviderNull); } // For custom providers, we will first query the queryProvider to check if the provider returns a data source instance. // If it doesn't, then we will create a instance of data source and pass it to the query provider. dataSourceInstance = queryProviderInstance.CurrentDataSource; if (dataSourceInstance == null) { dataSourceInstance = this.CreateDataSourceInstance(); queryProviderInstance.CurrentDataSource = dataSourceInstance; } if (!dataContextType.IsAssignableFrom(dataSourceInstance.GetType())) { throw new InvalidOperationException(Strings.DataServiceProviderWrapper_DataSourceTypeMustBeAssignableToContextType); } } else { // Create the data source from the service by calling DataService .CreateDataSource dataSourceInstance = this.CreateDataSourceInstance(); // Try if the data source implements IDSMP metadataProviderInstance = dataSourceInstance as IDataServiceMetadataProvider; if (metadataProviderInstance != null) { queryProviderInstance = dataSourceInstance as IDataServiceQueryProvider; if (queryProviderInstance == null) { throw new InvalidOperationException(Strings.DataService_IDataServiceQueryProviderNull); } // For customer providers if we already have the data source instance, we will pass it to the query provider. queryProviderInstance.CurrentDataSource = dataSourceInstance; } } // If we found IDSMP by now - it means we will use custom provider, otherwise we will use one of our built-in providers if (metadataProviderInstance != null) { Debug.Assert(queryProviderInstance != null, "If we have IDSMP we should also have IDSQP."); // For IDSMP, we cache the configuration object and we must NOT cache any of the metadata objects. // This means we call InitializeService() once per service instead of once per request. MetadataCacheItem metadata = MetadataCache.TryLookup(dataServiceType, dataSourceInstance); bool metadataRequiresInitialization = metadata == null; if (metadataRequiresInitialization) { metadata = new MetadataCacheItem(dataContextType); metadata.Configuration = CreateConfiguration(dataServiceType, metadataProviderInstance); MetadataCache.AddCacheItem(dataServiceType, dataSourceInstance, metadata); } Debug.Assert(metadata != null, "Metadata item should have been found or created within this function"); this.configuration = metadata.Configuration; // For IDSMP, we cannot cache anything except for the configuration. // We need to pass in a new MetadataCacheItem instance for each request. metadata = new MetadataCacheItem(dataContextType) { Configuration = this.configuration }; this.provider = new DataServiceProviderWrapper(metadata, metadataProviderInstance, queryProviderInstance); // For IDSMP we have to assume that friendly feeds are V1 compatible. We will validate this assumption // when processing requests and throw an exception if this happens to be false. friendlyFeedsV1Compatible = true; } else { MetadataCacheItem metadata = MetadataCache.TryLookup(dataServiceType, dataSourceInstance); bool metadataRequiresInitialization = metadata == null; if (metadataRequiresInitialization) { metadata = new MetadataCacheItem(dataContextType); } BaseServiceProvider dataProviderInstance; // use our built-in providers and policy layer if (typeof(ObjectContext).IsAssignableFrom(dataContextType)) { dataProviderInstance = new ObjectContextServiceProvider(metadata, this); } else { dataProviderInstance = new ReflectionServiceProvider(metadata, this); } dataProviderInstance.CurrentDataSource = dataSourceInstance; this.provider = new DataServiceProviderWrapper(metadata, dataProviderInstance, dataProviderInstance); dataProviderInstance.ProviderWrapper = this.provider; if (metadataRequiresInitialization) { // Populate metadata in provider. dataProviderInstance.PopulateMetadata(); dataProviderInstance.AddOperationsFromType(dataServiceType); // Create and cache configuration, which goes hand-in-hand with metadata. metadata.Configuration = CreateConfiguration(dataServiceType, dataProviderInstance); // Apply the access rights info from the configuration. dataProviderInstance.ApplyConfiguration(metadata.Configuration); // After all the operations are done, make metadata readonly. dataProviderInstance.MakeMetadataReadonly(); // Populate and cache the metadata. this.provider.PopulateMetadataCacheItemForV1Provider(); MetadataCache.AddCacheItem(dataServiceType, dataSourceInstance, metadata); } this.configuration = metadata.Configuration; friendlyFeedsV1Compatible = metadata.EpmIsV1Compatible; } this.configuration.ValidateServerOptions(friendlyFeedsV1Compatible); Debug.Assert(this.configuration != null, "configuration != null"); Debug.Assert(this.provider != null, "wrapper != null"); } /// /// Processes the incoming request and cache all the request headers /// ///description about the request uri. private RequestDescription ProcessIncomingRequestUri() { Debug.Assert( this.operationContext != null && this.operationContext.Host != null, "this.operationContext != null && this.operationContext.Host != null"); DataServiceHostWrapper host = this.operationContext.Host; // Validation of query parameters must happen only after the request parameters have been cached, // otherwise we might not serialize the errors in the correct serialization format. host.VerifyQueryParameters(); ValidateRequest(this.operationContext); // Query Processing Pipeline - Request start event DataServiceProcessingPipelineEventArgs eventArg = new DataServiceProcessingPipelineEventArgs(this.operationContext); this.processingPipeline.InvokeProcessingRequest(this, eventArg); // V1 OnStartProcessingRequest(). ((IDataService)this).InternalOnStartProcessingRequest(new ProcessRequestArgs(host.AbsoluteRequestUri, false /*isBatchOperation*/, this.operationContext)); // Query Processing Pipeline - Changeset start event // Note 1 we only invoke the event handler for CUD operations // Note 2 for a batch request this event will be invoked when we process the changeset boundary if (host.AstoriaHttpVerb != AstoriaVerbs.GET && !this.operationContext.IsBatchRequest) { this.processingPipeline.InvokeProcessingChangeset(this, new EventArgs()); } return RequestUriProcessor.ProcessRequestUri(host.AbsoluteRequestUri, this); } ////// Create the data source instance by calling the CreateDataSource virtual method /// ///returns the instance of the data source. private object CreateDataSourceInstance() { object dataSourceInstance = this.CreateDataSource(); if (dataSourceInstance == null) { throw new InvalidOperationException(Strings.DataService_CreateDataSourceNull); } return dataSourceInstance; } #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 HashSetcontentIds = new HashSet (new Int32EqualityComparer()); /// Dictionary to track objects represented by each content id within a changeset. private readonly DictionarycontentIdsToSegmentInfoMapping = 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 ListbatchRequestDescription = new List (); /// List of the all response headers and results of each operation within a changeset. private ListbatchOperationContexts = new List (); /// Number of CUD operations encountered in the current changeset. private int changeSetElementCount; ///The context of the current batch operation. private DataServiceOperationContext operationContext; ///Instance which implements IUpdatable interface. private UpdatableWrapper updatable; ///Instance which implements the IDataServicePagingProvider interface. private DataServicePagingProviderWrapper pagingProvider; ///Instance which implements IDataServiceStreamProvider interface. private DataServiceStreamProviderWrapper streamProvider; #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. public DataServiceConfiguration Configuration { get { return this.dataService.Configuration; } } ///Data provider for this data service. public DataServiceProviderWrapper Provider { get { return this.dataService.Provider; } } ///IUpdatable interface for this provider public UpdatableWrapper Updatable { get { return this.updatable ?? (this.updatable = new UpdatableWrapper(this)); } } ///IDataServicePagingProvider wrapper object. public DataServicePagingProviderWrapper PagingProvider { get { return this.pagingProvider ?? (this.pagingProvider = new DataServicePagingProviderWrapper(this)); } } ///Instance which implements IDataServiceStreamProvider interface. public DataServiceStreamProviderWrapper StreamProvider { get { return this.streamProvider ?? (this.streamProvider = new DataServiceStreamProviderWrapper(this)); } } ///Instance of the data provider. public object Instance { get { return this.dataService.Instance; } } ///Gets the context of the current batch operation. public DataServiceOperationContext OperationContext { get { return this.operationContext; } } ///Processing pipeline events public DataServiceProcessingPipeline ProcessingPipeline { get { return this.dataService.ProcessingPipeline; } } ////// 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. public void InternalApplyingExpansions(IQueryable queryable, ICollectionexpandPaths) { this.dataService.InternalApplyingExpansions(queryable, expandPaths); } /// Processes a catchable exception. /// The arguments describing how to handle the exception. public void InternalHandleException(HandleExceptionArgs args) { this.dataService.InternalHandleException(args); } #if ASTORIA_FF_CALLBACKS ////// Invoked once feed has been written to override the feed elements /// /// Feed being written void IDataService.InternalOnWriteFeed(SyndicationFeed feed) { this.dataService.InternalOnWriteFeed(feed); } ////// Invoked once an element has been written to override the element /// /// Item that has been written /// Object with content for thevoid IDataService.InternalOnWriteItem(SyndicationItem item, object obj) { this.dataService.InternalOnWriteItem(item, obj); } #endif /// /// 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. public SegmentInfo 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. public object GetResource(RequestDescription description, int segmentIndex, string typeFullName) { if (Deserializer.IsCrossReferencedSegment(description.SegmentInfos[0], this)) { Debug.Assert(segmentIndex >= 0 && segmentIndex < description.SegmentInfos.Length, "segment index must be a valid one"); if (description.SegmentInfos[segmentIndex].RequestEnumerable == null) { object resource = Deserializer.GetCrossReferencedResource(description.SegmentInfos[0]); for (int i = 1; i <= segmentIndex; i++) { resource = this.Updatable.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 Deserializer.GetCrossReferencedResource(description.SegmentInfos[segmentIndex]); } } return Deserializer.GetResource(description.SegmentInfos[segmentIndex], typeFullName, this, false /*checkForNull*/); } ////// Dispose the data source instance /// public void DisposeDataSource() { #if DEBUG this.dataService.ProcessingPipeline.AssertDebugStateAtDispose(); this.dataService.ProcessingPipeline.HasDisposedProviderInterfaces = true; #endif if (this.updatable != null) { this.updatable.DisposeProvider(); this.updatable = null; } if (this.pagingProvider != null) { this.pagingProvider.DisposeProvider(); this.pagingProvider = null; } if (this.streamProvider != null) { this.streamProvider.DisposeProvider(); this.streamProvider = null; } this.dataService.DisposeDataSource(); } ////// This method is called before a request is processed. /// /// Information about the request that is going to be processed. public void InternalOnStartProcessingRequest(ProcessRequestArgs args) { this.dataService.InternalOnStartProcessingRequest(args); } #endregion ////// Handle the batch content /// /// response stream for writing batch response internal void HandleBatchContent(Stream responseStream) { DataServiceOperationContext currentOperationContext = null; RequestDescription description; string changesetBoundary = null; Exception exceptionEncountered = null; bool serviceOperationRequests = true; try { // After we have completely read the request, we should not close // the request stream, since its owned by the underlying host. StreamWriter writer = new StreamWriter(responseStream, HttpProcessUtility.FallbackEncoding); while (!this.batchLimitExceeded && this.batchRequestStream.State != BatchStreamState.EndBatch) { // clear the context from the last operation this.operationContext = 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); // Query Processing Pipeline - Changeset start event // Note we only invoke the event handler for CUD operations this.dataService.ProcessingPipeline.InvokeProcessingChangeset(this.dataService, new EventArgs()); 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 && this.batchRequestDescription.Count > 0) { Debug.Assert(!String.IsNullOrEmpty(changesetBoundary), "!String.IsNullOrEmpty(changesetBoundary)"); // Bug 470090: We don't need to call SaveChanges if all requests in the changesets are requests to // service operations. But in V1, we used to call SaveChanges, we need to keep calling it, if its // implemented. if (serviceOperationRequests) { if (this.Provider.IsV1ProviderAndImplementsUpdatable()) { this.Updatable.SaveChanges(); } } else { // Save all the changes and write the response this.Updatable.SaveChanges(); } } if (exceptionEncountered == null) { // Query Processing Pipeline - Changeset end event // Note 1 we only invoke the event handler for CUD operations // Note 2 we invoke this event immediately after SaveChanges() // Note 3 we invoke this event before serialization happens this.dataService.ProcessingPipeline.InvokeProcessedChangeset(this.dataService, new EventArgs()); Debug.Assert(this.batchOperationContexts.Count == this.batchRequestDescription.Count, "counts must be the same"); for (int i = 0; i < this.batchRequestDescription.Count; i++) { this.operationContext = this.batchOperationContexts[i]; this.WriteRequest(this.batchRequestDescription[i], this.batchOperationContexts[i].Host.BatchServiceHost); } BatchWriter.WriteEndBoundary(writer, changesetBoundary); } else { this.HandleChangesetException(exceptionEncountered, this.batchOperationContexts, changesetBoundary, writer); } break; #endregion //EndChangeSet case BatchStreamState.Get: #region GET Operation this.IncreaseBatchCount(); currentOperationContext = CreateOperationContextFromBatchStream( this.dataService.OperationContext.AbsoluteServiceUri, this.batchRequestStream, this.contentIds, this.batchBoundary, writer); this.operationContext = currentOperationContext; // it must be GET operation Debug.Assert(this.operationContext.Host.AstoriaHttpVerb == AstoriaVerbs.GET, "this.operationContext.Host.AstoriaHttpVerb == AstoriaVerbs.GET"); Debug.Assert(this.batchRequestDescription.Count == 0, "this.batchRequestDescription.Count == 0"); Debug.Assert(this.batchOperationContexts.Count == 0, "this.batchRequestHost.Count == 0"); this.dataService.InternalOnStartProcessingRequest(new ProcessRequestArgs(this.operationContext.AbsoluteRequestUri, true /*isBatchOperation*/, this.operationContext)); description = RequestUriProcessor.ProcessRequestUri(this.operationContext.AbsoluteRequestUri, this); description = ProcessIncomingRequest(description, this); this.WriteRequest(description, currentOperationContext.Host.BatchServiceHost); 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(); currentOperationContext = CreateOperationContextFromBatchStream(this.dataService.OperationContext.AbsoluteServiceUri, this.batchRequestStream, this.contentIds, changesetBoundary, writer); if (exceptionEncountered == null) { this.batchOperationContexts.Add(currentOperationContext); this.operationContext = currentOperationContext; this.dataService.InternalOnStartProcessingRequest(new ProcessRequestArgs(this.operationContext.AbsoluteRequestUri, true /*isBatchOperation*/, this.operationContext)); description = RequestUriProcessor.ProcessRequestUri(this.operationContext.AbsoluteRequestUri, this); // If there are all batch requests in the changeset, then we don't need to call SaveChanges() serviceOperationRequests &= (description.TargetSource == RequestTargetSource.ServiceOperation); 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 (description != null) { if (this.batchRequestStream.State == BatchStreamState.Post) { Debug.Assert( description.TargetKind == RequestTargetKind.Resource || description.TargetSource == RequestTargetSource.ServiceOperation, "The target must be a resource or source should be a service operation, since otherwise cross-referencing doesn't make sense"); // if the content id is specified, only then add it to the collection string contentId = currentOperationContext.Host.BatchServiceHost.ContentId; if (contentId != null) { this.contentIdsToSegmentInfoMapping.Add(contentId, description.LastSegmentInfo); } } else if (this.batchRequestStream.State == BatchStreamState.Put) { // If this is a cross-referencing a previous POST resource, then we need to // replace the resource in the previous POST request with the new resource // that the provider returned for this request so that while serializing out, // we will have the same instance for POST/PUT this.UpdateRequestEnumerableForPut(description); } } } break; #endregion // CUD Operation default: Debug.Assert(this.batchRequestStream.State == BatchStreamState.EndBatch, "expecting end batch state"); // Query Processing Pipeline - Request end event // Note 1 we only invoke the event handler for ALL operations // Note 2 we invoke this event before serialization is complete // Note 3 we invoke this event before any provider interface held by the data service runtime is released/disposed DataServiceProcessingPipelineEventArgs eventArg = new DataServiceProcessingPipelineEventArgs(this.dataService.OperationContext); this.dataService.ProcessingPipeline.InvokeProcessedRequest(this.dataService, eventArg); break; } } catch (Exception exception) { if (!WebUtil.IsCatchableExceptionType(exception)) { throw; } if (this.batchRequestStream.State == BatchStreamState.EndChangeSet) { this.HandleChangesetException(exception, this.batchOperationContexts, 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 { DataServiceHostWrapper currentHost = this.operationContext == null ? null : this.operationContext.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 DataServiceHostWrapper(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.batchOperationContexts.Clear(); } } } BatchWriter.WriteEndBoundary(writer, this.batchBoundary); writer.Flush(); Exception ex = this.batchRequestStream.ValidateNoDataBeyondEndOfBatch(); if (ex != null) { ErrorHandler.HandleBatchRequestException(this, ex, writer); writer.Flush(); } } #if DEBUG catch { // We've hit an error, some of the processing pipeline events will not get fired. Setting this flag to skip validation. this.ProcessingPipeline.SkipDebugAssert = true; throw; } #endif finally { this.DisposeDataSource(); } } #region Private methods. ////// Creates an operation context for the current batch operation /// /// Absolute service uri /// batch stream which contains the header information. /// content ids that are defined in the batch. /// Part separator for host. /// Output writer. ///instance of the operation context which represents the current operation. private static DataServiceOperationContext CreateOperationContextFromBatchStream(Uri absoluteServiceUri, BatchStream batchStream, HashSetcontentIds, string boundary, StreamWriter writer) { Debug.Assert(absoluteServiceUri != null && absoluteServiceUri.IsAbsoluteUri, "absoluteServiceUri != null && absoluteServiceUri.IsAbsoluteUri"); 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; if (batchStream.ContentHeaders.TryGetValue(XmlConstants.HttpContentID, out contentIdValue) && !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(contentIdValue)); } if (!contentIds.Add(contentId)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_ContentIdMustBeUniqueInBatch(contentId)); } } BatchServiceHost host = new BatchServiceHost(absoluteServiceUri, batchStream, contentIdValue, boundary, writer); DataServiceOperationContext operationContext = new DataServiceOperationContext(true /*isBatchRequest*/, host); operationContext.InitializeAndCacheHeaders(); return operationContext; } /// /// Write the exception encountered in the middle of the changeset to the response /// /// exception encountered /// list of operation contexts in the changeset /// changeset boundary for the current processing changeset /// writer to which the response needs to be written private void HandleChangesetException( Exception exception, ListchangesetOperationContexts, string changesetBoundary, StreamWriter writer) { Debug.Assert(exception != null, "exception != null"); Debug.Assert(changesetOperationContexts != null, "changesetOperationContexts != 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 write the last operation information. // If there are no host, then just pass null. DataServiceHostWrapper currentHost = null; DataServiceOperationContext currentContext = changesetOperationContexts.Count == 0 ? null : changesetOperationContexts[changesetOperationContexts.Count - 1]; if (currentContext == null || currentContext.Host == null) { currentHost = new DataServiceHostWrapper(new BatchServiceHost(changesetBoundary, writer)); } else { currentHost = currentContext.Host; } ErrorHandler.HandleBatchProcessException(this, currentHost, exception, writer); // Write end boundary for the changeset BatchWriter.WriteEndBoundary(writer, changesetBoundary); this.Updatable.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)); } } ////// For POST operations, the RequestEnumerable could be out of date /// when a PUT is referring to the POST within the changeset. /// We need to update the RequestEnumerable to reflect what actually /// happened to the database. /// /// description for the current request. private void UpdateRequestEnumerableForPut(RequestDescription requestDescription) { Debug.Assert(this.batchRequestStream.State == BatchStreamState.Put, "This method must be called only for PUT requests"); Debug.Assert(this.batchRequestDescription[this.batchRequestDescription.Count - 1] == requestDescription, "The current request description must be the last one"); Debug.Assert(this.batchRequestDescription.Count == this.batchOperationContexts.Count, "Host and request description count must be the same"); // If this PUT request is cross referencing some resource string identifier = requestDescription.SegmentInfos[0].Identifier; if (identifier.StartsWith("$", StringComparison.Ordinal)) { // Get the content id of the POST request that is being cross-referenced string contentId = identifier.Substring(1); // Now we need to scan all the previous request to find the // POST request resource which is cross-referenced by the current request // and replace the resource in the POST request by the current one. // Note: since today we do not return payloads in the PUT request, this is fine. // When we support that, we need to find all the PUT requests that also refers // to the same resource and replace it with the current version. // Ignore the last one, since the parameters to the method are the last ones. for (int i = 0; i < this.batchOperationContexts.Count - 1; i++) { DataServiceOperationContext previousContext = this.batchOperationContexts[i]; BatchServiceHost previousHost = previousContext.Host.BatchServiceHost; RequestDescription previousRequest = this.batchRequestDescription[i]; if (previousContext.Host.AstoriaHttpVerb == AstoriaVerbs.POST && previousHost.ContentId == contentId) { object resource = Deserializer.GetCrossReferencedResource(requestDescription.LastSegmentInfo); previousRequest.LastSegmentInfo.RequestEnumerable = new object[] { resource }; break; } } } } ////// 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, batchHost, batchHost.ContentId, batchHost.BoundaryString); } else { ActionresponseWriter = DataService .SerializeResponseBody(description, this); if (responseWriter != null) { BatchWriter.WriteBoundaryAndHeaders(batchHost.Writer, batchHost, batchHost.ContentId, batchHost.BoundaryString); batchHost.Writer.Flush(); responseWriter(batchHost.Writer.BaseStream); batchHost.Writer.WriteLine(); } else { BatchWriter.WriteBoundaryAndHeaders(batchHost.Writer, batchHost, batchHost.ContentId, batchHost.BoundaryString); } } } #endregion Private methods. } /// /// For performance reasons we reuse results from existing query to read a projected value. We create an enumerator /// containing the projected value but must not dispose the original query until later. This wrapper allows us to /// pass the created enumerator and dispose the query at the right time. /// private class QueryResultsWrapper : IEnumerator, IDisposable { ////// Query that needs to be disposed. /// private IEnumerator query; ////// Enumerator containing the projected property. /// private IEnumerator enumerator; ////// QueryResultsWrapper constructor /// /// Enumerator containing the projected value. /// Query that needs to be disposed. public QueryResultsWrapper(IEnumerator enumerator, IEnumerator query) { Debug.Assert(enumerator != null, "enumerator != null"); this.enumerator = enumerator; this.query = query; } #region IEnumerator Members ////// Gets the current element from enumerator. /// object IEnumerator.Current { get { return this.enumerator.Current; } } ////// Moves the enumerator to the next element. /// ///true if the enumerator moved;false if the enumerator reached the end of the collection. bool IEnumerator.MoveNext() { return this.enumerator.MoveNext(); } ////// Resets the enumerator to the initial position. /// void IEnumerator.Reset() { this.enumerator.Reset(); } #endregion #region IDisposable Members ////// Disposes the cached query. /// void IDisposable.Dispose() { WebUtil.Dispose(this.query); GC.SuppressFinalize(this); } #endregion } } } // 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.Data.Services.Common; using System.Diagnostics; using System.IO; using System.Linq; using System.ServiceModel; using System.ServiceModel.Activation; using System.ServiceModel.Channels; #if ASTORIA_FF_CALLBACKS using System.ServiceModel.Syndication; #endif 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. ////// [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class DataServicewill typically be a subtype of /// or another class that provides /// properties. /// : IRequestHandler, IDataService { #region Private fields. /// A delegate used to create an instance of the data context. private static FunccachedConstructor; /// Service configuration information. private DataServiceConfiguration configuration; ///Data provider for this data service. private DataServiceProviderWrapper provider; ///IUpdatable interface for this datasource's provider private UpdatableWrapper updatable; ///Custom paging provider interface exposed by the service. private DataServicePagingProviderWrapper pagingProvider; ///Context for the current operation. private DataServiceOperationContext operationContext; ///Reference to IDataServiceStreamProvider interface. private DataServiceStreamProviderWrapper streamProvider; ///Events for the data service processing pipeline. private DataServiceProcessingPipeline processingPipeline = new DataServiceProcessingPipeline(); #if DEBUG #pragma warning disable 0169 [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1823")] private ActionrequestQueryableConstructed; #pragma warning restore 0169 #endif #endregion Private fields. #region Properties. /// Events for the data service processing pipeline. public DataServiceProcessingPipeline ProcessingPipeline { [DebuggerStepThrough] get { return this.processingPipeline; } } ///Service configuration information. DataServiceConfiguration IDataService.Configuration { [DebuggerStepThrough] get { return this.configuration; } } ///Data provider for this data service DataServiceProviderWrapper IDataService.Provider { [DebuggerStepThrough] get { return this.provider; } } ///Paging provider for this data service. DataServicePagingProviderWrapper IDataService.PagingProvider { [DebuggerStepThrough] get { Debug.Assert(this.provider != null, "this.provider != null"); if (this.pagingProvider == null) { this.pagingProvider = new DataServicePagingProviderWrapper(this); } return this.pagingProvider; } } ///Returns the instance of data service. object IDataService.Instance { [DebuggerStepThrough] get { return this; } } ///Cached request headers. DataServiceOperationContext IDataService.OperationContext { [DebuggerStepThrough] get { return this.operationContext; } } ///Processing pipeline events DataServiceProcessingPipeline IDataService.ProcessingPipeline { [DebuggerStepThrough] get { return this.processingPipeline; } } ///IUpdatable interface for this provider UpdatableWrapper IDataService.Updatable { [DebuggerStepThrough] get { Debug.Assert(this.provider != null, "this.provider != null"); return this.updatable; } } ///Reference to IDataServiceStreamProvider interface. DataServiceStreamProviderWrapper IDataService.StreamProvider { [DebuggerStepThrough] get { Debug.Assert(this.provider != null, "this.provider != null"); return this.streamProvider ?? (this.streamProvider = new DataServiceStreamProviderWrapper(this)); } } ///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, ICollectionexpandPaths) { 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 DEBUG // We've hit an error, some of the processing pipeline events will not get fired. Setting this flag to skip validation. this.ProcessingPipeline.SkipDebugAssert = true; #endif if (!WebUtil.IsCatchableExceptionType(handlingException)) { throw; } args.Exception = handlingException; } } #if ASTORIA_FF_CALLBACKS ////// Invoked once feed has been written to override the feed elements /// /// Feed being written void IDataService.InternalOnWriteFeed(SyndicationFeed feed) { this.OnWriteFeed(feed); } ////// Invoked once an element has been written to override the element /// /// Item that has been written /// Object with content for thevoid IDataService.InternalOnWriteItem(SyndicationItem item, object obj) { this.OnWriteItem(item, obj); } #endif /// /// 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 DEBUG this.processingPipeline.AssertDebugStateAtDispose(); this.processingPipeline.HasDisposedProviderInterfaces = true; #endif if (this.updatable != null) { this.updatable.DisposeProvider(); this.updatable = null; } if (this.streamProvider != null) { this.streamProvider.DisposeProvider(); this.streamProvider = null; } if (this.pagingProvider != null) { this.pagingProvider.DisposeProvider(); this.pagingProvider = null; } 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) { #if DEBUG this.processingPipeline.AssertDebugStateAtOnStartProcessingRequest(); this.processingPipeline.OnStartProcessingRequestInvokeCount++; #endif 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.operationContext = new DataServiceOperationContext(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 { Action. writer = this.HandleRequest(); Debug.Assert(writer != null, "writer != null"); Message result = CreateMessage(MessageVersion.None, "", ((IDataServiceHost)httpHost).ResponseContentType, writer, this); // If SuppressEntityBody is false, WCF will call DelegateBodyWriter.OnWriteBodyContent(), which // will dispose the data source and stream provider. Otherwise we need to dispose them in the // finally block below. if (!System.ServiceModel.Web.WebOperationContext.Current.OutgoingResponse.SuppressEntityBody) { shouldDispose = false; } return result; } #if DEBUG catch { this.processingPipeline.SkipDebugAssert = true; throw; } #endif finally { if (shouldDispose) { ((IDataService)this).DisposeDataSource(); } } } /// Provides a host-agnostic entry point for request processing. public void ProcessRequest() { if (this.operationContext == null) { throw new InvalidOperationException(Strings.DataService_HostNotAttached); } try { Actionwriter = this.HandleRequest(); if (writer != null) { writer(this.operationContext.Host.ResponseStream); } } #if DEBUG catch { this.processingPipeline.SkipDebugAssert = true; throw; } #endif finally { ((IDataService)this).DisposeDataSource(); #if DEBUG // Need to reset the states since the caller can reuse the same service instance. this.processingPipeline.ResetDebugState(); #endif } } #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"); #if DEBUG this.processingPipeline.SkipDebugAssert = true; #endif } ////// 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. } #if ASTORIA_FF_CALLBACKS ////// Invoked once feed has been written to override the feed elements /// /// Feed being written protected virtual void OnWriteFeed(SyndicationFeed feed) { } ////// Invoked once an element has been written to override the element /// /// Item that has been written /// Object with content for theprotected virtual void OnWriteItem(SyndicationItem item, object obj) { } #endif #endregion Protected methods. #region Private methods. /// /// Checks that if etag values are specified in the header, they must be valid. /// /// header values. /// request description. private static void CheckETagValues(DataServiceHostWrapper host, RequestDescription description) { Debug.Assert(host != null, "host != null"); // Media Resource ETags can be strong bool allowStrongEtag = description.TargetKind == RequestTargetKind.MediaResource; if (!WebUtil.IsETagValueValid(host.RequestIfMatch, allowStrongEtag)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_ETagValueNotValid(host.RequestIfMatch)); } if (!WebUtil.IsETagValueValid(host.RequestIfNoneMatch, allowStrongEtag)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_ETagValueNotValid(host.RequestIfNoneMatch)); } } ////// Creates a /// Version for message. /// Action for message. /// MIME content type for body. /// Callback. /// Service with context to dispose once the response has been written. ///that invokes the specified /// callback to write its body. /// 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 new data service configuration instance /// /// data service type /// provider instance ///data service configuration instance private static DataServiceConfiguration CreateConfiguration(Type dataServiceType, IDataServiceMetadataProvider provider) { Debug.Assert(dataServiceType != null, "dataServiceType != null"); Debug.Assert(provider != null, "provider != null"); DataServiceConfiguration configuration = new DataServiceConfiguration(provider); configuration.Initialize(dataServiceType); if (!(provider is BaseServiceProvider) && configuration.GetKnownTypes().Any()) { throw new InvalidOperationException(Strings.DataService_RegisterKnownTypeNotAllowedForIDSP); } configuration.Seal(); return configuration; } ////// 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(DataServiceHostWrapper 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. /// Context for current operation. private static void ValidateRequest(DataServiceOperationContext operationContext) { if (!String.IsNullOrEmpty(operationContext.Host.RequestIfMatch) && !String.IsNullOrEmpty(operationContext.Host.RequestIfNoneMatch)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_BothIfMatchAndIfNoneMatchHeaderSpecified); } } ////// Raises the response version header if necessary for the $metadata path. /// WARNING!!! This property can only be called for the $metadata path because it enumerates through all resource types. /// Calling it from outside of the $metadata path would break our IDSP contract. /// /// description about the request uri /// data service to which the request was made private static void RaiseResponseVersionForMetadata(RequestDescription description, IDataService dataService) { Debug.Assert(description.TargetKind == RequestTargetKind.Metadata, "This method can only be called from the $metadata path because it enumerates through all resource types."); if (dataService.Provider.IsV1Provider) { if (!dataService.Provider.GetEpmCompatiblityForV1Provider()) { description.RaiseResponseVersion(2, 0); } } else { foreach (ResourceType rt in dataService.Provider.Types) { if (!rt.EpmIsV1Compatible) { description.RaiseResponseVersion(2, 0); break; } } } } ////// 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.OperationContext.Host != null, "dataService.OperationContext.Host != null"); DataServiceHostWrapper host = dataService.OperationContext.Host; // Make a decision about metadata response version if (description.TargetKind == RequestTargetKind.Metadata) { RaiseResponseVersionForMetadata(description, dataService); } dataService.Configuration.ValidateMaxProtocolVersion(description); WebUtil.CheckVersion(dataService, description); CheckETagValues(host, description); ResourceSetWrapper lastSegmentContainer = description.LastSegmentInfo.TargetContainer; if (host.AstoriaHttpVerb == AstoriaVerbs.GET) { // This if expression was missing from V1.0, but is a breaking change to add it // without also checking for the new OverrideEntitySetRights if (description.LastSegmentInfo.Operation != null && (0 != (dataService.Configuration.GetServiceOperationRights(description.LastSegmentInfo.Operation.ServiceOperation) & ServiceOperationRights.OverrideEntitySetRights))) { DataServiceConfiguration.CheckServiceRights(description.LastSegmentInfo.Operation, description.IsSingleResult); } else { // For $count, the rights is already checked in the RequestUriProcessor and hence we don't need to check here. // Also, checking for ReadSingle right is wrong, since we need to only check for ReadMultiple rights. if (lastSegmentContainer != null && description.LastSegmentInfo.Identifier != XmlConstants.UriCountSegment) { DataServiceConfiguration.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 (host.AstoriaHttpVerb == AstoriaVerbs.POST) { newDescription = HandlePostOperation(description, dataService); if (description.LinkUri) { statusCode = 204; // 204 - No Content shouldWriteBody = false; } else { statusCode = 201; // 201 - Created. } } else if (host.AstoriaHttpVerb == AstoriaVerbs.PUT || host.AstoriaHttpVerb == AstoriaVerbs.MERGE) { if (lastSegmentContainer != null && !description.LinkUri) { if (host.AstoriaHttpVerb == AstoriaVerbs.PUT) { DataServiceConfiguration.CheckResourceRights(lastSegmentContainer, EntitySetRights.WriteReplace); } else { DataServiceConfiguration.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); Debug.Assert(description.ResponseVersion == RequestDescription.DataServiceDefaultResponseVersion, "description.ResponseVersion == RequestDescription.DataServiceDefaultResponseVersion"); statusCode = 204; // 204 - No Content } else if (host.AstoriaHttpVerb == AstoriaVerbs.DELETE) { if (lastSegmentContainer != null && !description.LinkUri) { DataServiceConfiguration.CheckResourceRights(lastSegmentContainer, EntitySetRights.WriteDelete); } HandleDeleteOperation(description, dataService); Debug.Assert(description.RequireMinimumVersion == new Version(1, 0), "description.RequireMinimumVersion == new Version(1, 0)"); Debug.Assert(description.ResponseVersion == RequestDescription.DataServiceDefaultResponseVersion, "description.ResponseVersion == RequestDescription.DataServiceDefaultResponseVersion"); 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. 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. Debug.Assert(description.ResponseVersion == newDescription.ResponseVersion, "description.ResponseVersion == newDescription.ResponseVersion"); host.ResponseVersion = newDescription.ResponseVersion.ToString() + ";"; host.ResponseStatusCode = statusCode; if (shouldWriteBody) { // 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 ActionSerializeResponseBody(RequestDescription description, IDataService dataService) { Debug.Assert(dataService.Provider != null, "dataService.Provider != null"); Debug.Assert(dataService.OperationContext.Host != null, "dataService.OperationContext.Host != null"); DataServiceHostWrapper host = dataService.OperationContext.Host; // 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 (!RequestDescription.IsETagHeaderAllowed(description)) { if (!String.IsNullOrEmpty(host.RequestIfMatch) || !String.IsNullOrEmpty(host.RequestIfNoneMatch)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_ETagCannotBeSpecified(host.AbsoluteRequestUri)); } } if (host.AstoriaHttpVerb == AstoriaVerbs.PUT || host.AstoriaHttpVerb == AstoriaVerbs.MERGE) { ResourceSetWrapper container; object actualEntity = GetContainerAndActualEntityInstance(dataService, description, out container); // We should only write etag in the response, if the type has one or more etag properties defined. // WriteETagValueInResponseHeader checks for null etag value (which means that no etag properties are defined) // that before calling the host. string etag; if (description.TargetKind == RequestTargetKind.MediaResource) { etag = dataService.StreamProvider.GetStreamETag(actualEntity, dataService.OperationContext); } else { etag = WebUtil.GetETagValue(dataService, actualEntity, container); } #if DEBUG WebUtil.WriteETagValueInResponseHeader(description, etag, host); #else WebUtil.WriteETagValueInResponseHeader(etag, host); #endif return WebUtil.GetEmptyStreamWriter(); } // 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.TargetResourceType, host.RequestAccept, description.MimeType, dataService); // This is the code path for service operations and GET requests returning multiple results if (description.TargetSource == RequestTargetSource.ServiceOperation || description.TargetSource == RequestTargetSource.None || !description.IsSingleResult) { // For service operations returning single result, etag checks must be performed by the service operation itself. Debug.Assert( (String.IsNullOrEmpty(host.RequestIfMatch) && String.IsNullOrEmpty(host.RequestIfNoneMatch)) || description.TargetSource == RequestTargetSource.ServiceOperation, "No etag can be specified for collection or it must be a service operation"); Encoding encoding = GetRequestAcceptEncoding(responseFormat, host.RequestAcceptCharSet); IEnumerator queryResults = WebUtil.GetRequestEnumerator(description.RequestEnumerable); try { bool hasMoved = queryResults.MoveNext(); if (description.IsSingleResult) { if (!hasMoved || queryResults.Current == null) { throw DataServiceException.CreateResourceNotFound(description.LastSegmentInfo.Identifier); } } // If we had to wait until we got a value to determine the valid contents, try that now. if (responseFormat == ContentFormat.Unknown) { responseFormat = ResolveUnknownFormat(description, queryResults.Current, dataService); } Debug.Assert(responseFormat != ContentFormat.Unknown, "responseFormat != ContentFormat.Unknown"); host.ResponseContentType = HttpProcessUtility.BuildContentType(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. /// resource type. /// Accept header value. /// Required MIME type. /// Data service. ////// The content format for the resource; Unknown if it cannot be determined statically. /// private static ContentFormat SelectResponseFormatForType( RequestTargetKind targetKind, ResourceType resourceType, string acceptTypesText, string mimeType, IDataService service) { ContentFormat responseFormat; if (targetKind == RequestTargetKind.PrimitiveValue) { responseFormat = SelectPrimitiveContentType(resourceType, acceptTypesText, mimeType, service.OperationContext.Host); } else if (targetKind == RequestTargetKind.MediaResource) { // We need the MLE instance to get the response format for the MediaResource. // We will resolve the response format in ResolveUnknownFormat() where we have the MLE instance. responseFormat = ContentFormat.Unknown; } else if (targetKind != RequestTargetKind.OpenPropertyValue) { bool entityTarget = targetKind == RequestTargetKind.Resource; responseFormat = SelectResponseFormat(service.OperationContext.Host, acceptTypesText, entityTarget); if (responseFormat == ContentFormat.Unsupported) { throw new DataServiceException(415, Strings.DataServiceException_UnsupportedMediaType); } } else { // We cannot negotiate a format until we know what the value is for the object. responseFormat = ContentFormat.Unknown; } return responseFormat; } ///Selects the correct content format for a primitive type. /// resource 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(ResourceType targetResourceType, string acceptTypesText, string requiredContentType, DataServiceHostWrapper host) { // Debug.Assert( // targetResourceType != null && // targetResourceType.ResourceTypeKind == ResourceTypeKind.Primitive, // "targetElementType != null && targetResourceType.ResourceTypeKind == ResourceTypeKind.Primitive"); string contentType; ContentFormat responseFormat = WebUtil.GetResponseFormatForPrimitiveValue(targetResourceType, out contentType); requiredContentType = requiredContentType ?? contentType; host.ResponseContentType = HttpProcessUtility.SelectRequiredMimeType( acceptTypesText, // acceptTypesText new string[] { requiredContentType }, // exactContentType requiredContentType); // inexactContentType return responseFormat; } ///Selects the correct content format for a media resource. /// The media link entry. /// Accept header value. /// Data service instance. ///The content format for the resource. private static ContentFormat SelectMediaResourceContentType(object mediaLinkEntry, string acceptTypesText, IDataService service) { Debug.Assert(mediaLinkEntry != null, "mediaLinkEntry != null"); Debug.Assert(service != null, "service != null"); string contentType = service.StreamProvider.GetStreamContentType(mediaLinkEntry, service.OperationContext); service.OperationContext.Host.ResponseContentType = HttpProcessUtility.SelectRequiredMimeType( acceptTypesText, // acceptTypesText new string[] { contentType }, // exactContentType contentType); // inexactContentType return ContentFormat.Binary; } ///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"); DataServiceHostWrapper host = dataService.OperationContext.Host; if (!String.IsNullOrEmpty(host.RequestIfMatch) || !String.IsNullOrEmpty(host.RequestIfNoneMatch)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_ETagSpecifiedForPost); } if (description.IsSingleResult) { throw DataServiceException.CreateMethodNotAllowed( Strings.BadRequest_InvalidUriForPostOperation(host.AbsoluteRequestUri), DataServiceConfiguration.GetAllowedMethods(dataService.Configuration, description)); } Debug.Assert( description.TargetSource == RequestTargetSource.EntitySet || description.Property.Kind == ResourcePropertyKind.ResourceSetReference, "Only ways to have collections of resources"); Stream requestStream = host.RequestStream; Debug.Assert(requestStream != null, "requestStream != null"); object entity = null; ResourceType targetResourceType = description.TargetResourceType; Debug.Assert(targetResourceType != null, "targetResourceType != null"); if (!description.LinkUri && dataService.Provider.HasDerivedTypes(targetResourceType) && WebUtil.HasMediaLinkEntryInHierarchy(targetResourceType, dataService.Provider)) { ResourceSetWrapper targetResourceSet = description.LastSegmentInfo.TargetContainer; Debug.Assert(targetResourceSet != null, "targetResourceSet != null"); targetResourceType = dataService.StreamProvider.ResolveType(targetResourceSet.Name, dataService); Debug.Assert(targetResourceType != null, "targetResourceType != null"); } UpdateTracker tracker = UpdateTracker.CreateUpdateTracker(dataService); if (!description.LinkUri && targetResourceType.IsMediaLinkEntry) { // Verify that the user has rights to add to the target container Debug.Assert(description.LastSegmentInfo.TargetContainer != null, "description.LastSegmentInfo.TargetContainer != null"); DataServiceConfiguration.CheckResourceRights(description.LastSegmentInfo.TargetContainer, EntitySetRights.WriteAppend); entity = Deserializer.CreateMediaLinkEntry(targetResourceType.FullName, requestStream, dataService, description, tracker); if (description.TargetSource == RequestTargetSource.Property) { Debug.Assert(description.Property.Kind == ResourcePropertyKind.ResourceSetReference, "Expecting POST resource set property"); Deserializer.HandleBindOperation(description, entity, dataService, tracker); } } else { using (Deserializer deserializer = Deserializer.CreateDeserializer(description, dataService, false /*update*/, tracker)) { entity = deserializer.HandlePostRequest(description); Debug.Assert(entity != null, "entity != null"); } } tracker.FireNotifications(); return RequestDescription.CreateSingleResultRequestDescription( description, entity, description.LastSegmentInfo.TargetContainer); } ///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"); DataServiceHostWrapper host = dataService.OperationContext.Host; if (!description.IsSingleResult) { throw DataServiceException.CreateMethodNotAllowed( Strings.BadRequest_InvalidUriForPutOperation(host.AbsoluteRequestUri), DataServiceConfiguration.GetAllowedMethods(dataService.Configuration, description)); } else if (description.LinkUri && description.Property.Kind != ResourcePropertyKind.ResourceReference) { throw DataServiceException.CreateMethodNotAllowed(Strings.DataService_CannotUpdateSetReferenceLinks, XmlConstants.HttpMethodDelete); } // Note that for Media Resources, we let the Stream Provider decide whether or not to support If-None-Match for PUT if (!String.IsNullOrEmpty(host.RequestIfNoneMatch) && description.TargetKind != RequestTargetKind.MediaResource) { throw DataServiceException.CreateBadRequestError(Strings.DataService_IfNoneMatchHeaderNotSupportedInPut); } else if (!RequestDescription.IsETagHeaderAllowed(description) && !String.IsNullOrEmpty(host.RequestIfMatch)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_ETagCannotBeSpecified(host.AbsoluteRequestUri)); } else if (description.Property != null && description.Property.IsOfKind(ResourcePropertyKind.Key)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_CannotUpdateKeyProperties(description.Property.Name)); } Stream requestStream = host.RequestStream; Debug.Assert(requestStream != null, "requestStream != null"); 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.OperationContext.Host != null, "dataService.OperationContext.Host != null"); DataServiceHostWrapper host = dataService.OperationContext.Host; // 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.IsSingleResult && description.LinkUri) { HandleUnbindOperation(description, dataService); } else if (description.IsSingleResult && description.TargetKind == RequestTargetKind.Resource) { Debug.Assert(description.LastSegmentInfo.TargetContainer != null, "description.LastSegmentInfo.TargetContainer != null"); if (description.RequestEnumerable == null) { throw DataServiceException.CreateBadRequestError(Strings.BadRequest_ResourceCanBeCrossReferencedOnlyForBindOperation); } // if (!String.IsNullOrEmpty(host.RequestIfNoneMatch)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_IfNoneMatchHeaderNotSupportedInDelete); } // 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*/); ResourceSetWrapper container = description.LastSegmentInfo.TargetContainer; // Need to check etag for DELETE operation dataService.Updatable.SetETagValues(entity, container); // object actualEntity = dataService.Updatable.ResolveResource(entity); ResourceType resourceType = dataService.Provider.GetResourceType(actualEntity); if (description.Property != null) { Debug.Assert(container != null, "container != null"); DataServiceConfiguration.CheckResourceRights(container, EntitySetRights.WriteDelete); } dataService.Updatable.DeleteResource(entity); if (resourceType != null && resourceType.IsMediaLinkEntry) { dataService.StreamProvider.DeleteStream(actualEntity, dataService.OperationContext); } UpdateTracker.FireNotification(dataService, 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 (!String.IsNullOrEmpty(host.RequestIfNoneMatch)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_IfNoneMatchHeaderNotSupportedInDelete); } 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'). ResourceSetWrapper container; // resource set to which the parent entity belongs to. object resource = Deserializer.GetResourceToModify(description, dataService, false /*allowCrossReference*/, out securityResource, out container, true /*checkETag*/); object actualEntity = dataService.Updatable.ResolveResource(securityResource); // 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); UpdateTracker.FireNotification(dataService, actualEntity, container, UpdateOperations.Change); } else if (description.TargetKind == RequestTargetKind.OpenProperty) { // Open navigation properties are not supported on OpenTypes. throw DataServiceException.CreateBadRequestError(Strings.OpenNavigationPropertiesNotSupportedOnOpenTypes(description.LastSegmentInfo.Identifier)); } else if (description.TargetKind == RequestTargetKind.OpenPropertyValue) { // if (!String.IsNullOrEmpty(host.RequestIfNoneMatch)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_IfNoneMatchHeaderNotSupportedInDelete); } object securityResource; ResourceSetWrapper container; object resource = Deserializer.GetResourceToModify(description, dataService, false /*allowCrossReference*/, out securityResource, out container, true /*checkETag*/); object actualEntity = dataService.Updatable.ResolveResource(securityResource); // 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); UpdateTracker.FireNotification(dataService, actualEntity, container, UpdateOperations.Change); } else { throw DataServiceException.CreateMethodNotAllowed( Strings.BadRequest_InvalidUriForDeleteOperation(host.AbsoluteRequestUri), DataServiceConfiguration.GetAllowedMethods(dataService.Configuration, 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 ActionHandleInternalResources(RequestDescription description, IDataService dataService) { string[] exactContentType = null; ContentFormat format = ContentFormat.Unknown; string mime = null; DataServiceHostWrapper host = dataService.OperationContext.Host; if (description.TargetKind == RequestTargetKind.Metadata) { exactContentType = new string[] { XmlConstants.MimeMetadata }; format = ContentFormat.MetadataDocument; mime = HttpProcessUtility.SelectRequiredMimeType( host.RequestAccept, // acceptTypesText exactContentType, // exactContentType XmlConstants.MimeApplicationXml); // inexactContentType } else if (description.TargetKind == RequestTargetKind.ServiceDirectory) { exactContentType = new string[] { XmlConstants.MimeApplicationAtomService, XmlConstants.MimeApplicationJson, XmlConstants.MimeApplicationXml }; mime = HttpProcessUtility.SelectRequiredMimeType( host.RequestAccept, // 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(host.RequestAcceptCharSet); 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 ActionCompareETagAndWriteResponse( RequestDescription description, ContentFormat responseFormat, IDataService dataService) { Debug.Assert(description != null, "description != null"); Debug.Assert(dataService != null, "dataService != null"); Debug.Assert(dataService.OperationContext != null && dataService.OperationContext.Host != null, "dataService.OperationContext != null && dataService.OperationContext.Host != null"); DataServiceHostWrapper host = dataService.OperationContext.Host; Debug.Assert( String.IsNullOrEmpty(host.RequestIfMatch) || String.IsNullOrEmpty(host.RequestIfNoneMatch), "Both If-Match and If-None-Match header cannot be specified"); IEnumerator queryResults = null; try { if (host.AstoriaHttpVerb == AstoriaVerbs.GET) { bool writeResponse = true; // Get the index of the last resource in the request uri int parentResourceIndex = description.GetIndexOfTargetEntityResource(); Debug.Assert(parentResourceIndex >= 0 && parentResourceIndex < description.SegmentInfos.Length, "parentResourceIndex >= 0 && parentResourceIndex < description.SegmentInfos.Length"); SegmentInfo parentEntitySegment = description.SegmentInfos[parentResourceIndex]; queryResults = RequestDescription.GetSingleResultFromEnumerable(parentEntitySegment); object resource = queryResults.Current; string etagValue = null; if (description.LinkUri) { // This must be already checked in SerializeResponseBody method. Debug.Assert(String.IsNullOrEmpty(host.RequestIfMatch) && String.IsNullOrEmpty(host.RequestIfNoneMatch), "ETag cannot be specified for $link requests"); if (resource == null) { throw DataServiceException.CreateResourceNotFound(description.LastSegmentInfo.Identifier); } } else if (RequestDescription.IsETagHeaderAllowed(description) && description.TargetKind != RequestTargetKind.MediaResource) { // Media Resources have their own ETags, we let the Stream Provider handle it. No need to compare the MLE ETag here. ResourceSetWrapper container = parentEntitySegment.TargetContainer; Debug.Assert(container != null, "container != null"); etagValue = WebUtil.CompareAndGetETag(resource, resource, container, dataService, out writeResponse); } if (resource == null && description.TargetKind == RequestTargetKind.Resource) { Debug.Assert(description.Property != null, "non-open type property"); WebUtil.Dispose(queryResults); queryResults = null; // If you are querying reference nav property and the value is null, // return 204 - No Content e.g. /Customers(1)/BestFriend host.ResponseStatusCode = 204; // No Content return WebUtil.GetEmptyStreamWriter(); } if (writeResponse) { return WriteSingleElementResponse(description, responseFormat, queryResults, parentResourceIndex, etagValue, dataService); } else { WebUtil.Dispose(queryResults); queryResults = null; #if DEBUG WebUtil.WriteETagValueInResponseHeader(description, etagValue, host); #else WebUtil.WriteETagValueInResponseHeader(etagValue, host); #endif host.ResponseStatusCode = 304; // Not Modified return WebUtil.GetEmptyStreamWriter(); } } else { Debug.Assert(host.AstoriaHttpVerb == AstoriaVerbs.POST, "Must be POST method"); ResourceSetWrapper container; object actualEntity = GetContainerAndActualEntityInstance(dataService, description, out container); host.ResponseLocation = Serializer.GetUri(actualEntity, dataService.Provider, container, host.AbsoluteServiceUri).AbsoluteUri; string etagValue = WebUtil.GetETagValue(dataService, actualEntity, container); queryResults = RequestDescription.GetSingleResultFromEnumerable(description.LastSegmentInfo); return WriteSingleElementResponse(description, responseFormat, queryResults, description.SegmentInfos.Length - 1, etagValue, dataService); } } catch { WebUtil.Dispose(queryResults); throw; } } /// 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 == RequestTargetKind.MediaResource, description.TargetKind + " is open property, open property value, or MediaResource."); WebUtil.CheckResourceExists(element != null, description.LastSegmentInfo.Identifier); ResourceType resourceType = WebUtil.GetResourceType(dataService.Provider, element); Debug.Assert(resourceType != null, "resourceType != null, WebUtil.GetResourceType() should throw if it fails to resolve the resource type."); DataServiceHostWrapper host = dataService.OperationContext.Host; // Determine the appropriate target type based on the kind of resource. bool rawValue = description.TargetKind == RequestTargetKind.OpenPropertyValue || description.TargetKind == RequestTargetKind.MediaResource; 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) { if (resourceType.IsMediaLinkEntry) { return SelectMediaResourceContentType(element, host.RequestAccept, dataService); } else { throw DataServiceException.CreateBadRequestError(Strings.BadRequest_InvalidUriForMediaResource(host.AbsoluteRequestUri)); } } else { targetKind = RequestTargetKind.Resource; } break; } if (description.LinkUri) { targetKind = RequestTargetKind.Link; } return SelectResponseFormatForType(targetKind, resourceType, host.RequestAccept, null, dataService); } ////// 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 ActionWriteSingleElementResponse( RequestDescription description, ContentFormat responseFormat, IEnumerator queryResults, int parentResourceIndex, string etagValue, IDataService dataService) { try { // The queryResults parameter contains the enumerator of the parent resource. If the parent resource's RequestEnumerable is not // the same instance as that of the last segment, we need to get the enumerator for the last segment. // Take MediaResource for example, the MLE is its parent resource, which is what we want to write out and we don't want to // query for another instance of the enumerator. if (description.SegmentInfos[parentResourceIndex].RequestEnumerable != description.LastSegmentInfo.RequestEnumerable) { object resource = queryResults.Current; for (int segmentIdx = parentResourceIndex + 1; segmentIdx < description.SegmentInfos.Length; segmentIdx++) { SegmentInfo parentSegment = description.SegmentInfos[segmentIdx - 1]; SegmentInfo currentSegment = description.SegmentInfos[segmentIdx]; WebUtil.CheckResourceExists(resource != null, parentSegment.Identifier); // $value has the same query as the preceding segment. if (currentSegment.TargetKind == RequestTargetKind.PrimitiveValue || currentSegment.TargetKind == RequestTargetKind.OpenPropertyValue) { Debug.Assert(segmentIdx == description.SegmentInfos.Length - 1, "$value has to be the last segment."); break; } if (currentSegment.TargetKind == RequestTargetKind.OpenProperty) { ResourceType openTypeParentResourceType = WebUtil.GetResourceType(dataService.Provider, resource); if (openTypeParentResourceType.ResourceTypeKind == ResourceTypeKind.ComplexType) { ResourceProperty resProperty = openTypeParentResourceType.Properties.First(p => p.Name == currentSegment.Identifier); resource = WebUtil.GetPropertyValue(dataService.Provider, resource, openTypeParentResourceType, resProperty, null); } else { Debug.Assert(openTypeParentResourceType.ResourceTypeKind == ResourceTypeKind.EntityType, "Entity Type expected"); resource = WebUtil.GetPropertyValue(dataService.Provider, resource, openTypeParentResourceType, null, currentSegment.Identifier); } } else { resource = WebUtil.GetPropertyValue(dataService.Provider, resource, parentSegment.TargetResourceType, currentSegment.ProjectedProperty, null); } } RequestDescription.CheckQueryResult(resource, description.LastSegmentInfo); queryResults = new QueryResultsWrapper((new object[] { resource }).GetEnumerator(), queryResults); queryResults.MoveNext(); } // If we had to wait until we got a value to determine the valid contents, try that now. if (responseFormat == ContentFormat.Unknown) { responseFormat = ResolveUnknownFormat(description, queryResults.Current, dataService); } Debug.Assert(responseFormat != ContentFormat.Unknown, "responseFormat != ContentFormat.Unknown"); DataServiceHostWrapper host = dataService.OperationContext.Host; // Write the etag header #if DEBUG WebUtil.WriteETagValueInResponseHeader(description, etagValue, host); #else WebUtil.WriteETagValueInResponseHeader(etagValue, host); #endif Encoding encoding = GetRequestAcceptEncoding(responseFormat, host.RequestAcceptCharSet); host.ResponseContentType = HttpProcessUtility.BuildContentType(host.ResponseContentType, encoding); return new ResponseBodyWriter( encoding, true /* hasMoved */, dataService, queryResults, description, responseFormat).Write; } catch { WebUtil.Dispose(queryResults); throw; } } /// /// Returns the actual entity instance and its containers for the resource in the description results. /// /// Data service /// 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( IDataService service, RequestDescription description, out ResourceSetWrapper 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] = service.Updatable.ResolveResource(results[0]); container = description.LastSegmentInfo.TargetContainer; if (container == null) { // Open navigation properties are not supported on OpenTypes. throw DataServiceException.CreateBadRequestError(Strings.OpenNavigationPropertiesNotSupportedOnOpenTypes(description.LastSegmentInfo.Identifier)); } Debug.Assert(container != null, "description.LastSegmentInfo.TargetContainer != null"); return results[0]; } ////// 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"); if (!String.IsNullOrEmpty(dataService.OperationContext.Host.RequestIfMatch) || !String.IsNullOrEmpty(dataService.OperationContext.Host.RequestIfNoneMatch)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_ETagNotSupportedInUnbind); } object parentEntity; ResourceSetWrapper parentEntityResourceSet; Deserializer.GetResourceToModify(description, dataService, out parentEntity, out parentEntityResourceSet); Debug.Assert(description.Property != null, "description.Property != null"); if (description.Property.Kind == ResourcePropertyKind.ResourceReference) { dataService.Updatable.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.Updatable.RemoveReferenceFromCollection(parentEntity, description.Property.Name, childEntity); } if (dataService.Configuration.DataServiceBehavior.InvokeInterceptorsOnLinkDelete) { object actualParentEntity = dataService.Updatable.ResolveResource(parentEntity); UpdateTracker.FireNotification(dataService, actualParentEntity, parentEntityResourceSet, UpdateOperations.Change); } } ////// 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 (WebUtil.CompareMimeType(mime, XmlConstants.MimeApplicationJson)) { return ContentFormat.Json; } else if (WebUtil.CompareMimeType(mime, XmlConstants.MimeApplicationAtom)) { return ContentFormat.Atom; } else { Debug.Assert( WebUtil.CompareMimeType(mime, XmlConstants.MimeApplicationXml) || WebUtil.CompareMimeType(mime, XmlConstants.MimeApplicationAtomService) || WebUtil.CompareMimeType(mime, XmlConstants.MimeTextXml), "expecting application/xml, application/atomsvc+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 ActionHandleRequest() { Debug.Assert(this.operationContext != null, "this.operationContext != null"); // Need to cache the request headers for every request. Note that the while the underlying // host instance may stay the same across requests, the request headers can change between // requests. We have to refresh the cache for every request. this.operationContext.InitializeAndCacheHeaders(); Action writer = null; try { this.EnsureProviderAndConfigForRequest(); } catch (Exception ex) { #if DEBUG // We've hit an error, some of the processing pipeline events will not get fired. Setting this flag to skip validation. this.ProcessingPipeline.SkipDebugAssert = true; #endif int responseStatusCode = 500; if (!WebUtil.IsCatchableExceptionType(ex)) { throw; } // if Exception been thrown is DSE, we keep the exception's status code // otherwise, the status code is 500. DataServiceException dse = ex as DataServiceException; if (dse != null) { responseStatusCode = dse.StatusCode; } // safe handling of initialization time error DataServiceHostWrapper host = this.operationContext.Host; host.ResponseStatusCode = responseStatusCode; host.ResponseVersion = XmlConstants.DataServiceVersion1Dot0 + ";"; throw; } try { RequestDescription description = this.ProcessIncomingRequestUri(); if (description.TargetKind != RequestTargetKind.Batch) { writer = this.HandleNonBatchRequest(description); // Query Processing Pipeline - Request end event // Note 1 we only invoke the event handler for ALL operations // Note 2 we invoke this event before serialization is complete // Note 3 we invoke this event before any provider interface held by the data service runtime is released/disposed DataServiceProcessingPipelineEventArgs eventArg = new DataServiceProcessingPipelineEventArgs(this.operationContext); this.processingPipeline.InvokeProcessedRequest(this, eventArg); } else { writer = this.HandleBatchRequest(); } } catch (Exception exception) { #if DEBUG // We've hit an error, some of the processing pipeline events will not get fired. Setting this flag to skip validation. this.ProcessingPipeline.SkipDebugAssert = true; #endif // Exception should be re-thrown if not handled. if (!WebUtil.IsCatchableExceptionType(exception)) { throw; } string accept = (this.operationContext != null) ? this.operationContext.Host.RequestAccept : null; string acceptCharset = (this.operationContext != null) ? this.operationContext.Host.RequestAcceptCharSet : 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 ActionHandleNonBatchRequest(RequestDescription description) { Debug.Assert(description.TargetKind != RequestTargetKind.Batch, "description.TargetKind != RequestTargetKind.Batch"); bool serviceOperationRequest = (description.TargetSource == RequestTargetSource.ServiceOperation); // The reason to create UpdatableWrapper here is to make sure that the right data service instance is // passed to the UpdatableWrapper. Earlier this line used to live in EnsureProviderAndConfigForRequest // method, which means that same data service instance was passed to the UpdatableWrapper, irrespective // of whether this was a batch request or not. The issue with that was in UpdatableWrapper.SetConcurrencyValues // method, if someone tried to access the service.RequestParams, this will give you the headers for the // top level batch request, not the part of the batch request we are processing. this.updatable = new UpdatableWrapper(this); description = ProcessIncomingRequest(description, this); if (this.operationContext.Host.AstoriaHttpVerb != AstoriaVerbs.GET) { // Bug 470090: Since we used to call SaveChanges() for service operations in V1, we need to // keep doing that for V1 providers that implement IUpdatable. In other words, for ObjectContextServiceProvider // we will always do this, and for reflection service provider, we will have to check. if (serviceOperationRequest) { if (this.provider.IsV1ProviderAndImplementsUpdatable()) { this.updatable.SaveChanges(); } } else { this.updatable.SaveChanges(); } // Query Processing Pipeline - Changeset end event // Note 1 we only invoke the event handler for CUD operations // Note 2 we invoke this event immediately after SaveChanges() // Note 3 we invoke this event before serialization happens this.processingPipeline.InvokeProcessedChangeset(this, new EventArgs()); } return (description == null) ? WebUtil.GetEmptyStreamWriter() : SerializeResponseBody(description, this); } /// Handle the batch request. ///Returns the delegate which takes the response stream for writing the response. private ActionHandleBatchRequest() { Debug.Assert(this.operationContext != null && this.operationContext.Host != null, "this.operationContext != null && this.operationContext.Host != null"); DataServiceHostWrapper host = this.operationContext.Host; // Verify the HTTP method. if (host.AstoriaHttpVerb != AstoriaVerbs.POST) { throw DataServiceException.CreateMethodNotAllowed( Strings.DataService_BatchResourceOnlySupportsPost, XmlConstants.HttpMethodPost); } WebUtil.CheckVersion(this, null); // Verify the content type and get the boundary string Encoding encoding; string boundary; if (!BatchStream.GetBoundaryAndEncodingFromMultipartMixedContentType(host.RequestContentType, out boundary, out encoding) || String.IsNullOrEmpty(boundary)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_InvalidContentTypeForBatchRequest); } // Write the response headers host.ResponseStatusCode = 202; // OK host.ResponseCacheControl = XmlConstants.HttpCacheControlNoCache; string batchBoundary = XmlConstants.HttpMultipartBoundaryBatchResponse + '_' + Guid.NewGuid().ToString(); host.ResponseContentType = String.Format( System.Globalization.CultureInfo.InvariantCulture, "{0}; {1}={2}", XmlConstants.MimeMultiPartMixed, XmlConstants.HttpMultipartBoundary, batchBoundary); // DEVNOTE([....]): // Added for V2+ services // The batch response format should be 1.0 until we update the format itself // Each individual batch response will set its version to the batch host. host.ResponseVersion = XmlConstants.DataServiceVersion1Dot0 + ";"; BatchStream batchStream = new BatchStream(host.RequestStream, boundary, encoding, true); BatchDataService batchDataService = new BatchDataService(this, batchStream, batchBoundary); return batchDataService.HandleBatchContent; } /// Creates the provider and configuration as necessary to be used for this request. private void EnsureProviderAndConfigForRequest() { if (this.provider == null) { this.CreateProvider(); } else { Debug.Assert(this.configuration != null, "this.configuration != null -- otherwise this.provider was ----signed with no configuration"); } #if DEBUG // No event should be fired before this point. // No provider interfaces except IDSP should be created before this point. this.processingPipeline.AssertInitialDebugState(); #endif } ////// Creates a provider implementation that wraps the T type. /// private void CreateProvider() { // From the IDSP Spec: // If the class derived from DataServiceimplements IServiceProvider then: // a. Invoke // IDataServiceMetadataProvider provider = IServiceProvider.GetService(TypeOf(IDataMetadataServiceProvider)) // // b. If provider != null, then the service is using a Custom provider (ie. custom implementation of IDSP) // Note: DataService .CreateDataSource is NOT invoked for custom data service providers // // c. If provider == null, then: // i. Create an instance of T by invoking DataService .CreateDataSource (this method may be overridden by a service author) // ii. If T implements IDataServiceMetadataProvider then the service is using a custom data service provider (skip step iii. & iv.) // iii. If typeof(T) == typeof(System.Data.Objects.ObjectContext) then the service will use the built-in IDSP implementation for EF (ie. the “EF provider”) // iv. If typeof(T) != typeof(System.Data.Objects.ObjectContext) then the service will use the built-in reflection for arbitrary .NET classes (ie. the “reflection provider”) Type dataServiceType = this.GetType(); Type dataContextType = typeof(T); bool friendlyFeedsV1Compatible; // If the GetService call returns a provider, that means there is a custom implementation of the provider IDataServiceMetadataProvider metadataProviderInstance = WebUtil.GetService (this); IDataServiceQueryProvider queryProviderInstance = null; object dataSourceInstance = null; if (metadataProviderInstance != null) { queryProviderInstance = WebUtil.GetService (this); if (queryProviderInstance == null) { throw new InvalidOperationException(Strings.DataService_IDataServiceQueryProviderNull); } // For custom providers, we will first query the queryProvider to check if the provider returns a data source instance. // If it doesn't, then we will create a instance of data source and pass it to the query provider. dataSourceInstance = queryProviderInstance.CurrentDataSource; if (dataSourceInstance == null) { dataSourceInstance = this.CreateDataSourceInstance(); queryProviderInstance.CurrentDataSource = dataSourceInstance; } if (!dataContextType.IsAssignableFrom(dataSourceInstance.GetType())) { throw new InvalidOperationException(Strings.DataServiceProviderWrapper_DataSourceTypeMustBeAssignableToContextType); } } else { // Create the data source from the service by calling DataService .CreateDataSource dataSourceInstance = this.CreateDataSourceInstance(); // Try if the data source implements IDSMP metadataProviderInstance = dataSourceInstance as IDataServiceMetadataProvider; if (metadataProviderInstance != null) { queryProviderInstance = dataSourceInstance as IDataServiceQueryProvider; if (queryProviderInstance == null) { throw new InvalidOperationException(Strings.DataService_IDataServiceQueryProviderNull); } // For customer providers if we already have the data source instance, we will pass it to the query provider. queryProviderInstance.CurrentDataSource = dataSourceInstance; } } // If we found IDSMP by now - it means we will use custom provider, otherwise we will use one of our built-in providers if (metadataProviderInstance != null) { Debug.Assert(queryProviderInstance != null, "If we have IDSMP we should also have IDSQP."); // For IDSMP, we cache the configuration object and we must NOT cache any of the metadata objects. // This means we call InitializeService() once per service instead of once per request. MetadataCacheItem metadata = MetadataCache.TryLookup(dataServiceType, dataSourceInstance); bool metadataRequiresInitialization = metadata == null; if (metadataRequiresInitialization) { metadata = new MetadataCacheItem(dataContextType); metadata.Configuration = CreateConfiguration(dataServiceType, metadataProviderInstance); MetadataCache.AddCacheItem(dataServiceType, dataSourceInstance, metadata); } Debug.Assert(metadata != null, "Metadata item should have been found or created within this function"); this.configuration = metadata.Configuration; // For IDSMP, we cannot cache anything except for the configuration. // We need to pass in a new MetadataCacheItem instance for each request. metadata = new MetadataCacheItem(dataContextType) { Configuration = this.configuration }; this.provider = new DataServiceProviderWrapper(metadata, metadataProviderInstance, queryProviderInstance); // For IDSMP we have to assume that friendly feeds are V1 compatible. We will validate this assumption // when processing requests and throw an exception if this happens to be false. friendlyFeedsV1Compatible = true; } else { MetadataCacheItem metadata = MetadataCache.TryLookup(dataServiceType, dataSourceInstance); bool metadataRequiresInitialization = metadata == null; if (metadataRequiresInitialization) { metadata = new MetadataCacheItem(dataContextType); } BaseServiceProvider dataProviderInstance; // use our built-in providers and policy layer if (typeof(ObjectContext).IsAssignableFrom(dataContextType)) { dataProviderInstance = new ObjectContextServiceProvider(metadata, this); } else { dataProviderInstance = new ReflectionServiceProvider(metadata, this); } dataProviderInstance.CurrentDataSource = dataSourceInstance; this.provider = new DataServiceProviderWrapper(metadata, dataProviderInstance, dataProviderInstance); dataProviderInstance.ProviderWrapper = this.provider; if (metadataRequiresInitialization) { // Populate metadata in provider. dataProviderInstance.PopulateMetadata(); dataProviderInstance.AddOperationsFromType(dataServiceType); // Create and cache configuration, which goes hand-in-hand with metadata. metadata.Configuration = CreateConfiguration(dataServiceType, dataProviderInstance); // Apply the access rights info from the configuration. dataProviderInstance.ApplyConfiguration(metadata.Configuration); // After all the operations are done, make metadata readonly. dataProviderInstance.MakeMetadataReadonly(); // Populate and cache the metadata. this.provider.PopulateMetadataCacheItemForV1Provider(); MetadataCache.AddCacheItem(dataServiceType, dataSourceInstance, metadata); } this.configuration = metadata.Configuration; friendlyFeedsV1Compatible = metadata.EpmIsV1Compatible; } this.configuration.ValidateServerOptions(friendlyFeedsV1Compatible); Debug.Assert(this.configuration != null, "configuration != null"); Debug.Assert(this.provider != null, "wrapper != null"); } /// /// Processes the incoming request and cache all the request headers /// ///description about the request uri. private RequestDescription ProcessIncomingRequestUri() { Debug.Assert( this.operationContext != null && this.operationContext.Host != null, "this.operationContext != null && this.operationContext.Host != null"); DataServiceHostWrapper host = this.operationContext.Host; // Validation of query parameters must happen only after the request parameters have been cached, // otherwise we might not serialize the errors in the correct serialization format. host.VerifyQueryParameters(); ValidateRequest(this.operationContext); // Query Processing Pipeline - Request start event DataServiceProcessingPipelineEventArgs eventArg = new DataServiceProcessingPipelineEventArgs(this.operationContext); this.processingPipeline.InvokeProcessingRequest(this, eventArg); // V1 OnStartProcessingRequest(). ((IDataService)this).InternalOnStartProcessingRequest(new ProcessRequestArgs(host.AbsoluteRequestUri, false /*isBatchOperation*/, this.operationContext)); // Query Processing Pipeline - Changeset start event // Note 1 we only invoke the event handler for CUD operations // Note 2 for a batch request this event will be invoked when we process the changeset boundary if (host.AstoriaHttpVerb != AstoriaVerbs.GET && !this.operationContext.IsBatchRequest) { this.processingPipeline.InvokeProcessingChangeset(this, new EventArgs()); } return RequestUriProcessor.ProcessRequestUri(host.AbsoluteRequestUri, this); } ////// Create the data source instance by calling the CreateDataSource virtual method /// ///returns the instance of the data source. private object CreateDataSourceInstance() { object dataSourceInstance = this.CreateDataSource(); if (dataSourceInstance == null) { throw new InvalidOperationException(Strings.DataService_CreateDataSourceNull); } return dataSourceInstance; } #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 HashSetcontentIds = new HashSet (new Int32EqualityComparer()); /// Dictionary to track objects represented by each content id within a changeset. private readonly DictionarycontentIdsToSegmentInfoMapping = 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 ListbatchRequestDescription = new List (); /// List of the all response headers and results of each operation within a changeset. private ListbatchOperationContexts = new List (); /// Number of CUD operations encountered in the current changeset. private int changeSetElementCount; ///The context of the current batch operation. private DataServiceOperationContext operationContext; ///Instance which implements IUpdatable interface. private UpdatableWrapper updatable; ///Instance which implements the IDataServicePagingProvider interface. private DataServicePagingProviderWrapper pagingProvider; ///Instance which implements IDataServiceStreamProvider interface. private DataServiceStreamProviderWrapper streamProvider; #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. public DataServiceConfiguration Configuration { get { return this.dataService.Configuration; } } ///Data provider for this data service. public DataServiceProviderWrapper Provider { get { return this.dataService.Provider; } } ///IUpdatable interface for this provider public UpdatableWrapper Updatable { get { return this.updatable ?? (this.updatable = new UpdatableWrapper(this)); } } ///IDataServicePagingProvider wrapper object. public DataServicePagingProviderWrapper PagingProvider { get { return this.pagingProvider ?? (this.pagingProvider = new DataServicePagingProviderWrapper(this)); } } ///Instance which implements IDataServiceStreamProvider interface. public DataServiceStreamProviderWrapper StreamProvider { get { return this.streamProvider ?? (this.streamProvider = new DataServiceStreamProviderWrapper(this)); } } ///Instance of the data provider. public object Instance { get { return this.dataService.Instance; } } ///Gets the context of the current batch operation. public DataServiceOperationContext OperationContext { get { return this.operationContext; } } ///Processing pipeline events public DataServiceProcessingPipeline ProcessingPipeline { get { return this.dataService.ProcessingPipeline; } } ////// 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. public void InternalApplyingExpansions(IQueryable queryable, ICollectionexpandPaths) { this.dataService.InternalApplyingExpansions(queryable, expandPaths); } /// Processes a catchable exception. /// The arguments describing how to handle the exception. public void InternalHandleException(HandleExceptionArgs args) { this.dataService.InternalHandleException(args); } #if ASTORIA_FF_CALLBACKS ////// Invoked once feed has been written to override the feed elements /// /// Feed being written void IDataService.InternalOnWriteFeed(SyndicationFeed feed) { this.dataService.InternalOnWriteFeed(feed); } ////// Invoked once an element has been written to override the element /// /// Item that has been written /// Object with content for thevoid IDataService.InternalOnWriteItem(SyndicationItem item, object obj) { this.dataService.InternalOnWriteItem(item, obj); } #endif /// /// 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. public SegmentInfo 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. public object GetResource(RequestDescription description, int segmentIndex, string typeFullName) { if (Deserializer.IsCrossReferencedSegment(description.SegmentInfos[0], this)) { Debug.Assert(segmentIndex >= 0 && segmentIndex < description.SegmentInfos.Length, "segment index must be a valid one"); if (description.SegmentInfos[segmentIndex].RequestEnumerable == null) { object resource = Deserializer.GetCrossReferencedResource(description.SegmentInfos[0]); for (int i = 1; i <= segmentIndex; i++) { resource = this.Updatable.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 Deserializer.GetCrossReferencedResource(description.SegmentInfos[segmentIndex]); } } return Deserializer.GetResource(description.SegmentInfos[segmentIndex], typeFullName, this, false /*checkForNull*/); } ////// Dispose the data source instance /// public void DisposeDataSource() { #if DEBUG this.dataService.ProcessingPipeline.AssertDebugStateAtDispose(); this.dataService.ProcessingPipeline.HasDisposedProviderInterfaces = true; #endif if (this.updatable != null) { this.updatable.DisposeProvider(); this.updatable = null; } if (this.pagingProvider != null) { this.pagingProvider.DisposeProvider(); this.pagingProvider = null; } if (this.streamProvider != null) { this.streamProvider.DisposeProvider(); this.streamProvider = null; } this.dataService.DisposeDataSource(); } ////// This method is called before a request is processed. /// /// Information about the request that is going to be processed. public void InternalOnStartProcessingRequest(ProcessRequestArgs args) { this.dataService.InternalOnStartProcessingRequest(args); } #endregion ////// Handle the batch content /// /// response stream for writing batch response internal void HandleBatchContent(Stream responseStream) { DataServiceOperationContext currentOperationContext = null; RequestDescription description; string changesetBoundary = null; Exception exceptionEncountered = null; bool serviceOperationRequests = true; try { // After we have completely read the request, we should not close // the request stream, since its owned by the underlying host. StreamWriter writer = new StreamWriter(responseStream, HttpProcessUtility.FallbackEncoding); while (!this.batchLimitExceeded && this.batchRequestStream.State != BatchStreamState.EndBatch) { // clear the context from the last operation this.operationContext = 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); // Query Processing Pipeline - Changeset start event // Note we only invoke the event handler for CUD operations this.dataService.ProcessingPipeline.InvokeProcessingChangeset(this.dataService, new EventArgs()); 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 && this.batchRequestDescription.Count > 0) { Debug.Assert(!String.IsNullOrEmpty(changesetBoundary), "!String.IsNullOrEmpty(changesetBoundary)"); // Bug 470090: We don't need to call SaveChanges if all requests in the changesets are requests to // service operations. But in V1, we used to call SaveChanges, we need to keep calling it, if its // implemented. if (serviceOperationRequests) { if (this.Provider.IsV1ProviderAndImplementsUpdatable()) { this.Updatable.SaveChanges(); } } else { // Save all the changes and write the response this.Updatable.SaveChanges(); } } if (exceptionEncountered == null) { // Query Processing Pipeline - Changeset end event // Note 1 we only invoke the event handler for CUD operations // Note 2 we invoke this event immediately after SaveChanges() // Note 3 we invoke this event before serialization happens this.dataService.ProcessingPipeline.InvokeProcessedChangeset(this.dataService, new EventArgs()); Debug.Assert(this.batchOperationContexts.Count == this.batchRequestDescription.Count, "counts must be the same"); for (int i = 0; i < this.batchRequestDescription.Count; i++) { this.operationContext = this.batchOperationContexts[i]; this.WriteRequest(this.batchRequestDescription[i], this.batchOperationContexts[i].Host.BatchServiceHost); } BatchWriter.WriteEndBoundary(writer, changesetBoundary); } else { this.HandleChangesetException(exceptionEncountered, this.batchOperationContexts, changesetBoundary, writer); } break; #endregion //EndChangeSet case BatchStreamState.Get: #region GET Operation this.IncreaseBatchCount(); currentOperationContext = CreateOperationContextFromBatchStream( this.dataService.OperationContext.AbsoluteServiceUri, this.batchRequestStream, this.contentIds, this.batchBoundary, writer); this.operationContext = currentOperationContext; // it must be GET operation Debug.Assert(this.operationContext.Host.AstoriaHttpVerb == AstoriaVerbs.GET, "this.operationContext.Host.AstoriaHttpVerb == AstoriaVerbs.GET"); Debug.Assert(this.batchRequestDescription.Count == 0, "this.batchRequestDescription.Count == 0"); Debug.Assert(this.batchOperationContexts.Count == 0, "this.batchRequestHost.Count == 0"); this.dataService.InternalOnStartProcessingRequest(new ProcessRequestArgs(this.operationContext.AbsoluteRequestUri, true /*isBatchOperation*/, this.operationContext)); description = RequestUriProcessor.ProcessRequestUri(this.operationContext.AbsoluteRequestUri, this); description = ProcessIncomingRequest(description, this); this.WriteRequest(description, currentOperationContext.Host.BatchServiceHost); 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(); currentOperationContext = CreateOperationContextFromBatchStream(this.dataService.OperationContext.AbsoluteServiceUri, this.batchRequestStream, this.contentIds, changesetBoundary, writer); if (exceptionEncountered == null) { this.batchOperationContexts.Add(currentOperationContext); this.operationContext = currentOperationContext; this.dataService.InternalOnStartProcessingRequest(new ProcessRequestArgs(this.operationContext.AbsoluteRequestUri, true /*isBatchOperation*/, this.operationContext)); description = RequestUriProcessor.ProcessRequestUri(this.operationContext.AbsoluteRequestUri, this); // If there are all batch requests in the changeset, then we don't need to call SaveChanges() serviceOperationRequests &= (description.TargetSource == RequestTargetSource.ServiceOperation); 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 (description != null) { if (this.batchRequestStream.State == BatchStreamState.Post) { Debug.Assert( description.TargetKind == RequestTargetKind.Resource || description.TargetSource == RequestTargetSource.ServiceOperation, "The target must be a resource or source should be a service operation, since otherwise cross-referencing doesn't make sense"); // if the content id is specified, only then add it to the collection string contentId = currentOperationContext.Host.BatchServiceHost.ContentId; if (contentId != null) { this.contentIdsToSegmentInfoMapping.Add(contentId, description.LastSegmentInfo); } } else if (this.batchRequestStream.State == BatchStreamState.Put) { // If this is a cross-referencing a previous POST resource, then we need to // replace the resource in the previous POST request with the new resource // that the provider returned for this request so that while serializing out, // we will have the same instance for POST/PUT this.UpdateRequestEnumerableForPut(description); } } } break; #endregion // CUD Operation default: Debug.Assert(this.batchRequestStream.State == BatchStreamState.EndBatch, "expecting end batch state"); // Query Processing Pipeline - Request end event // Note 1 we only invoke the event handler for ALL operations // Note 2 we invoke this event before serialization is complete // Note 3 we invoke this event before any provider interface held by the data service runtime is released/disposed DataServiceProcessingPipelineEventArgs eventArg = new DataServiceProcessingPipelineEventArgs(this.dataService.OperationContext); this.dataService.ProcessingPipeline.InvokeProcessedRequest(this.dataService, eventArg); break; } } catch (Exception exception) { if (!WebUtil.IsCatchableExceptionType(exception)) { throw; } if (this.batchRequestStream.State == BatchStreamState.EndChangeSet) { this.HandleChangesetException(exception, this.batchOperationContexts, 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 { DataServiceHostWrapper currentHost = this.operationContext == null ? null : this.operationContext.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 DataServiceHostWrapper(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.batchOperationContexts.Clear(); } } } BatchWriter.WriteEndBoundary(writer, this.batchBoundary); writer.Flush(); Exception ex = this.batchRequestStream.ValidateNoDataBeyondEndOfBatch(); if (ex != null) { ErrorHandler.HandleBatchRequestException(this, ex, writer); writer.Flush(); } } #if DEBUG catch { // We've hit an error, some of the processing pipeline events will not get fired. Setting this flag to skip validation. this.ProcessingPipeline.SkipDebugAssert = true; throw; } #endif finally { this.DisposeDataSource(); } } #region Private methods. ////// Creates an operation context for the current batch operation /// /// Absolute service uri /// batch stream which contains the header information. /// content ids that are defined in the batch. /// Part separator for host. /// Output writer. ///instance of the operation context which represents the current operation. private static DataServiceOperationContext CreateOperationContextFromBatchStream(Uri absoluteServiceUri, BatchStream batchStream, HashSetcontentIds, string boundary, StreamWriter writer) { Debug.Assert(absoluteServiceUri != null && absoluteServiceUri.IsAbsoluteUri, "absoluteServiceUri != null && absoluteServiceUri.IsAbsoluteUri"); 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; if (batchStream.ContentHeaders.TryGetValue(XmlConstants.HttpContentID, out contentIdValue) && !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(contentIdValue)); } if (!contentIds.Add(contentId)) { throw DataServiceException.CreateBadRequestError(Strings.DataService_ContentIdMustBeUniqueInBatch(contentId)); } } BatchServiceHost host = new BatchServiceHost(absoluteServiceUri, batchStream, contentIdValue, boundary, writer); DataServiceOperationContext operationContext = new DataServiceOperationContext(true /*isBatchRequest*/, host); operationContext.InitializeAndCacheHeaders(); return operationContext; } /// /// Write the exception encountered in the middle of the changeset to the response /// /// exception encountered /// list of operation contexts in the changeset /// changeset boundary for the current processing changeset /// writer to which the response needs to be written private void HandleChangesetException( Exception exception, ListchangesetOperationContexts, string changesetBoundary, StreamWriter writer) { Debug.Assert(exception != null, "exception != null"); Debug.Assert(changesetOperationContexts != null, "changesetOperationContexts != 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 write the last operation information. // If there are no host, then just pass null. DataServiceHostWrapper currentHost = null; DataServiceOperationContext currentContext = changesetOperationContexts.Count == 0 ? null : changesetOperationContexts[changesetOperationContexts.Count - 1]; if (currentContext == null || currentContext.Host == null) { currentHost = new DataServiceHostWrapper(new BatchServiceHost(changesetBoundary, writer)); } else { currentHost = currentContext.Host; } ErrorHandler.HandleBatchProcessException(this, currentHost, exception, writer); // Write end boundary for the changeset BatchWriter.WriteEndBoundary(writer, changesetBoundary); this.Updatable.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)); } } ////// For POST operations, the RequestEnumerable could be out of date /// when a PUT is referring to the POST within the changeset. /// We need to update the RequestEnumerable to reflect what actually /// happened to the database. /// /// description for the current request. private void UpdateRequestEnumerableForPut(RequestDescription requestDescription) { Debug.Assert(this.batchRequestStream.State == BatchStreamState.Put, "This method must be called only for PUT requests"); Debug.Assert(this.batchRequestDescription[this.batchRequestDescription.Count - 1] == requestDescription, "The current request description must be the last one"); Debug.Assert(this.batchRequestDescription.Count == this.batchOperationContexts.Count, "Host and request description count must be the same"); // If this PUT request is cross referencing some resource string identifier = requestDescription.SegmentInfos[0].Identifier; if (identifier.StartsWith("$", StringComparison.Ordinal)) { // Get the content id of the POST request that is being cross-referenced string contentId = identifier.Substring(1); // Now we need to scan all the previous request to find the // POST request resource which is cross-referenced by the current request // and replace the resource in the POST request by the current one. // Note: since today we do not return payloads in the PUT request, this is fine. // When we support that, we need to find all the PUT requests that also refers // to the same resource and replace it with the current version. // Ignore the last one, since the parameters to the method are the last ones. for (int i = 0; i < this.batchOperationContexts.Count - 1; i++) { DataServiceOperationContext previousContext = this.batchOperationContexts[i]; BatchServiceHost previousHost = previousContext.Host.BatchServiceHost; RequestDescription previousRequest = this.batchRequestDescription[i]; if (previousContext.Host.AstoriaHttpVerb == AstoriaVerbs.POST && previousHost.ContentId == contentId) { object resource = Deserializer.GetCrossReferencedResource(requestDescription.LastSegmentInfo); previousRequest.LastSegmentInfo.RequestEnumerable = new object[] { resource }; break; } } } } ////// 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, batchHost, batchHost.ContentId, batchHost.BoundaryString); } else { ActionresponseWriter = DataService .SerializeResponseBody(description, this); if (responseWriter != null) { BatchWriter.WriteBoundaryAndHeaders(batchHost.Writer, batchHost, batchHost.ContentId, batchHost.BoundaryString); batchHost.Writer.Flush(); responseWriter(batchHost.Writer.BaseStream); batchHost.Writer.WriteLine(); } else { BatchWriter.WriteBoundaryAndHeaders(batchHost.Writer, batchHost, batchHost.ContentId, batchHost.BoundaryString); } } } #endregion Private methods. } /// /// For performance reasons we reuse results from existing query to read a projected value. We create an enumerator /// containing the projected value but must not dispose the original query until later. This wrapper allows us to /// pass the created enumerator and dispose the query at the right time. /// private class QueryResultsWrapper : IEnumerator, IDisposable { ////// Query that needs to be disposed. /// private IEnumerator query; ////// Enumerator containing the projected property. /// private IEnumerator enumerator; ////// QueryResultsWrapper constructor /// /// Enumerator containing the projected value. /// Query that needs to be disposed. public QueryResultsWrapper(IEnumerator enumerator, IEnumerator query) { Debug.Assert(enumerator != null, "enumerator != null"); this.enumerator = enumerator; this.query = query; } #region IEnumerator Members ////// Gets the current element from enumerator. /// object IEnumerator.Current { get { return this.enumerator.Current; } } ////// Moves the enumerator to the next element. /// ///true if the enumerator moved;false if the enumerator reached the end of the collection. bool IEnumerator.MoveNext() { return this.enumerator.MoveNext(); } ////// Resets the enumerator to the initial position. /// void IEnumerator.Reset() { this.enumerator.Reset(); } #endregion #region IDisposable Members ////// Disposes the cached query. /// void IDisposable.Dispose() { WebUtil.Dispose(this.query); GC.SuppressFinalize(this); } #endregion } } } // 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
- PresentationTraceSources.cs
- PreloadHost.cs
- Tuple.cs
- _AutoWebProxyScriptHelper.cs
- PackagingUtilities.cs
- WindowsRegion.cs
- ContextStack.cs
- ImageCodecInfo.cs
- SourceFileBuildProvider.cs
- WebPartCancelEventArgs.cs
- HttpCapabilitiesSectionHandler.cs
- CustomTokenProvider.cs
- Table.cs
- ToolStripItemTextRenderEventArgs.cs
- TableItemStyle.cs
- GeneratedView.cs
- COSERVERINFO.cs
- PathFigureCollectionValueSerializer.cs
- WebHttpEndpoint.cs
- TypeConverterMarkupExtension.cs
- DataGridViewCellCollection.cs
- InstallerTypeAttribute.cs
- DocumentPageTextView.cs
- XmlSigningNodeWriter.cs
- PixelFormatConverter.cs
- PriorityItem.cs
- GridView.cs
- ServerValidateEventArgs.cs
- FormsAuthenticationConfiguration.cs
- EmulateRecognizeCompletedEventArgs.cs
- TextEmbeddedObject.cs
- PropertyValue.cs
- XmlStreamStore.cs
- SchemaElementLookUpTable.cs
- PrivateUnsafeNativeCompoundFileMethods.cs
- XmlTextEncoder.cs
- TypeUtil.cs
- XPathMessageFilterElementCollection.cs
- HttpModulesSection.cs
- WebReferenceOptions.cs
- basevalidator.cs
- FlowDocumentPageViewerAutomationPeer.cs
- BCryptSafeHandles.cs
- SqlDependencyListener.cs
- DocumentPageTextView.cs
- ConfigurationElementCollection.cs
- IgnoreDeviceFilterElementCollection.cs
- ConstructorArgumentAttribute.cs
- GroupLabel.cs
- XsdDateTime.cs
- documentsequencetextcontainer.cs
- TimeSpanConverter.cs
- RsaSecurityKey.cs
- HttpWebRequestElement.cs
- DataListItem.cs
- Soap12ServerProtocol.cs
- GroupQuery.cs
- CultureInfo.cs
- UnsafeMethods.cs
- Events.cs
- DocumentAutomationPeer.cs
- TimeIntervalCollection.cs
- IntPtr.cs
- ThemeDirectoryCompiler.cs
- EntityFunctions.cs
- DataGridViewRowCancelEventArgs.cs
- Main.cs
- ParallelRangeManager.cs
- DataSysAttribute.cs
- Rotation3D.cs
- Storyboard.cs
- TextRenderer.cs
- Bidi.cs
- ExpressionParser.cs
- ThreadExceptionEvent.cs
- XmlImplementation.cs
- ListViewInsertEventArgs.cs
- SchemaCollectionPreprocessor.cs
- TemplateControlBuildProvider.cs
- DbConnectionPoolOptions.cs
- SRGSCompiler.cs
- RelationalExpressions.cs
- ProxyWebPart.cs
- ToolStripDropDownItemDesigner.cs
- StatusBar.cs
- ToolStripDropTargetManager.cs
- GeometryHitTestResult.cs
- SrgsElementFactory.cs
- CodeGen.cs
- TrackingMemoryStreamFactory.cs
- FlowDocumentReader.cs
- WebPartHeaderCloseVerb.cs
- EventLog.cs
- safelink.cs
- ImportCatalogPart.cs
- CodeVariableDeclarationStatement.cs
- ForeignKeyFactory.cs
- SapiRecoContext.cs
- LineInfo.cs
- DBSqlParserTableCollection.cs