Beispiel #1
0
 /// <summary>
 /// Default constructor
 /// </summary>
 /// <param name="modelInstance">Entity object on for which this request was meant</param>
 /// <param name="entityInfo">Info about the entity object</param>
 /// <param name="method">Type of http method (GET/PATCH/POST/...)</param>
 /// <param name="apiCall">Rest call to execute</param>
 /// <param name="backupApiCall">Backup rest api call, will be used in case we encounter a mixed batch</param>
 /// <param name="fromJsonCasting">Delegate for json type parsing</param>
 /// <param name="postMappingJson">Delegate for post mapping</param>
 /// <param name="operationName">Name of the operation, used for telemetry purposes</param>
 /// <param name="order">Order of the request in the list of requests</param>
 internal BatchRequest(TransientObject modelInstance, EntityInfo entityInfo, HttpMethod method, ApiCall apiCall, ApiCall backupApiCall, Func <FromJson, object> fromJsonCasting, Action <string> postMappingJson, string operationName, int order)
 {
     Id              = Guid.NewGuid();
     Model           = modelInstance;
     EntityInfo      = entityInfo;
     Method          = method;
     ApiCall         = apiCall;
     BackupApiCall   = backupApiCall;
     FromJsonCasting = fromJsonCasting;
     PostMappingJson = postMappingJson;
     OperationName   = operationName;
     Order           = order;
     ExecutionNeeded = true;
 }
Beispiel #2
0
        /// <summary>
        /// When using REST batch requests the URL needs to be correctly cased, so we're loading the web url while doing an interactive request.
        /// Also loading the default needed properties to save additional loads for missing key properties
        /// </summary>
        /// <param name="context">PnPContext being initialized</param>
        /// <param name="options">Options for the initialization of this context</param>
        /// <returns></returns>
        internal static async Task InitializeContextAsync(PnPContext context, PnPContextOptions options)
        {
            // Set environment if not yet set
            if (!context.Environment.HasValue)
            {
                context.Environment = CloudManager.GetEnvironmentFromUri(context.Uri);
                // Ensure the Microsoft Graph URL is set depending on the used cloud environment
                context.GraphClient.UpdateBaseAddress(CloudManager.GetMicrosoftGraphAuthority(context.Environment.Value));
            }

            // Store the provided options, needed for context cloning
            context.LocalContextOptions = options;

            // IMPORTANT: this first call is an interactive call by design as that allows us set the
            //            web URL using the correct casing. Correct casing is required in REST batching

            // IMPORTANT: if you change this logic by adding more initialization data you also need
            //            to update the CopyContextInitialization method!

            // Combine the default properties to load with optional additional properties
            var(siteProps, webProps) = GetDefaultPropertiesToLoad(options);

            // Use the query client to build the correct initialization query for the given Web properties
            BaseDataModel <IWeb> concreteEntity = EntityManager.GetEntityConcreteInstance(typeof(IWeb), context.Web, context) as BaseDataModel <IWeb>;
            var entityInfo     = EntityManager.GetClassInfo(concreteEntity.GetType(), concreteEntity, null, webProps.ToArray());
            var apiCallRequest = await QueryClient.BuildGetAPICallAsync(concreteEntity, entityInfo, new ApiCall($"_api/Web", ApiType.SPORest), true).ConfigureAwait(false);

            // Load required web properties
            var api = new ApiCall(apiCallRequest.ApiCall.Request, ApiType.SPORest)
            {
                Interactive = true
            };

            await(context.Web as Web).RequestAsync(api, HttpMethod.Get, "Get").ConfigureAwait(false);

            // Replace the context URI with the value using the correct casing
            context.Uri = context.Web.Url;

            // Request the site properties
            await context.Site.LoadAsync(siteProps.ToArray()).ConfigureAwait(false);

            // Ensure the Graph ID is set once and only once
            if (context.Web is IMetadataExtensible me)
            {
                if (!me.Metadata.ContainsKey(PnPConstants.MetaDataGraphId))
                {
                    me.Metadata.Add(PnPConstants.MetaDataGraphId, $"{context.Uri.DnsSafeHost},{context.Site.Id},{context.Web.Id}");
                }
            }

            // If the GroupId is available ensure it's also correctly set on the Group metadata so that calls via that
            // model can work
            if (context.Site.IsPropertyAvailable(p => p.GroupId) && context.Site.GroupId != Guid.Empty)
            {
                if (context.Group is IMetadataExtensible groupMetaData)
                {
                    if (!groupMetaData.Metadata.ContainsKey(PnPConstants.MetaDataGraphId))
                    {
                        groupMetaData.Metadata.Add(PnPConstants.MetaDataGraphId, context.Site.GroupId.ToString());
                    }
                }
            }
        }
