Add PhotoUpload feature for uploading photos to file storage

Adds UploadPhotoCommand/Handler/Validator and PhotoUploadRepository, invoked via MediatR by other features' commands (e.g. a future CreateBreweryPhoto) rather than exposed through its own controller. Wires the feature and IFileStorageProvider into API.Core.
This commit is contained in:
Aaron Po
2026-08-28 00:32:53 -04:00
parent 999c94eedc
commit 05882f36e4
10 changed files with 182 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
using MediatR;
using Microsoft.AspNetCore.Http;
namespace Features.ImageUploads.Commands.UploadPhoto;
/// <summary>
/// Uploads a new photo to storage under <c>{Key}/{generated photo id}</c> and records it. Sent by
/// other features' handlers (e.g. a brewery photo upload command) rather than bound from a request
/// directly, since this feature exposes no controller of its own.
/// </summary>
public record UploadPhotoCommand(Guid UploadedById, string Key, IFormFile File) : IRequest<Guid>;

View File

@@ -0,0 +1,40 @@
using Domain.Entities;
using Features.ImageUploads.Repository;
using Infrastructure.FileUpload;
using MediatR;
namespace Features.ImageUploads.Commands.UploadPhoto;
/// <summary>Handles <see cref="UploadPhotoCommand" />.</summary>
public class UploadPhotoHandler(IFileStorageProvider fileStorageProvider, IPhotoUploadRepository photoUploadRepository)
: IRequestHandler<UploadPhotoCommand, Guid>
{
public async Task<Guid> Handle(UploadPhotoCommand request, CancellationToken cancellationToken)
{
Guid photoId = Guid.NewGuid();
string key = $"{request.Key}/{photoId}";
await using Stream stream = request.File.OpenReadStream();
await fileStorageProvider.UploadAsync(
key,
stream,
request.File.ContentType,
cancellationToken
);
Photo photo = new()
{
Hyperlink = key,
UploadedById = request.UploadedById,
};
await photoUploadRepository.CreateAsync(photo,cancellationToken);
return photoId;
}
}

View File

@@ -0,0 +1,33 @@
using FluentValidation;
namespace Features.ImageUploads.Commands.UploadPhoto;
/// <summary>Validates <see cref="UploadPhotoCommand" />.</summary>
public class UploadPhotoValidator : AbstractValidator<UploadPhotoCommand>
{
public UploadPhotoValidator()
{
RuleFor(x => x.UploadedById)
.NotEmpty()
.WithMessage("UploadedById is required.");
RuleFor(x => x.File)
.NotNull()
.WithMessage("File is required.");
RuleFor(x => x.Key)
.NotEmpty()
.WithMessage("Key is required.");
RuleFor(x => x.File.Length)
.GreaterThan(0)
.When(x => x.File is not null)
.WithMessage("File cannot be empty.");
RuleFor(x => x.File.ContentType)
.Must(contentType => contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase))
.When(x => x.File is not null)
.WithMessage("File must be an image.");
}
}