mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-15 10:04:44 +00:00
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:
@@ -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>;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using Features.ImageUploads.Repository;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Features.ImageUploads.DependencyInjection;
|
||||
|
||||
/// <summary>Registers the services required by the PhotoUpload feature.</summary>
|
||||
public static class FeaturesPhotoUploadServiceCollectionExtensions
|
||||
{
|
||||
/// <summary>Registers <see cref="IPhotoUploadRepository" /> and its implementation.</summary>
|
||||
/// <returns><paramref name="services" />, for chaining.</returns>
|
||||
public static IServiceCollection AddFeaturesPhotoUpload(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IPhotoUploadRepository, PhotoUploadRepository>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,10 @@
|
||||
<ProjectReference Include="..\..\Domain\Domain.Entities\Domain.Entities.csproj" />
|
||||
<ProjectReference Include="..\..\Domain\Domain.Exceptions\Domain.Exceptions.csproj" />
|
||||
<ProjectReference Include="..\..\Database\Database.Connection\Database.Connection.csproj" />
|
||||
<ProjectReference Include="..\..\Infrastructure\Infrastructure.FileUpload\Infrastructure.FileUpload.csproj" />
|
||||
<ProjectReference Include="..\..\Shared\Shared.Contracts\Shared.Contracts.csproj" />
|
||||
<ProjectReference Include="..\..\Shared\Shared.Application\Shared.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Data.Common;
|
||||
using Dapper;
|
||||
using Database.Connection;
|
||||
using Domain.Entities;
|
||||
using Domain.Exceptions;
|
||||
|
||||
namespace Features.ImageUploads.Repository;
|
||||
|
||||
public interface IPhotoUploadRepository
|
||||
{
|
||||
Task CreateAsync(Photo photo, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public class PhotoUploadRepository(ISqlConnectionFactory connectionFactory)
|
||||
: DapperRepository(connectionFactory),
|
||||
IPhotoUploadRepository
|
||||
{
|
||||
public async Task CreateAsync(Photo photo, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
await using DbConnection connection = await CreateConnection();
|
||||
await using DbTransaction transaction = await connection.BeginTransactionAsync();
|
||||
|
||||
try
|
||||
{
|
||||
bool userExists =
|
||||
await connection.ExecuteScalarAsync<int?>(
|
||||
new CommandDefinition(
|
||||
"""
|
||||
SELECT 1
|
||||
FROM
|
||||
dbo.UserAccount
|
||||
WHERE
|
||||
UserAccountID = @UploadedById
|
||||
""",
|
||||
new { photo.UploadedById },
|
||||
transaction
|
||||
)
|
||||
)
|
||||
is not null;
|
||||
|
||||
if (!userExists)
|
||||
throw new NotFoundException("User not found.");
|
||||
|
||||
await connection.ExecuteAsync(
|
||||
new CommandDefinition(
|
||||
"""
|
||||
INSERT INTO dbo.Photo (Hyperlink, UploadedByID)
|
||||
VALUES (@Hyperlink, @UploadedById)
|
||||
""",
|
||||
new
|
||||
{
|
||||
photo.Hyperlink,
|
||||
photo.UploadedById
|
||||
},
|
||||
transaction
|
||||
)
|
||||
);
|
||||
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
}
|
||||
catch
|
||||
{
|
||||
await RollbackQuietlyAsync(transaction);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user