Beispiel #3
0
        /// <summary>
        /// Add a new request to this <see cref="Batch"/>
        /// </summary>
        /// <param name="model">Entity object on for which this request was meant</param>
        /// <param name="entityInfo">Info about the entity object</param>
        /// <param name="method">Type of http method (GET/PATCH/POST/...)</param>
        /// <param name="apiCall">Rest/Graph call</param>
        /// <param name="backupApiCall">Backup rest api call, will be used in case we encounter a mixed batch</param>
        /// <param name="fromJsonCasting">Delegate for json type parsing</param>
        /// <param name="postMappingJson">Delegate for post mapping</param>
        /// <param name="operationName">Name of the operation, used for telemetry purposes</param>
        /// <returns>The id to created batch request</returns>
        internal Guid Add(TransientObject model, EntityInfo entityInfo, HttpMethod method, ApiCall apiCall, ApiCall backupApiCall, Func <FromJson, object> fromJsonCasting, Action <string> postMappingJson, string operationName)
        {
            var lastAddedRequest = GetLastRequest();
            int order            = 0;

            if (lastAddedRequest != null)
            {
                order = lastAddedRequest.Order + 1;
            }

            var batchRequest = new BatchRequest(model, entityInfo, method, apiCall, backupApiCall, fromJsonCasting, postMappingJson, operationName, order);

            Requests.Add(order, batchRequest);

            return(batchRequest.Id);
        }
Beispiel #4
0
        /// <summary>
        /// Add a new request to this <see cref="Batch"/>
        /// </summary>
        /// <param name="model">Entity object on for which this request was meant</param>
        /// <param name="entityInfo">Info about the entity object</param>
        /// <param name="method">Type of http method (GET/PATCH/POST/...)</param>
        /// <param name="apiCall">Rest/Graph call</param>
        /// <param name="backupApiCall">Backup rest api call, will be used in case we encounter a mixed batch</param>
        /// <param name="fromJsonCasting">Delegate for json type parsing</param>
        /// <param name="postMappingJson">Delegate for post mapping</param>
        /// <param name="operationName">Name of the operation, used for telemetry purposes</param>
        /// <returns>The id to created batch request</returns>
        internal Guid Add(TransientObject model, EntityInfo entityInfo, HttpMethod method, ApiCall apiCall, ApiCall backupApiCall, Func <FromJson, object> fromJsonCasting, Action <string> postMappingJson, string operationName)
        {
            // Copy the request modules list as it will get cleared at context level
            List <IRequestModule> requestModulesToUse = null;
            var requestModules = (model as IDataModelWithContext).PnPContext.RequestModules;

            if (requestModules != null)
            {
                requestModulesToUse = new List <IRequestModule>(requestModules);
            }

            return(Add(model, entityInfo, method, apiCall, backupApiCall, fromJsonCasting, postMappingJson, operationName, requestModulesToUse));
        }
Beispiel #5
0
 internal ApiResponse(ApiCall apiCall, JsonElement jsonElement, Guid batchRequestId)
 {
     ApiCall        = apiCall;
     JsonElement    = jsonElement;
     BatchRequestId = batchRequestId;
 }
