private async Task <(MemberDto, ImageDto?)> UploadUserImageMultipartContent(Guid targetUserId, Stream requestBody, byte[] rowVersion, string?contentType, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); var now = _systemClock.UtcNow.UtcDateTime; var defaultFormOptions = new FormOptions(); // Create a Collection of KeyValue Pairs. var formAccumulator = new KeyValueAccumulator(); // Determine the Multipart Boundary. var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(contentType), defaultFormOptions.MultipartBoundaryLengthLimit); var reader = new MultipartReader(boundary, requestBody); var section = await reader.ReadNextSectionAsync(cancellationToken); ImageDto? imageDto = null; MemberDto userDto = new MemberDto(); // Loop through each 'Section', starting with the current 'Section'. while (section != null) { // Check if the current 'Section' has a ContentDispositionHeader. var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition); if (hasContentDispositionHeader) { if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition)) { if (contentDisposition != null) { var sectionFileName = contentDisposition.FileName.Value; // use an encoded filename in case there is anything weird var encodedFileName = WebUtility.HtmlEncode(Path.GetFileName(sectionFileName)); // read the section filename to get the content type var fileExtension = Path.GetExtension(sectionFileName); // now make it unique var uniqueFileName = $"{Guid.NewGuid()}{fileExtension}"; if (!_acceptedFileTypes.Contains(fileExtension.ToLower())) { _logger.LogError("file extension:{0} is not an accepted image file", fileExtension); throw new ValidationException("Image", "The image is not in an accepted format"); } var compressedImage = _imageService.TransformImageForAvatar(section.Body); try { await _blobStorageProvider.UploadFileAsync(compressedImage.Image, uniqueFileName, MimeTypesMap.GetMimeType(encodedFileName), cancellationToken); } catch (Exception ex) { _logger.LogError(ex, "An error occurred uploading file to blob storage"); throw; } // trick to get the size without reading the stream in memory var size = section.Body.Position; imageDto = new ImageDto { FileSizeBytes = size, FileName = uniqueFileName, Height = compressedImage.Height, Width = compressedImage.Width, IsDeleted = false, MediaType = compressedImage.MediaType, CreatedBy = targetUserId, CreatedAtUtc = now }; var imageValidator = new ImageValidator(MaxFileSizeBytes); var imageValidationResult = await imageValidator.ValidateAsync(imageDto, cancellationToken); if (imageValidationResult.Errors.Count > 0) { await _blobStorageProvider.DeleteFileAsync(uniqueFileName); _logger.LogError("File size:{0} is greater than the max allowed size:{1}", size, MaxFileSizeBytes); throw new ValidationException(imageValidationResult); } } } else if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition)) { // if for some reason other form data is sent it would get processed here var key = HeaderUtilities.RemoveQuotes(contentDisposition?.Name.ToString().ToLowerInvariant()); var encoding = GetEncoding(section); using (var streamReader = new StreamReader(section.Body, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true)) { var value = await streamReader.ReadToEndAsync(); if (string.Equals(value, "undefined", StringComparison.OrdinalIgnoreCase)) { value = string.Empty; } formAccumulator.Append(key.Value, value); if (formAccumulator.ValueCount > defaultFormOptions.ValueCountLimit) { _logger.LogError("UserEdit: Form key count limit {0} exceeded.", defaultFormOptions.ValueCountLimit); throw new FormatException($"Form key count limit { defaultFormOptions.ValueCountLimit } exceeded."); } } } } // Begin reading the next 'Section' inside the 'Body' of the Request. section = await reader.ReadNextSectionAsync(cancellationToken); } if (formAccumulator.HasValues) { var formValues = formAccumulator.GetResults(); // Check if users been updated // Unable to place in controller due to disabling form value model binding var user = await _userCommand.GetMemberAsync(targetUserId, cancellationToken); if (!user.RowVersion.SequenceEqual(rowVersion)) { _logger.LogError($"Precondition Failed: UpdateUserAsync - User:{0} has changed prior to submission", targetUserId); throw new PreconditionFailedExeption("Precondition Failed: User has changed prior to submission"); } var firstNameFound = formValues.TryGetValue("firstName", out var firstName); if (firstNameFound is false || string.IsNullOrEmpty(firstName)) { throw new ArgumentNullException($"First name was not provided"); } formValues.TryGetValue("lastName", out var surname); var pronoundsFound = formValues.TryGetValue("pronouns", out var pronouns); if (pronoundsFound is false) { throw new ArgumentNullException($"Pronouns were not provided"); } formValues.TryGetValue("imageid", out var image); var imageId = Guid.TryParse(image, out var imageGuid) ? (Guid?)imageGuid : null; if (imageId.HasValue) { if (imageId == new Guid()) { throw new ArgumentOutOfRangeException($"Incorrect Id provided"); } } userDto = new MemberDto { Id = targetUserId, FirstName = firstName, Surname = surname, Pronouns = pronouns, ImageId = imageId, ModifiedAtUTC = now, ModifiedBy = targetUserId, }; } return(userDto, imageDto); }