/// <summary> /// Updates an existing attachment entry with the provided files. If the file already exists /// in the attachment entry, it will be replaced. Otherwise, a new attachment line is added. /// </summary> /// <param name="attachmentEntry"> /// The attachment entry ID to be updated. /// </param> /// <param name="files"> /// A Dictionary containing the files to be updated, where the file name is the Key and the file is the Value. /// </param> public async Task PatchAttachmentsAsync(int attachmentEntry, IDictionary <string, Stream> files) { await ExecuteRequest(async() => { if (files == null || files.Count == 0) { throw new ArgumentException("No files to be sent."); } var result = await ServiceLayerRoot .AppendPathSegment($"Attachments2({attachmentEntry})") .WithCookies(Cookies) .PatchMultipartAsync(mp => { // Removes double quotes from boundary, otherwise the request fails with error 405 Method Not Allowed var boundary = mp.Headers.ContentType.Parameters.First(o => o.Name.Equals("boundary", StringComparison.OrdinalIgnoreCase)); boundary.Value = boundary.Value.Replace("\"", string.Empty); foreach (var file in files) { var content = new StreamContent(file.Value); content.Headers.Add("Content-Disposition", $"form-data; name=\"files\"; filename=\"{file.Key}\""); content.Headers.Add("Content-Type", "application/octet-stream"); mp.Add(content); } }); return(result); }); }
/// <summary> /// Performs a POST Logout request, ending the current session. /// </summary> public async Task LogoutAsync() { if (Cookies == null) { return; } try { await ServiceLayerRoot.AppendPathSegment("Logout").WithCookies(Cookies).PostAsync(); _loginResponse = new SLLoginResponse(); _lastRequest = default; Cookies = null; } catch (FlurlHttpException ex) { try { if (ex.Call.HttpResponseMessage == null) { throw; } var response = await ex.GetResponseJsonAsync <SLResponseError>(); throw new SLException(response.Error.Message.Value, response.Error, ex); } catch { throw ex; } } }
/// <summary> /// Downloads the specified attachment file as a <see cref="byte"/> array. By default, the first attachment /// line is downloaded if there are multiple attachment lines in one attachment. /// </summary> /// <param name="attachmentEntry"> /// The attachment entry ID to be downloaded. /// </param> /// <param name="fileName"> /// The file name of the attachment to be downloaded (including the file extension). Only required if /// you want to download an attachment line other than the first attachment line. /// </param> /// <returns> /// The downloaded attachment file as a <see cref="byte"/> array. /// </returns> public async Task <byte[]> GetAttachmentAsBytesAsync(int attachmentEntry, string fileName = null) { return(await ExecuteRequest(async() => { var file = await ServiceLayerRoot .AppendPathSegment($"Attachments2({attachmentEntry})/$value") .SetQueryParam("filename", !string.IsNullOrEmpty(fileName) ? $"'{fileName}'" : null) .WithCookies(Cookies) .GetBytesAsync(); return file; })); }
/// <summary> /// Performs the POST Login request to the Service Layer. /// </summary> /// <param name="forceLogin"> /// Whether the login request should be forced even if the current session has not expired. /// </param> /// <param name="expectReturn"> /// Wheter the login information should be returned. /// </param> private async Task <SLLoginResponse> ExecuteLoginAsync(bool forceLogin = false, bool expectReturn = false) { // Prevents multiple login requests in a multi-threaded scenario await _semaphoreSlim.WaitAsync(); try { if (forceLogin) { _lastRequest = default; } // Session still valid, no need to login again if (DateTime.Now.Subtract(_lastRequest).TotalMinutes < _loginResponse.SessionTimeout) { return(expectReturn ? LoginResponse : null); } var loginResponse = await ServiceLayerRoot .AppendPathSegment("Login") .WithCookies(out var cookieJar) .PostJsonAsync(new { CompanyDB, UserName, Password, Language }) .ReceiveJson <SLLoginResponse>(); Cookies = cookieJar; _loginResponse = loginResponse; _loginResponse.LastLogin = DateTime.Now; return(expectReturn ? LoginResponse : null); } catch (FlurlHttpException ex) { try { if (ex.Call.HttpResponseMessage == null) { throw; } var response = await ex.GetResponseJsonAsync <SLResponseError>(); throw new SLException(response.Error.Message.Value, response.Error, ex); } catch { throw ex; } } finally { _semaphoreSlim.Release(); } }
/// <summary> /// Performs a batch request (multiple operations sent in a single HTTP request) with the provided <see cref="SLBatchRequest"/> collection. /// </summary> /// <remarks> /// See section 'Batch Operations' in the Service Layer User Manual for more details. /// </remarks> /// <param name="requests"> /// A collection of <see cref="SLBatchRequest"/> to be sent in the batch.</param> /// <param name="singleChangeSet"> /// Whether all the requests in this batch should be sent in a single change set. This means that any unsuccessful request will cause the whole batch to be rolled back. /// </param> /// <returns> /// An <see cref="HttpResponseMessage"/> array containg the response messages of the batch request. /// </returns> public async Task <HttpResponseMessage[]> PostBatchAsync(IEnumerable <SLBatchRequest> requests, bool singleChangeSet = true) { // Adds required "msgtype" parameter to the HttpContent in order for the ReadAsHttpResponseMessageAsync method to work as expected void AddMsgTypeToHttpContent(HttpContent httpContent) { if (httpContent.Headers.ContentType.MediaType.Equals("application/http", StringComparison.OrdinalIgnoreCase) && !httpContent.Headers.ContentType.Parameters.Any(p => p.Name.Equals("msgtype", StringComparison.OrdinalIgnoreCase) && p.Value.Equals("response", StringComparison.OrdinalIgnoreCase))) { httpContent.Headers.ContentType.Parameters.Add(new NameValueHeaderValue("msgtype", "response")); } } return(await ExecuteRequest(async() => { if (requests == null || requests.Count() == 0) { throw new ArgumentException("No requests to be sent."); } var response = singleChangeSet ? await ServiceLayerRoot .AppendPathSegment("$batch") .WithCookies(Cookies) .PostMultipartAsync(mp => { mp.Headers.ContentType.MediaType = "multipart/mixed"; mp.Add(BuildMixedMultipartContent(requests)); }) : await ServiceLayerRoot .AppendPathSegment("$batch") .WithCookies(Cookies) .PostMultipartAsync(mp => { mp.Headers.ContentType.MediaType = "multipart/mixed"; foreach (var request in requests) { string boundary = "changeset_" + Guid.NewGuid(); mp.Add(BuildMixedMultipartContent(request, boundary)); } }); var responseList = new List <HttpResponseMessage>(); var multipart = await response.ResponseMessage.Content.ReadAsMultipartAsync(); foreach (HttpContent httpContent in multipart.Contents) { if (httpContent.Headers.ContentType.MediaType.Equals("application/http", StringComparison.OrdinalIgnoreCase)) { AddMsgTypeToHttpContent(httpContent); var innerResponse = await httpContent.ReadAsHttpResponseMessageAsync(); responseList.Add(innerResponse); } else if (httpContent.Headers.ContentType.MediaType.Equals("multipart/mixed", StringComparison.OrdinalIgnoreCase)) { var innerMultipart = await httpContent.ReadAsMultipartAsync(); foreach (HttpContent innerHttpContent in innerMultipart.Contents) { AddMsgTypeToHttpContent(innerHttpContent); var innerResponse = await innerHttpContent.ReadAsHttpResponseMessageAsync(); responseList.Add(innerResponse); } } } return responseList.ToArray(); })); }
/// <summary> /// Initializes a new instance of the <see cref="SLRequest"/> class that represents a request to the associated <see cref="SLConnection"/>. /// </summary> /// <remarks> /// The request can be configured using the extension methods provided in <see cref="SLRequestExtensions"/>. /// </remarks> /// <param name="resource"> /// The resource name to be requested. /// </param> /// <param name="id"> /// The entity ID to be requested. /// </param> public SLRequest Request(string resource, object id) { return(new SLRequest(this, new FlurlRequest(ServiceLayerRoot.AppendPathSegment(id is string?$"{resource}('{id}')" : $"{resource}({id})"))));; }
/// <summary> /// Initializes a new instance of the <see cref="SLRequest"/> class that represents a request to the associated <see cref="SLConnection"/>. /// </summary> /// <remarks> /// The request can be configured using the extension methods provided in <see cref="SLRequestExtensions"/>. /// </remarks> /// <param name="resource"> /// The resource name to be requested. /// </param> public SLRequest Request(string resource) { return(new SLRequest(this, new FlurlRequest(ServiceLayerRoot.AppendPathSegment(resource)))); }