Beispiel #6
0
        private static async Task <ApiCallRequest> BuildGetAPICallRestAsync <TModel>(BaseDataModel <TModel> model, EntityInfo entity, ODataQuery <TModel> oDataQuery, ApiCall apiOverride, bool useLinqGet, bool loadPages)
        {
            string getApi = useLinqGet ? entity.SharePointLinqGet : entity.SharePointGet;

            IEnumerable <EntityFieldInfo> fields = entity.Fields.Where(p => p.Load);

            Dictionary <string, string> urlParameters = new Dictionary <string, string>();

            StringBuilder sb = new StringBuilder();

            // Only add select statement whenever there was a filter specified
            if (entity.SharePointFieldsLoadedViaExpression)
            {
                // $select
                foreach (var field in fields)
                {
                    // If there was a selection on which fields to include in an expand (via the QueryProperties() option) then add those fields
                    if (field.SharePointExpandable && field.ExpandFieldInfo != null)
                    {
                        AddExpandableSelectRest(sb, field, null, "");
                    }
                    else
                    {
                        sb.Append($"{JsonMappingHelper.GetRestField(field)},");
                    }
                }

                urlParameters.Add("$select", sb.ToString().TrimEnd(new char[] { ',' }));
                sb.Clear();
            }

            // $expand
            foreach (var field in fields.Where(p => p.SharePointExpandable))
            {
                if (entity.SharePointFieldsLoadedViaExpression)
                {
                    sb.Append($"{JsonMappingHelper.GetRestField(field)},");

                    // If there was a selection on which fields to include in an expand (via the Include() option) and the included field was expandable itself then add it
                    if (field.ExpandFieldInfo != null)
                    {
                        string path = "";
                        AddExpandableExpandRest(sb, field, null, path);
                    }
                }
                else
                {
                    if (field.ExpandableByDefault)
                    {
                        sb.Append($"{JsonMappingHelper.GetRestField(field)},");
                    }
                }
            }
            urlParameters.Add("$expand", sb.ToString().TrimEnd(new char[] { ',' }));

            oDataQuery.AddODataToUrlParameters(urlParameters, ODataTargetPlatform.SPORest);

            // REST apis do not apply a default top
            // In order to not receive all items in one request, we apply a default top
            // We don't change the original ODataQuery to avoid side effects
            if (useLinqGet && !urlParameters.ContainsKey(ODataQuery <TModel> .TopKey))
            {
                urlParameters.Add(ODataQuery <TModel> .TopKey, model.PnPContext.GlobalOptions.HttpSharePointRestDefaultPageSize.ToString());
            }

            sb.Clear();

            // Build the API call
            string baseApiCall = "";

            if (apiOverride.Equals(default(ApiCall)))
            {
                baseApiCall = $"{model.PnPContext.Uri.AbsoluteUri.TrimEnd(new char[] { '/' })}/{getApi}";
            }
            else
            {
                baseApiCall = $"{model.PnPContext.Uri.AbsoluteUri.TrimEnd(new char[] { '/' })}/{apiOverride.Request}";
            }

            // Parse tokens in the base api call
            baseApiCall = await ApiHelper.ParseApiCallAsync(model, baseApiCall).ConfigureAwait(false);

            sb.Append(baseApiCall);

            // Build the querystring parameters
            NameValueCollection queryString = HttpUtility.ParseQueryString(string.Empty);

            foreach (var urlParameter in urlParameters.Where(i => !string.IsNullOrEmpty(i.Value)))
            {
                // Add key and value, which will be automatically URL-encoded, if needed
                queryString.Add(urlParameter.Key, urlParameter.Value);
            }

            // Build the whole URL
            if (queryString.AllKeys.Length > 0)
            {
                // In .NET Framework to ToString() of a NameValueCollection will use HttpUtility.UrlEncodeUnicode under
                // the covers resulting in issues. So we decode and encode again as a workaround. This code produces the
                // same result when used under .NET5/Core versus .NET Framework
                sb.Append($"?{queryString.ToEncodedString()}");
            }

            // Create ApiCall instance and call the override option if needed
            var call = new ApiCallRequest(new ApiCall(sb.ToString(), ApiType.SPORest, loadPages: loadPages));

            if (model.GetApiCallOverrideHandler != null)
            {
                call = await model.GetApiCallOverrideHandler.Invoke(call).ConfigureAwait(false);
            }

            return(call);
        }
