Example #1
0
        /// <summary>
        /// Submits an item binary via the Records365 vNext Connector API.
        /// </summary>
        /// <param name="submitContext"></param>
        /// <returns></returns>
        public override async Task Submit(SubmitContext submitContext)
        {
            var binarySubmitContext = submitContext as BinarySubmitContext;

            ValidateFields(binarySubmitContext);

            // Submit via HTTP API Client that is generated with AutoRest
            var apiClient = ApiClientFactory.CreateApiClient(submitContext.ApiClientFactorySettings);
            var result    = await GetRetryPolicy(submitContext).ExecuteAsync(
                async(ct) =>
            {
                // In case a stream is reused during submission retry, it might not be in 0 Position
                // since the previous submission already read to the end. We should reset this value.
                if (binarySubmitContext.Stream.CanSeek)
                {
                    binarySubmitContext.Stream.Position = 0;
                }

                var authHelper = ApiClientFactory.CreateAuthenticationHelper();
                var headers    = await authHelper.GetHttpRequestHeaders(submitContext.AuthenticationHelperSettings).ConfigureAwait(false);
                return(await apiClient.ApiBinariesPostWithHttpMessagesAndStreamAsync(
                           binarySubmitContext.ConnectorConfigId.ToString(),
                           binarySubmitContext.ItemExternalId,
                           binarySubmitContext.ExternalId,
                           binarySubmitContext.FileName,
                           binarySubmitContext.CorrelationId.ToString(),
                           inputStream: binarySubmitContext.Stream,
                           customHeaders: headers,
                           cancellationToken: ct
                           ).ConfigureAwait(false));
            },
                binarySubmitContext.CancellationToken
                ).ConfigureAwait(false);

            var shouldContinue = true;

            shouldContinue = await HandleSubmitResponse(submitContext, result, "Binary").ConfigureAwait(false);

            if (shouldContinue)
            {
                await InvokeNext(submitContext).ConfigureAwait(false);
            }
        }
 /// <summary>
 /// Invokes the next element in the submission pipeline, if one exists.
 /// </summary>
 /// <param name="submitContext"></param>
 /// <returns></returns>
 protected async Task InvokeNext(SubmitContext submitContext)
 {
     if (_next != null)
     {
         // Note the timing here may not be the most intuitive.
         // The timing for any given element will be the sum of all processing in the remaining chain.
         // Be aware of this when analysing the metrics.
         using (var perfEvent = CreatePerformanceEvent(submitContext, nameof(Submit)))
         {
             try
             {
                 submitContext.CancellationToken.ThrowIfCancellationRequested();
                 await _next.Submit(submitContext).ConfigureAwait(false);
             }
             catch (Exception ex)
             {
                 perfEvent?.Exception(ex);
                 throw;
             }
         }
     }
 }
        /// <summary>
        /// Submits an aggregation to the Records365 vNext Connector API.
        /// </summary>
        /// <param name="submitContext"></param>
        /// <returns></returns>
        public async override Task Submit(SubmitContext submitContext)
        {
            // Submit via HTTP API Client that is generated with AutoRest
            var apiClient = ApiClientFactory.CreateApiClient(submitContext.ApiClientFactorySettings);

            Func <string, DateTime> parseDateTime = (string value) =>
            {
                DateTime result;
                return(!string.IsNullOrEmpty(value) && DateTime.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out result) ? result : DateTime.UtcNow);
            };

            var aggregationModel = new AggregationSubmissionInputModel()
            {
                ItemTypeId                = submitContext.ItemTypeId,
                ExternalId                = submitContext.CoreMetaData?.FirstOrDefault(metadata => metadata.Name == Fields.ExternalId)?.Value ?? "",
                ParentExternalId          = submitContext.CoreMetaData?.FirstOrDefault(metadata => metadata.Name == Fields.ParentExternalId)?.Value ?? "",
                Title                     = submitContext.CoreMetaData?.FirstOrDefault(metadata => metadata.Name == Fields.Title)?.Value ?? "",
                Author                    = submitContext.CoreMetaData?.FirstOrDefault(metadata => metadata.Name == Fields.Author)?.Value ?? "",
                SourceLastModifiedDate    = parseDateTime(submitContext.CoreMetaData?.FirstOrDefault(metadata => metadata.Name == Fields.SourceLastModifiedDate)?.Value),
                SourceCreatedDate         = parseDateTime(submitContext.CoreMetaData?.FirstOrDefault(metadata => metadata.Name == Fields.SourceCreatedDate)?.Value),
                SourceLastModifiedBy      = submitContext.CoreMetaData?.FirstOrDefault(metadata => metadata.Name == Fields.SourceLastModifiedBy)?.Value ?? "",
                SourceCreatedBy           = submitContext.CoreMetaData?.FirstOrDefault(metadata => metadata.Name == Fields.SourceCreatedBy)?.Value ?? "",
                ConnectorId               = submitContext.ConnectorConfigId.ToString(),
                Location                  = submitContext.CoreMetaData?.FirstOrDefault(metadata => metadata.Name == Fields.Location)?.Value ?? "",
                MediaType                 = submitContext.CoreMetaData?.FirstOrDefault(metadata => metadata.Name == Fields.MediaType)?.Value ?? "Electronic",
                BarcodeType               = submitContext.CoreMetaData?.FirstOrDefault(metadata => metadata.Name == Fields.BarcodeType)?.Value ?? "",
                BarcodeValue              = submitContext.CoreMetaData?.FirstOrDefault(metadata => metadata.Name == Fields.BarcodeValue)?.Value ?? "",
                RecordCategoryId          = submitContext.CoreMetaData?.FirstOrDefault(metadata => metadata.Name == Fields.RecordCategoryID)?.Value ?? "",
                SecurityProfileIdentifier = submitContext.CoreMetaData?.FirstOrDefault(metadata => metadata.Name == Fields.SecurityProfileIdentifier)?.Value ?? "",
                SourceProperties          = new List <SubmissionMetaDataModel>(),
                Relationships             = new List <RelationshipDataModel>()
            };

            if (submitContext.SourceMetaData != null)
            {
                aggregationModel.SourceProperties = submitContext.SourceMetaData;
            }

            if (submitContext.Relationships != null)
            {
                aggregationModel.Relationships = submitContext.Relationships;
            }

            var shouldContinue = true;

            try
            {
                var result = await GetRetryPolicy(submitContext).ExecuteAsync(
                    async(ct) =>
                {
                    var authHelper = ApiClientFactory.CreateAuthenticationHelper();
                    var headers    = await authHelper.GetHttpRequestHeaders(submitContext.AuthenticationHelperSettings).ConfigureAwait(false);
                    return(await apiClient.ApiAggregationsPostWithHttpMessagesAsync(
                               aggregationModel,
                               customHeaders: headers,
                               cancellationToken: ct
                               ).ConfigureAwait(false));
                },
                    submitContext.CancellationToken
                    ).ConfigureAwait(false);

                shouldContinue = await HandleSubmitResponse(submitContext, result, "Aggregation").ConfigureAwait(false);
            }
            catch (HttpOperationException ex)
                when(ex.Response?.StatusCode == System.Net.HttpStatusCode.Conflict)
                {
                    // submitted item already exists!  Nothing to do but continue with the submission pipeline
                    LogVerbose(submitContext, nameof(Submit), $"Submission returned {ex.Response.StatusCode} : Aggregation already submitted.");
                }

            if (shouldContinue)
            {
                await InvokeNext(submitContext).ConfigureAwait(false);
            }
        }
 private IPerformanceEvent CreatePerformanceEvent(SubmitContext submitContext, string methodName)
 {
     return(new PerformanceEvent(_next.GetType(), methodName, submitContext.LogPrefix(), Log));
 }
 /// <summary>
 /// Logs a warning message, providing information from the SubmitContext.
 /// </summary>
 /// <param name="context"></param>
 /// <param name="methodName"></param>
 /// <param name="message"></param>
 protected void LogWarning(SubmitContext context, string methodName, string message)
 {
     Log?.LogWarning(GetType(), methodName, $"{context.LogPrefix()} {message}");
 }
 /// <summary>
 /// Implement in a derived class to provide custom submit pipeline functionality.
 /// </summary>
 /// <param name="submitContext"></param>
 /// <returns></returns>
 public abstract Task Submit(SubmitContext submitContext);
        /// <summary>
        ///
        /// </summary>
        /// <param name="submitContext"></param>
        /// <returns></returns>
        public override async Task Submit(SubmitContext submitContext)
        {
            var binarySubmitContext = submitContext as BinarySubmitContext;

            ValidateFields(binarySubmitContext);

            if (!CircuitProvider.IsCircuitClosed(out _))
            {
                submitContext.SubmitResult.SubmitStatus = SubmitResult.Status.Deferred;
                return;
            }

            //Create the DirectBinarySubmissionInputModel needed for the SaS token call
            var binarySubmissionInputModel = new DirectBinarySubmissionInputModel(
                binarySubmitContext.ConnectorConfigId.ToString(),
                binarySubmitContext.ItemExternalId,
                binarySubmitContext.ExternalId,
                sourceLastModifiedDate: binarySubmitContext?.SourceLastModifiedDate,
                fileSize: binarySubmitContext.Stream.Length,
                fileName: binarySubmitContext.FileName,
                fileHash: binarySubmitContext.FileHash,
                mimeType: binarySubmitContext.MimeType ?? binarySubmitContext.SourceMetaData?.FirstOrDefault(metaInfo => metaInfo.Name == Fields.MimeType)?.Value,
                correlationId: binarySubmitContext.CorrelationId.ToString()
                );

            // Get token and URL via Autorest-generated API call
            var apiClient   = ApiClientFactory.CreateApiClient(submitContext.ApiClientFactorySettings);
            var retryPolicy = GetRetryPolicy(binarySubmitContext);

            var result = await retryPolicy.ExecuteAsync(
                async (ct) =>
            {
                var authHelper = ApiClientFactory.CreateAuthenticationHelper();
                var headers    = await authHelper.GetHttpRequestHeaders(submitContext.AuthenticationHelperSettings).ConfigureAwait(false);
                return(await apiClient.ApiBinariesGetSASTokenPostWithHttpMessagesAsync(
                           binarySubmissionInputModel: binarySubmissionInputModel,
                           customHeaders: headers,
                           cancellationToken: ct
                           ).ConfigureAwait(false));
            },
                submitContext.CancellationToken
                ).ConfigureAwait(false);

            if (result.Response.StatusCode == HttpStatusCode.MethodNotAllowed)
            {
                // Direct binary submission is disabled, fall back to old submission method
                await base.Submit(submitContext).ConfigureAwait(false);

                return;
            }

            if (await HandleSubmitResponse(submitContext, result, "Binary").ConfigureAwait(false))
            {
                var response = result.Body as DirectBinarySubmissionResponseModel;

                if (binarySubmitContext.Stream.CanSeek && binarySubmitContext.Stream.Length > response.MaxFileSize)
                {
                    // We want to skip submission if the binary is too large. The CanSeek is to prevent NotSupportedException if
                    // we attempt to get the length of an unseekable stream.
                    // If we cannot seek the stream, we assume it's under the maxFileSize and allow submission.
                    submitContext.SubmitResult.SubmitStatus = SubmitResult.Status.Skipped;
                    return;
                }

                if (!binarySubmitContext.Stream.CanSeek)
                {
                    //TODO: Log that submission is was allowed to proceed since size could not be determined.
                }

                // Retrieve reference to a blob. Use the DefaultBlobFactory if the BlobFactory on the pipeline element has not been set
                var blockBlob = BlobFactory != null?BlobFactory(response.Url) : DefaultBlobFactory(response.Url);

                // Set Blob ContentType
                blockBlob.Properties.ContentType = "application/octet-stream";

                // If catch TooManyRequestsException, make it return a TooManyRequests Status
                try
                {
                    await RetryProvider.ExecuteWithRetry(
                        blockBlob.ServiceClient,
                        //Upload to blob
                        async() =>
                    {
                        await blockBlob.UploadFromStreamAsync(binarySubmitContext.Stream, binarySubmitContext.CancellationToken).ConfigureAwait(false);
                    },
                        GetType(),
                        nameof(Submit)).ConfigureAwait(false);
                }
                catch (TooManyRequestsException ex)
                {
                    submitContext.SubmitResult.SubmitStatus  = SubmitResult.Status.TooManyRequests;
                    submitContext.SubmitResult.WaitUntilTime = ex.WaitUntilTime;
                    return;
                }

                if (!string.IsNullOrWhiteSpace(binarySubmitContext.FileName))
                {
                    blockBlob.Metadata[MetaDataKeys.ItemBinary_FileName]      = EscapeBlobMetaDataValue(binarySubmitContext.FileName);
                    blockBlob.Metadata[MetaDataKeys.ItemBinary_CorrelationId] = EscapeBlobMetaDataValue(binarySubmitContext.CorrelationId.ToString());

                    // If catch TooManyRequestsException, make it return a TooManyRequests Status
                    try
                    {
                        await RetryProvider.ExecuteWithRetry(
                            blockBlob.ServiceClient,
                            async() =>
                        {
                            await blockBlob.SetMetadataAsync(binarySubmitContext.CancellationToken).ConfigureAwait(false);
                        },
                            GetType(),
                            nameof(Submit)).ConfigureAwait(false);
                    }
                    catch (TooManyRequestsException ex)
                    {
                        submitContext.SubmitResult.SubmitStatus  = SubmitResult.Status.TooManyRequests;
                        submitContext.SubmitResult.WaitUntilTime = ex.WaitUntilTime;
                        return;
                    }
                }

                var notifyResult = await retryPolicy.ExecuteAsync(
                    async (ct) =>
                {
                    //If the item corresponding to the submitted binary is not yet present, the platform will have to handle this.
                    var authHelper = ApiClientFactory.CreateAuthenticationHelper();
                    var headers    = await authHelper.GetHttpRequestHeaders(submitContext.AuthenticationHelperSettings).ConfigureAwait(false);
                    return(await apiClient.ApiBinariesNotifyBinarySubmissionPostWithHttpMessagesAsync(
                               binarySubmissionInputModel: binarySubmissionInputModel,
                               customHeaders: headers,
                               cancellationToken: ct
                               ).ConfigureAwait(false));
                },
                    submitContext.CancellationToken
                    ).ConfigureAwait(false);

                if (notifyResult.Response.StatusCode != HttpStatusCode.OK)
                {
                    var notificationStatusCode = "<No Status Code>";
                    if (result.Response != null)
                    {
                        notificationStatusCode = result.Response.StatusCode.ToString();
                    }
                    // An issue with notification occurred, so we must throw
                    throw new HttpOperationException(submitContext.LogPrefix() +
                                                     $"Submission returned {notificationStatusCode} : Notification of binary submission failed.");
                }
            }
            else
            {
                return;
            }

            await InvokeNext(submitContext).ConfigureAwait(false);
        }
