public async Task <PhotoResponse.Photo> GetPhotoInfo(PhotoId photoId, CancellationToken ct)
        {
            var photoInfo = await _pictureFetchingService.GetPhotoInfo(photoId.flickrId, ct);

            ColoredConsole.WriteLine($"        > info for #{photoId.flickrId} fetched: {photoInfo.photo.dates.taken}", Colors.txtInfo);
            return(photoInfo.photo);
        }
        private async Task RemoveLikeRecord(UserId userId, PhotoId photoId)
        {
            logWriter.LogInformation($"{nameof(RemoveLikeRecord)}({nameof(userId)} = '{userId}', {nameof(photoId)} = '{photoId}')");

            var photo = await photoRepository.GetPhotoById(photoId, Guid.Empty);

            var newLikeCount = photo.LikeCount - 1;

            var deleteItemRequest = new DeleteItemRequest(
                tableName,
                Mappers.PhotoLike.ToDbKey(new PhotoLikeRecord
            {
                UserId      = userId,
                PhotoId     = photoId,
                CreatedTime = system.Time.UtcNow
            }));

            try
            {
                await dynamoDbCore.DeleteItem(deleteItemRequest);
                await UpdateLikeCount(photo, newLikeCount);
            }
            catch (Exception ex)
            {
                logWriter.LogError(ex, $"Error in {nameof(RemoveLikeRecord)}({nameof(userId)} = '{userId}', {nameof(photoId)} = '{photoId}'):\n{ex.ToString()}");
                throw;
            }
        }
        public async Task <Sizes> GetSizes(PhotoId photoId, CancellationToken ct)
        {
            var response = await _pictureFetchingService.GetSize(photoId.flickrId, ct);

            ColoredConsole.WriteLine($"        > sizes for #{photoId.flickrId} fetched: {response.sizes.size.FirstOrDefault()?.url}", Colors.txtInfo);
            return(response.sizes);
        }
        private async Task AddLikeRecord(UserId userId, PhotoId photoId)
        {
            logWriter.LogInformation($"{nameof(AddLikeRecord)}({nameof(userId)} = '{userId}', {nameof(photoId)} = '{photoId}')");

            var photo = await photoRepository.GetPhotoById(photoId, Guid.Empty);

            var newLikeCount = photo.LikeCount + 1;

            var putItemRequest = new PutItemRequest()
            {
                TableName = tableName,
                Item      = Mappers.PhotoLike.ToDbItem(new PhotoLikeRecord
                {
                    UserId      = userId,
                    PhotoId     = photoId,
                    CreatedTime = DateTimeOffset.UtcNow
                }),
            };

            try
            {
                await dynamoDbCore.PutItem(putItemRequest);
                await UpdateLikeCount(photo, newLikeCount);
            }
            catch (Exception ex)
            {
                logWriter.LogError(ex, $"Error in {nameof(AddLikeRecord)}({nameof(userId)} = '{userId}', {nameof(photoId)} = '{photoId}'):\n{ex.ToString()}");
                throw;
            }
        }
        public async Task SetPhotoState(UserId userId, PhotoId photoId, PhotoState photoState)
        {
            logWriter.LogInformation($"{nameof(SetPhotoState)}({nameof(userId)} = '{userId}', {nameof(photoId)} = '{photoId}', {nameof(photoState)} = '{photoState}'");

            var request = new UpdateItemRequest
            {
                TableName = tableName,
                Key       = new Dictionary <string, AttributeValue>
                {
                    { FieldMappings.PartitionKey, new AttributeValue(photoId.ToDbValue()) },
                    { FieldMappings.SortKey, new AttributeValue(photoId.ToDbValue()) }
                },
                UpdateExpression          = $"SET {FieldMappings.Photo.State} = :newstate",
                ExpressionAttributeValues = new Dictionary <string, AttributeValue>
                {
                    {
                        ":newstate", new AttributeValue
                        {
                            S = photoState.ToString()
                        }
                    }
                }
            };

            await dynamoDbCore.UpdateItem(request);
        }
