/// <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; }
/// <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()); } } } }
/// <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); }
/// <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)); }
internal ApiResponse(ApiCall apiCall, JsonElement jsonElement, Guid batchRequestId) { ApiCall = apiCall; JsonElement = jsonElement; BatchRequestId = batchRequestId; }
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); }
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)); } }
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);
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)); }
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)); } }