Example #8
0
        /// <summary>
        /// Handles the result of a call to a submission API.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="submitContext">The context of the submission.</param>
        /// <param name="result">The result of the submit API call.</param>
        /// <param name="itemTypeName">A string that identifies the item type (e.g., "Aggregation"). Used only for constructing log strings.</param>
        /// <returns>True if the submit pipeline should continue as a result of successful submission, false otherwise.</returns>
        protected async Task <bool> HandleSubmitResponse <T>(SubmitContext submitContext, HttpOperationResponse <T> result, string itemTypeName)
        {
            var shouldContinueSubmitPipeline = true;

            if (result == null)
            {
                throw new ArgumentNullException($"{submitContext.LogPrefix()}Invalid {itemTypeName} submission! Expecting a return type of {nameof(HttpOperationResponse<T>)} but got null!");
            }

            if (result.Response == null)
            {
                throw new HttpOperationException($"{submitContext.LogPrefix()}Invalid {itemTypeName} submission! Expecting a return type of {nameof(HttpResponseMessage)} but got null!");
            }

            // Default the SubmitStatus to OK first in case any developer reuse the same SubmitContext
            // Then all the following SubmitContext will have the previous result
            submitContext.SubmitResult.SubmitStatus = SubmitResult.Status.OK;

            switch (result.Response.StatusCode)
            {
            case System.Net.HttpStatusCode.Conflict:
                // shouldContinueSubmitPipeline should still be true on Conflict.
                // There may have been previous submissions of that item that failed at a later stage and need to be re-tried.
                LogVerbose(submitContext, nameof(HandleSubmitResponse), $"Submission returned {result.Response.StatusCode} : {itemTypeName} already submitted.");
                break;

            case System.Net.HttpStatusCode.OK:
                LogVerbose(submitContext, nameof(HandleSubmitResponse), $"Submission returned {result.Response.StatusCode} : {itemTypeName} submitted.");
                break;

            case System.Net.HttpStatusCode.Created:
                LogVerbose(submitContext, nameof(HandleSubmitResponse), $"Submission returned {result.Response.StatusCode} : {itemTypeName} submitted.");
                break;

            case System.Net.HttpStatusCode.Accepted:
                LogVerbose(submitContext, nameof(HandleSubmitResponse), $"Submission returned {result.Response.StatusCode} : {itemTypeName} accepted for submission.");
                break;

            case System.Net.HttpStatusCode.NoContent:
                LogVerbose(submitContext, nameof(HandleSubmitResponse), $"Submission returned {result.Response.StatusCode} : {itemTypeName} accepted for submission.");
                break;

            case System.Net.HttpStatusCode.BadRequest:
            {
                shouldContinueSubmitPipeline = false;

                // BadRequest (400) is returned in one of three scenarios:
                // - The submit was invalid (e.g. missing required field)
                // - the connector was disabled
                // - we're submitting a binary but binary protection is disabled for this connector
                var errorResponse = result.Body as ErrorResponseModel;
                if (errorResponse?.Error?.MessageCode == MessageCode.ConnectorNotEnabled)
                {
                    // Connector was disabled
                    LogWarning(submitContext, nameof(HandleSubmitResponse), $"Submission returned {result.Response.StatusCode} : {itemTypeName} NOT submitted because the connector was disabled.");
                    submitContext.SubmitResult.SubmitStatus = SubmitResult.Status.ConnectorDisabled;
                }
                else if (errorResponse?.Error?.MessageCode == MessageCode.ProtectionNotEnabled)
                {
                    // Protection is disabled
                    LogWarning(submitContext, nameof(HandleSubmitResponse), $"Binary submission returned {result.Response.StatusCode} : {itemTypeName} NOT submitted because the connector does not have protection enabled.");
                    submitContext.SubmitResult.SubmitStatus = SubmitResult.Status.Skipped;
                }
                else
                {
                    // Bad request
                    LogWarning(submitContext, nameof(HandleSubmitResponse),
                               $"Submission returned {result.Response.StatusCode} : {itemTypeName} NOT submitted because the submit was invalid. " +
                               $"Error Message: {errorResponse?.Error?.Message ?? "<null>"} " +
                               $"MessageCode: {errorResponse?.Error?.MessageCode ?? "<null>"} ");

                    // On an invalid submission, throw an exception to encourage correct dead lettering.
                    // The rest of the pipeline won't execute.
                    var httpContent = "<null>";
                    if (result.Response != null && result.Response.Content != null)
                    {
                        try
                        {
                            httpContent = await result.Response.Content.ReadAsStringAsync().ConfigureAwait(false);
                        }
                        catch { }
                    }
                    throw new HttpOperationException(submitContext.LogPrefix() +
                                                     $"Submission returned {result.Response.StatusCode} : {itemTypeName} NOT submitted because the submit was invalid." +
                                                     $"Http Content: {httpContent}");
                }
                break;
            }

            case System.Net.HttpStatusCode.Forbidden:
                shouldContinueSubmitPipeline = false;

                LogWarning(submitContext, nameof(HandleSubmitResponse), $"Submission returned {result.Response.StatusCode} : {itemTypeName} NOT submitted because the connector was not found.");
                submitContext.SubmitResult.SubmitStatus = SubmitResult.Status.ConnectorNotFound;
                break;

            case System.Net.HttpStatusCode.PreconditionFailed:
                shouldContinueSubmitPipeline = false;

                LogVerbose(submitContext, nameof(HandleSubmitResponse), $"Submission returned {result.Response.StatusCode} : {itemTypeName} NOT submitted because a precondition failed.");
                submitContext.SubmitResult.SubmitStatus = SubmitResult.Status.Deferred;
                break;

            case (System.Net.HttpStatusCode) 429:
                shouldContinueSubmitPipeline = false;

                if (result.Response.Headers.TryGetValues(waitUntilTime, out var values))
                {
                    var timeValue = values?.FirstOrDefault();
                    if (!string.IsNullOrEmpty(timeValue) && DateTime.TryParse(timeValue, out var time))
                    {
                        submitContext.SubmitResult.WaitUntilTime = time.ToUniversalTime();
                    }
                }

                if (submitContext.SubmitResult.WaitUntilTime == null)
                {
                    submitContext.SubmitResult.WaitUntilTime = DateTime.UtcNow.AddSeconds(defaultWaitTimeSeconds);
                }

                LogVerbose(submitContext, nameof(HandleSubmitResponse), $"Submission returned {result.Response.StatusCode} : {itemTypeName} NOT submitted because a part of the system is experiencing heavy load. " +
                           $"Try again after {submitContext.SubmitResult.WaitUntilTime}.");
                submitContext.SubmitResult.SubmitStatus = SubmitResult.Status.TooManyRequests;
                break;

            default:
                shouldContinueSubmitPipeline = false;

                LogWarning(submitContext, nameof(HandleSubmitResponse),
                           $"Submission returned {result.Response.StatusCode} : {itemTypeName} NOT submitted.");

                throw new HttpOperationException(submitContext.LogPrefix() +
                                                 $"Submission returned {result.Response.StatusCode} : NOT submitted. " +
                                                 $"Http Content: {await result.Response.Content.ReadAsStringAsync().ConfigureAwait(false)}");
            }

            return(shouldContinueSubmitPipeline);
        }
Example #9
0
 /// <summary>
 /// Gets a retry policy which can be used for communicating with Records365
 /// </summary>
 /// <returns></returns>
 protected Policy GetRetryPolicy(SubmitContext submitContext, string methodName = nameof(Submit))
 {
     return(ApiClientRetryPolicy.GetPolicy(Log, GetType(), methodName, MaxRetryAttempts, submitContext.CancellationToken, submitContext.LogPrefix()));
 }