Пример #6
0
        public override void ToStream(Stream output)
        {
            output.Write(TLUtils.SignatureToBytes(Signature));

            PhotoId.ToStream(output);
            Caption.ToStream(output);
        }
        public async Task DeletePhoto(PhotoId photoId)
        {
            logWriter.LogInformation($"{nameof(DeletePhoto)}({nameof(photoId)} = '{photoId}'");

            // Tirst we need to get the keys for all items to delete
            IEnumerable <Dictionary <string, AttributeValue> > itemKeys = await GetAllItemKeys(photoId);

            var request = new BatchWriteItemRequest
            {
                RequestItems = new Dictionary <string, List <WriteRequest> >
                {
                    {
                        tableName,
                        itemKeys.Distinct(new DbKeyEqualityComparer()).Select(key => new WriteRequest {
                            DeleteRequest = new DeleteRequest
                            {
                                Key = key
                            }
                        }).ToList()
                    }
                }
            };

            await dynamoDbCore.BatchWriteItem(request);
        }
Пример #8
0
 public override byte[] ToBytes()
 {
     return(TLUtils.Combine(
                TLUtils.SignatureToBytes(Signature),
                PhotoId.ToBytes(),
                Caption.ToBytes()));
 }
        public async Task SetPhotoText(UserId userId, PhotoId photoId, string text, IEnumerable <HashtagModel> hashtags)
        {
            logWriter.LogInformation($"{nameof(SetPhotoText)}({nameof(userId)} = '{userId}', {nameof(photoId)} = '{photoId}', {nameof(text)} = '{text}')");

            var existingItem = await GetPhotoById(photoId, userId);

            BatchWriteItemRequest writeItemsRequest = GetHashtagsRequests(text, hashtags, existingItem, HashtagUpdateMode.Update);

            var request = GetUpdateTextRequest(new PhotoModel
            {
                PhotoId  = photoId,
                UserId   = userId,
                RawText  = text,
                Hashtags = hashtags
            });

            logWriter.LogInformation($"UpdateItemRequest:\n{JsonConvert.SerializeObject(request)}");
            logWriter.LogInformation($"BatchWriteItemRequest:\n{JsonConvert.SerializeObject(writeItemsRequest)}");

            await dynamoDbCore.UpdateItem(request);

            if (writeItemsRequest != null)
            {
                await dynamoDbCore.BatchWriteItem(writeItemsRequest);
            }
        }
        public async Task AddPhotoComment(PhotoId photoId, UserId userId, string userName, string text)
        {
            logWriter.LogInformation($"{nameof(AddPhotoComment)}({nameof(photoId)} = '{photoId}', {nameof(userId)} = '{userId}', {nameof(userName)} = '{userName}', {nameof(text)} = '{text}')");

            if (string.IsNullOrWhiteSpace(text))
            {
                throw new Exception("Cannot add empty comments.");
            }

            PhotoComment comment = new PhotoComment
            {
                PhotoId     = photoId,
                UserId      = userId,
                UserName    = userName,
                Text        = text,
                CreatedTime = DateTimeOffset.UtcNow
            };

            try
            {
                await commentRepository.AddPhotoComment(comment);
            }
            catch (Exception ex)
            {
                logWriter.LogError(ex, $"{nameof(AddPhotoComment)}({nameof(photoId)} = '{photoId}', {nameof(userId)} = '{userId}', {nameof(userName)} = '{userName}', {nameof(text)} = '{text}'):\n{ex.ToString()}");
                throw;
            }
        }