Beispiel #7
0
        internal static async Task <ApiCallRequest> BuildGetAPICallAsync <TModel>(BaseDataModel <TModel> model, EntityInfo entity, ODataQuery <TModel> oDataQuery, ApiCall apiOverride, bool forceSPORest = false, bool useLinqGet = false, bool loadPages = false)
        {
            // Can we use Microsoft Graph for this GET request?
            bool useGraph = model.PnPContext.GraphFirst && // See if Graph First is enabled/configured
                            !forceSPORest &&               // and if we are not forced to use SPO REST
                            entity.CanUseGraphGet;         // and if the entity supports GET via Graph

            // If entity cannot be surfaced with SharePoint Rest then force graph
            if (string.IsNullOrEmpty(entity.SharePointType))
            {
                useGraph = true;
            }
            // Else if we've overriden the query then simply take what was set in the query override
            else if (!apiOverride.Equals(default(ApiCall)))
            {
                useGraph = apiOverride.Type == ApiType.Graph;
            }
            else if (useGraph && useLinqGet)
            {
                // LINQ Get will based upon the defined LinqGet query + query arguments determined via entity and oDataQuery.
                // e.g. _api/web/lists or _api/web/webs
                // When there are no query arguments and when the model supports Graph (e.g. Web) then useGraph = true while
                // the queried collections (e.g. webs, lists) might not be decorated with a GraphType attribute

                if (string.IsNullOrEmpty(entity.GraphLinqGet) && !string.IsNullOrEmpty(entity.SharePointLinqGet))
                {
                    useGraph = false;
                }
            }

            if (useGraph)
            {
                return(await BuildGetAPICallGraphAsync(model, entity, oDataQuery, apiOverride, useLinqGet, loadPages).ConfigureAwait(false));
            }
            else
            {
                return(await BuildGetAPICallRestAsync(model, entity, oDataQuery, apiOverride, useLinqGet, loadPages).ConfigureAwait(false));
            }
        }
Beispiel #8
0
        private static async Task <ApiCallRequest> BuildGetAPICallGraphAsync <TModel>(BaseDataModel <TModel> model, EntityInfo entity, ODataQuery <TModel> oDataQuery, ApiCall apiOverride, bool useLinqGet, bool loadPages)
        {
            string getApi = useLinqGet ? entity.GraphLinqGet : entity.GraphGet;

            ApiType apiType = ApiType.Graph;

            if (entity.GraphBeta)
            {
                if (CanUseGraphBeta(model, entity))
                {
                    apiType = ApiType.GraphBeta;
                }
                else
                {
                    // we can't make this request
                    var cancelledApiCallRequest = new ApiCallRequest(default);
Beispiel #9
0
 internal static Task <ApiCallRequest> BuildGetAPICallAsync <TModel>(BaseDataModel <TModel> model, EntityInfo entity, ApiCall apiOverride, bool forceSPORest = false, bool useLinqGet = false, bool loadPages = false)
 {
     return(BuildGetAPICallAsync(model, entity, new ODataQuery <TModel>(), apiOverride, forceSPORest, useLinqGet, loadPages));
 }
Beispiel #10
0
        internal static async Task <ApiCallRequest> BuildGetAPICallAsync <TModel>(BaseDataModel <TModel> model, EntityInfo entity, ODataQuery <TModel> oDataQuery, ApiCall apiOverride, bool forceSPORest = false, bool useLinqGet = false, bool loadPages = false)
        {
            // Can we use Microsoft Graph for this GET request?
            bool useGraph = model.PnPContext.GraphFirst && // See if Graph First is enabled/configured
                            !forceSPORest &&               // and if we are not forced to use SPO REST
                            entity.CanUseGraphGet;         // and if the entity supports GET via Graph

            // If entity cannot be surfaced with SharePoint Rest then force graph
            if (string.IsNullOrEmpty(entity.SharePointType))
            {
                useGraph = true;
            }
            // Else if we've overriden the query then simply take what was set in the query override
            else if (!apiOverride.Equals(default(ApiCall)))
            {
                useGraph = apiOverride.Type == ApiType.Graph;
            }

            if (useGraph)
            {
                return(await BuildGetAPICallGraphAsync(model, entity, oDataQuery, apiOverride, useLinqGet, loadPages).ConfigureAwait(false));
            }
            else
            {
                return(await BuildGetAPICallRestAsync(model, entity, oDataQuery, apiOverride, useLinqGet, loadPages).ConfigureAwait(false));
            }
        }