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