Пример #11
0
        private async Task <string> ProcessImages(
            S3Event s3Event,
            IImageScaler imageScaler,
            ILogWriter <ImageResizeHandler> logger,
            IPhotosService photoService)
        {
            logger.LogInformation($"{nameof(ProcessImages)}");

            var s3Entity = s3Event.Records?[0].S3;

            if (s3Entity == null)
            {
                logger.LogCritical($"{nameof(s3Entity)} is null");
                return("NULL");
            }

            var     urlDecodedKey = System.Web.HttpUtility.UrlDecode(s3Entity.Object.Key);
            UserId  userId        = GetUserIdFromKey(urlDecodedKey);
            PhotoId photoId       = GetPhotoIdFromKey(urlDecodedKey);

            try
            {
                logger.LogInformation($"{nameof(urlDecodedKey)} is '{urlDecodedKey}'");



                await photoService.SetPhotoState(userId, photoId, PhotoState.ProcessingStarted);

                // generate filenames to use for the scaled images
                ImageKeys imageKeys = GetImageKeysWithoutExtension(urlDecodedKey);

                IEnumerable <Size> imageSizes;

                using (var s3InputObject = await S3Client.GetObjectAsync(s3Entity.Bucket.Name, urlDecodedKey))
                    using (var originalImageStream = new MemoryStream())
                    {
                        await ReadImageIntoStream(logger, originalImageStream, s3InputObject.ResponseStream);

                        imageSizes = await CreateScaledImages(imageScaler, logger, imageKeys, originalImageStream, GetTargetBucket());
                    }

                logger.LogInformation($"Updating photo data, making it available.");
                await UpdatePhotoData(photoService, photoId, imageSizes);

                // finish with deleting the upload file
                await DeleteOriginalSourceFile(logger, s3Entity, urlDecodedKey);

                logger.LogInformation($"Done with {urlDecodedKey}");
                return("OK");
            }
            catch (Exception ex)
            {
                logger.LogError(ex, $"Error when resizing {s3Entity.Object.Key} from bucket {s3Entity.Bucket.Name}:\n{ex.ToString()}");
                // set photo state to ProcessingFailed
                await photoService.SetPhotoState(userId, photoId, PhotoState.ProcessingFailed);

                throw;
            }
        }
 private IEnumerable <HashtagModel> GetHashtags(PhotoId photoId, string text, DateTimeOffset utcNow)
 {
     return(textSplitter.Split(text, TextPatterns.Hashtags)
            .Where(x => x.ItemType == TextItemType.HashTag)
            .Select(x => new HashtagModel {
         PhotoId = photoId, Hashtag = x.Text, CreatedTime = utcNow
     }));
 }
Пример #13
0
        private static async Task UpdatePhotoData(IPhotosService photoService, PhotoId photoId, IEnumerable <Size> imageSizes)
        {
            var photoFromDb = await photoService.GetPhoto(photoId, Guid.Empty);

            photoFromDb.State = PhotoState.PhotoAvailable;
            photoFromDb.Sizes = imageSizes;
            await photoService.UpdatePhoto(photoFromDb);
        }
        public Task SetLikeState(PhotoId photoId, UserId userId, bool like)
        {
            logWriter.LogInformation($"{nameof(SetLikeState)}({nameof(photoId)} = '{photoId}',  {nameof(userId)} = '{userId}', {nameof(like)} = '{like}')");

            return(like
                ? AddLikeRecord(userId, photoId)
                : RemoveLikeRecord(userId, photoId));
        }
Пример #15
0
 public PhotoLikeRecord FromDbItem(Dictionary <string, AttributeValue> input)
 {
     return(new PhotoLikeRecord
     {
         UserId = input.GetValue(FieldMappings.PhotoLike.UserId, value => UserId.FromDbValue(value)),
         PhotoId = input.GetValue(FieldMappings.PartitionKey, value => PhotoId.FromDbValue(value)),
         CreatedTime = input.GetDateTimeOffset(FieldMappings.PhotoLike.CreatedTime)
     });
 }
Пример #16
0
 public HashtagModel FromDbItem(Dictionary <string, AttributeValue> input)
 {
     return(new HashtagModel
     {
         Hashtag = input.GetValue(FieldMappings.Hashtag.HastagId, value => $"#{value.Split('|')[1]}"),
         PhotoId = input.GetValue(FieldMappings.Hashtag.PhotoId, value => PhotoId.FromDbValue(value)),
         CreatedTime = input.GetDateTimeOffset(FieldMappings.Hashtag.CreatedTime)
     });
 }
        private IEnumerable <PhotoWithCommentsAndLikes> ParsePhotosWithCommentsAndLikes(IOrderedEnumerable <Dictionary <string, AttributeValue> > sortedResult)
        {
            PhotoId latestPhotoId = Guid.Empty;
            List <PhotoWithCommentsAndLikes> result = new List <PhotoWithCommentsAndLikes>();

            PhotoWithCommentsAndLikes currentResult        = null;
            List <PhotoComment>       currentPhotoComments = null;
            List <PhotoLikeRecord>    currentPhotoLikes    = null;

            foreach (var item in sortedResult)
            {
                var    photoId    = PhotoId.FromDbValue(item[FieldMappings.PartitionKey].S);
                string recordType = item[FieldMappings.RecordType].S;


                if (latestPhotoId != photoId)
                {
                    if (currentResult != null)
                    {
                        result.Add(currentResult);
                    }

                    currentResult          = new PhotoWithCommentsAndLikes();
                    currentResult.Comments = (currentPhotoComments = new List <PhotoComment>());
                    currentResult.Likes    = (currentPhotoLikes = new List <PhotoLikeRecord>());

                    latestPhotoId = photoId;
                }

                switch (recordType.ToLowerInvariant())
                {
                case "photo":
                    currentResult.Photo = Mappers.PhotoModel.FromDbItem(item);
                    break;

                case "photocomment":
                    currentPhotoComments.Add(Mappers.PhotoComment.FromDbItem(item));
                    break;

                case "photolike":
                    currentPhotoLikes.Add(Mappers.PhotoLike.FromDbItem(item));
                    break;

                default:
                    logWriter.LogWarning($"RecordType '{recordType}' was not expected in {nameof(ParsePhotosWithCommentsAndLikes)}");
                    break;
                }
            }

            if (currentResult != null)
            {
                result.Add(currentResult);
            }

            return(result);
        }
        public async Task <string> SetPhotoText(PhotoId photoId, UserId userId, string text)
        {
            logWriter.LogInformation($"{nameof(SetPhotoText)}({nameof(photoId)} = '{photoId}', {nameof(userId)} = '{userId}', {nameof(text)} = '{text}')");
            var utcNow = DateTimeOffset.UtcNow;
            IEnumerable <HashtagModel> hashtags = GetHashtags(photoId, text, utcNow);

            await dataRepository.SetPhotoText(userId, photoId, text, hashtags);

            return(text.GetHtmlText(textSplitter));
        }
Пример #19
0
        public PhotoModel FromDbItem(Dictionary <string, AttributeValue> input)
        {
            PhotoId photoId = input.TryGetValue(FieldMappings.Photo.PhotoId, out var photoIdValue)
                    ? PhotoId.FromDbValue(photoIdValue.S)
                    : (PhotoId)Guid.Empty;

            var result = new PhotoModel
            {
                CreatedTime  = input.GetDateTimeOffset(FieldMappings.Photo.CreatedTime),
                PhotoId      = photoId,
                ObjectKey    = input.GetString(FieldMappings.Photo.ObjectKey),
                State        = input.GetValue(FieldMappings.Photo.State, value => (PhotoState)Enum.Parse(typeof(PhotoState), value)),
                UserId       = input.GetValue(FieldMappings.Photo.UserId, value => UserId.FromDbValue(value)),
                UserName     = input.GetString(FieldMappings.Photo.UserName),
                LikeCount    = input.GetInt32(FieldMappings.Photo.LikeCount),
                CommentCount = input.GetInt32(FieldMappings.Photo.CommentCount),
                Hashtags     = input.GetList(FieldMappings.Photo.Hashtags, value => new HashtagModel {
                    PhotoId = photoId, Hashtag = value
                })
            };

            if (input.TryGetValue(FieldMappings.Photo.RawText, out var rawCommentValue))
            {
                result.RawText = rawCommentValue.S;
            }

            if (input.TryGetValue(FieldMappings.Photo.Score, out var scoreValue))
            {
                result.Score = double.Parse(scoreValue.N);
            }

            if (input.TryGetValue(FieldMappings.Photo.Sizes, out var sizeValues))
            {
                result.Sizes = sizeValues.L.Select(value =>
                {
                    int width;
                    int height;

                    if (!value.M.TryGetValue("Width", out var widthValue) || !int.TryParse(widthValue.S, out width))
                    {
                        throw new Exception($"Failed to parse '{widthValue.S}' as a Size Width");
                    }

                    if (!value.M.TryGetValue("Height", out var heightValue) || !int.TryParse(heightValue.S, out height))
                    {
                        throw new Exception($"Failed to parse '{heightValue.S}' as a Size Height");
                    }

                    return(new Size(width, height));
                });
            }

            return(result);
        }
 public IObservable <SavePhotoModel> GetPhotoInfoForSave(PhotoId x)
 {
     return(Observable.FromAsync(async(ct) =>
     {
         var photoTask = _m.GetPhotoInfo(x, ct);
         var locationTask = _m.GetLocation(x, ct);
         var sizesTask = _m.GetSizes(x, ct);
         await Task.WhenAll(photoTask, locationTask, sizesTask);
         var model = new SavePhotoModel(photoTask.Result, locationTask.Result, sizesTask.Result);
         return model;
     }));
 }
        public async Task <Location> GetLocation(PhotoId photoId, CancellationToken ct)
        {
            var locationResponse = await _pictureFetchingService.GetLocation(photoId.flickrId, ct);

            if (locationResponse == null)
            {
                ColoredConsole.WriteLine($"empty location for photo #{photoId.flickrId}", Colors.txtWarning);
                return(null);
            }
            ColoredConsole.WriteLine($"        > location for #{photoId.flickrId} fetched: {locationResponse.photo.location.place_id}", Colors.txtInfo);
            return(locationResponse.photo.location);
        }
Пример #22
0
        private async Task <IActionResult> SetLikeState(PhotoId photoId, UserId userId, bool likeState)
        {
            try
            {
                await feedbackService.SetLikeState(photoId, userId, likeState);
            }
            catch (Exception ex)
            {
                logger.LogError(ex, $"Error in {nameof(SetLikeState)}({photoId})");
                throw;
            }

            return(Json("OK"));
        }
Пример #23
0
        public Task <IEnumerable <PhotoComment> > GetComments(PhotoId photoId)
        {
            logWriter.LogInformation($"{nameof(GetComments)}({nameof(photoId)} = '{photoId}')");
            QueryRequest request = new QueryRequest
            {
                TableName     = tableName,
                KeyConditions = new Dictionary <string, Condition>
                {
                    { FieldMappings.PartitionKey, dynamoDbCore.GetStringEqualsCondition(photoId.ToDbValue()) },
                    { FieldMappings.SortKey, dynamoDbCore.GetStringBeginsWithCondition("comment") }
                }
            };

            return(dynamoDbCore.Query(request, Mappers.PhotoComment));
        }
        public async Task <PhotoState> GetPhotoState(PhotoId photoId)
        {
            logWriter.LogInformation($"{nameof(GetPhotoState)}({nameof(photoId)} = '{photoId}')");

            var request = new GetItemRequest(tableName, new Dictionary <string, AttributeValue>
            {
                { FieldMappings.PartitionKey, new AttributeValue(photoId.ToDbValue()) },
                { FieldMappings.SortKey, new AttributeValue(photoId.ToDbValue()) }
            });

            var photo = await dynamoDbCore.GetItem(request, Mappers.PhotoModel);

            if (photo == null)
            {
                logWriter.LogInformation($"No photo found for id '{photoId.ToDbValue()}'");
                return(PhotoState.Undefined);
            }

            return(photo.State);
        }
        private async Task <IEnumerable <PhotoId> > GetTouchedPhotoIds(DateTimeOffset dateTimeOffset)
        {
            logWriter.LogInformation($"{nameof(GetTouchedPhotoIds)}({nameof(dateTimeOffset)} = '{dateTimeOffset.ToString(Constants.DateTimeFormatWithMilliseconds)}')");

            ScanRequest query = new ScanRequest(tableName)
            {
                // 'photolike', 'photocomment', 'photo'
                IndexName                 = "GSI2",
                FilterExpression          = $"(RecordType = :photolike OR RecordType = :photocomment OR RecordType = :photo) AND CreatedTime > :createdTime",
                ExpressionAttributeValues = new Dictionary <string, AttributeValue>
                {
                    { ":photolike", new AttributeValue("photolike") },
                    { ":photocomment", new AttributeValue("photocomment") },
                    { ":photo", new AttributeValue("photo") },
                    { ":createdTime", new AttributeValue(dateTimeOffset.ToString(Constants.DateTimeFormatWithMilliseconds)) },
                },
                ReturnConsumedCapacity = ReturnConsumedCapacity.INDEXES
            };

            var response = await dynamoDbCore.Scan(query, Mappers.Noop);

            return(response.Select(x => PhotoId.FromDbValue(x[FieldMappings.PartitionKey].S)).Distinct());
        }
Пример #26
0
        private async Task AddLikeRecord(UserId userId, PhotoId photoId)
        {
            logWriter.LogInformation($"{nameof(AddLikeRecord)}({nameof(userId)} = '{userId}', {nameof(photoId)} = '{photoId}')");

            var photo = await photoRepository.GetPhotoById(photoId, Guid.Empty);

            if (photo.PhotoIsLikedByCurrentUser)
            {
                return;
            }

            PhotoLikeRecord likeRecord = new PhotoLikeRecord
            {
                UserId      = userId,
                PhotoId     = photoId,
                CreatedTime = DateTimeOffset.UtcNow
            };

            var scoreDelta = scoreCalculator.GetLikeScore(likeRecord);

            var putItemRequest = new PutItemRequest()
            {
                TableName = tableName,
                Item      = Mappers.PhotoLike.ToDbItem(likeRecord),
            };

            try
            {
                await dynamoDbCore.PutItem(putItemRequest);
                await UpdateLikeCountAndScore(photo, 1, scoreDelta);
            }
            catch (Exception ex)
            {
                logWriter.LogError(ex, $"Error in {nameof(AddLikeRecord)}({nameof(userId)} = '{userId}', {nameof(photoId)} = '{photoId}'):\n{ex.ToString()}");
                throw;
            }
        }
        public async Task <bool> RequireDatabaseUpdate(PhotoId photoId, CancellationToken ct)
        {
            var(flickrId, title) = photoId;
            try
            {
                var photo = await _photoRepository.GetByHostingId(flickrId);

                if (photo == null)
                {
                    return(true);
                }


                // ColoredConsole.WriteLine("already saved: " + JsonSerializer.Serialize(photo), Colors.txtWarning);

                // todo: detect if caption is changed and the reupload is required.
                return(false);
            }
            catch (Exception e)
            {
                ColoredConsole.WriteLine($"{e}", Colors.bgDanger);
                return(false);
            }
        }
Пример #28
0
 => Apply(new PhotoCreated(PhotoId, name));
        private async Task AddLikeDataForUser(UserId currentUserId, ConcurrentDictionary <PhotoId, PhotoModel> result)
        {
            if (currentUserId != (UserId)Guid.Empty)
            {
                var request = new QueryRequest
                {
                    TableName = tableName,
                    IndexName = "GSI1",
                    KeyConditionExpression    = $"{FieldMappings.Gsi1PartitionKey} = :userId and {FieldMappings.SortKey} = :userlike",
                    ExpressionAttributeValues = new Dictionary <string, AttributeValue>
                    {
                        { ":userId", new AttributeValue {
                              S = currentUserId.ToDbValue()
                          } },
                        { ":userlike", new AttributeValue {
                              S = $"like|{currentUserId.ToDbValue()}"
                          } }
                    },
                    ProjectionExpression = FieldMappings.PartitionKey
                };

                var likedPhotoIds = (await dynamoDbCore.Query(request, Mappers.Noop)).Select(x => PhotoId.FromDbValue(x[FieldMappings.PartitionKey].S));

                foreach (var photoId in likedPhotoIds)
                {
                    if (result.TryGetValue(photoId, out var photo))
                    {
                        photo.PhotoIsLikedByCurrentUser = true;
                    }
                }
            }
        }
        private async Task <IEnumerable <Dictionary <string, AttributeValue> > > GetAllItemKeys(PhotoId photoId)
        {
            logWriter.LogInformation($"{nameof(GetAllItemKeys)}({nameof(photoId)} = '{photoId}')");

            var photo = await GetPhotoById(photoId, Guid.Empty);

            UserId userId = photo.UserId;

            QueryRequest requestForPhotoWithCommentsAndLikes = new QueryRequest(tableName)
            {
                KeyConditionExpression    = $"{FieldMappings.PartitionKey} = :pkvalue",
                ExpressionAttributeValues = new Dictionary <string, AttributeValue>
                {
                    { ":pkvalue", new AttributeValue(photoId.ToDbValue()) }
                }
            };

            // get all items related to the photo (mainly hashtags)
            QueryRequest requestForItemsRelatedToPhoto = new QueryRequest(tableName)
            {
                IndexName = "GSI1",
                KeyConditionExpression    = $"{FieldMappings.Gsi1PartitionKey} = :userId AND {FieldMappings.SortKey} = :photoId",
                ExpressionAttributeValues = new Dictionary <string, AttributeValue>
                {
                    { ":userId", new AttributeValue(userId.ToDbValue()) },
                    { ":photoId", new AttributeValue(photoId.ToDbValue()) }
                }
            };

            var queryTasks = new[] {
                dynamoDbCore.Query(requestForPhotoWithCommentsAndLikes, Mappers.Noop),
                dynamoDbCore.Query(requestForItemsRelatedToPhoto, Mappers.Noop),
            };

            await Task.WhenAll(queryTasks);

            var comparer = EqualityComparer <Dictionary <string, AttributeValue> > .Default;

            return(queryTasks
                   // flatten into one sequence of dictionaries
                   .SelectMany(x => x.Result)
                   // strip all values except main partition- and sort key
                   .Select(x => x.Where(y => y.Key == FieldMappings.PartitionKey || y.Key == FieldMappings.SortKey).ToDictionary(y => y.Key, y => y.Value))
                   // filter down to distinct values
                   .Distinct(comparer));
        }