Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
84ae189
feat(domain): add Image domain entity
SubochArtem Apr 19, 2026
615a8f5
feat(infrastructure): add ImageRepository
SubochArtem Apr 19, 2026
ef0541d
feat(infrastructure): add Image entities to ApplicationDbContext
SubochArtem Apr 19, 2026
f5e1310
feat(infrastructure): add DeletedImageRepository
SubochArtem Apr 19, 2026
8eee752
feat(infrastructure): add Image Repositories to UnitOfWork
SubochArtem Apr 19, 2026
606a7d4
feat(infrastructure): add Image configurations
SubochArtem Apr 19, 2026
854674b
feat(infrastructure): add MinioStorageService
SubochArtem Apr 19, 2026
399ff1b
feat(infrastructure): add ImageCleanupJob and MinioStorageService to DI
SubochArtem Apr 19, 2026
2d324cd
feat(infrastructure): add including Images in CityRepository
SubochArtem Apr 20, 2026
5fff11a
feat(infrastructure): add including Images in PollRepository
SubochArtem Apr 20, 2026
513e945
feat(infrastructure): add including Images in IdeaRepository
SubochArtem Apr 20, 2026
98408cb
feat(infrastructure): add GetByIdWithImagesAsync method Repository in…
SubochArtem Apr 20, 2026
cbbbd25
refactor(api): rename ct to cancellationToken in DeletedImageRepository
SubochArtem Apr 21, 2026
a8590d3
refactor(infastructure): add RepositoryCollection to reduce UnitOfWor…
SubochArtem Apr 21, 2026
a9feae2
refactor(application):replace public fields with properties in ImageFile
SubochArtem Apr 21, 2026
9468e60
refactor(api): delete needless filters from user controllers
SubochArtem Apr 20, 2026
d4d98cd
refactor(api): remove needles [AllowAnonymous] atributs from controllers
SubochArtem Apr 27, 2026
dd4730d
refactor(infrastructure): rename Add and Remove to Create and Delete …
SubochArtem Apr 27, 2026
c7763a5
refactor(application): rename EndsAtBefore to EndsBefore in PollFilter
SubochArtem Apr 27, 2026
d9822e4
refactor(application): move endsAtChanged declaration closer to EndsA…
SubochArtem Apr 27, 2026
bbf33ce
refactor(Jobs): move PollCleanupJob and PollStatusJob to application …
SubochArtem Apr 27, 2026
9673cf7
Merge branch 'task/status-change-bg-job' into task/polls-images-infra…
SubochArtem Apr 27, 2026
febfe29
feature(infrastructure) add RepositoryCollection to DI
SubochArtem Apr 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ Task<PagedList<City>> GetFilteredAsync(
Guid id,
PollStatus? status,
CancellationToken cancellationToken = default);

Task<City?> GetByIdWithImagesAsync(
Guid id,
CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Polls.Domain.Images;

namespace Polls.Application.Common.Interfaces;

public interface IDeletedImageRepository
{
Task<IReadOnlyList<DeletedImage>> GetBatchAsync(
int batchSize,
CancellationToken cancellationToken = default);

void AddRange(IEnumerable<DeletedImage> images);
void RemoveRange(IEnumerable<DeletedImage> images);
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ Task<PagedList<Idea>> GetFilteredAsync(
Task<Idea?> GetWithPollAsync(
Guid id,
CancellationToken cancellationToken = default);

Task<Idea?> GetByIdWithImagesAsync(
Guid id,
CancellationToken cancellationToken = default);

Task UpdateStatusByCityAsync(
Guid cityId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Polls.Domain.Images;

namespace Polls.Application.Common.Interfaces;

public interface IImageRepository<TImage> where TImage : Image
{
Task<IReadOnlyList<TImage>> GetByIdsAsync(IEnumerable<Guid> ids, CancellationToken ct = default);
void AddRange(IEnumerable<TImage> images);
void RemoveRange(IEnumerable<TImage> images);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Polls.Application.Common.Models;

namespace Polls.Application.Common.Interfaces;

public interface IImageStorageService
{
Task<IReadOnlyList<string>> UploadRangeAsync(
IReadOnlyList<ImageFile> files,
CancellationToken cancellationToken = default);

Task DeleteByNamesAsync(
IEnumerable<string> objectNames,
CancellationToken cancellationToken = default);

string GetUrl(string fileName);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ Task<PagedList<Poll>> GetFilteredAsync(
Guid id,
IdeaStatus? ideaStatus,
CancellationToken cancellationToken = default);

Task<Poll?> GetByIdWithImagesAsync(
Guid id,
CancellationToken cancellationToken = default);

Task UpdateStatusByCityAsync(
Guid cityId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Polls.Domain.Images;

namespace Polls.Application.Common.Interfaces;

public interface IUnitOfWork
Expand All @@ -6,6 +8,10 @@ public interface IUnitOfWork
IPollRepository Polls { get; }
IIdeaRepository Ideas { get; }
IPollScheduleJobRepository PollScheduleJobs { get; }
IImageRepository<CityImage> CityImages { get; }
IImageRepository<PollImage> PollImages { get; }
IImageRepository<IdeaImage> IdeaImages { get; }
IDeletedImageRepository DeletedImages { get; }
Task SaveChangesAsync(CancellationToken cancellationToken = default);
Task<IUnitOfWorkTransaction> BeginTransactionAsync(CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ public abstract class BaseFilter
public int Page { get; set; } = DefaultPage;
public int PageSize { get; set; } = DefaultPageSize;
public string? SearchTerm { get; set; }
public bool IncludeImages { get; set; } = true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@ namespace Polls.Application.Common.Models;
public class CityFilter : BaseFilter
{
public CityStatus? Status { get; set; }

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Polls.Application.Common.Models;

public class ImageFile
{
public required Stream Content { get; set; }
public required string FileName { get; set; }
public required string ContentType { get; set; }
}
2 changes: 2 additions & 0 deletions src/Backend/Services/Polls/Polls.Domain/Cities/City.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Polls.Domain.Cities.Enums;
using Polls.Domain.Common;
using Polls.Domain.Images;
using Polls.Domain.Polls;

namespace Polls.Domain.Cities;
Expand All @@ -9,4 +10,5 @@ public class City : EntityBase
public required Coordinates Coordinates { get; set; }
public CityStatus Status { get; set; } = CityStatus.Undefined;
public ICollection<Poll> Polls { get; set; } = [];
public ICollection<CityImage> Images { get; set; } = [];
}
2 changes: 2 additions & 0 deletions src/Backend/Services/Polls/Polls.Domain/Ideas/Idea.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Polls.Domain.Common;
using Polls.Domain.Ideas.Enums;
using Polls.Domain.Images;
using Polls.Domain.Polls;

namespace Polls.Domain.Ideas;
Expand All @@ -10,4 +11,5 @@ public class Idea : EntityBase
public Guid PollId { get; set; }
public Poll Poll { get; set; } = null!;
public IdeaStatus Status { get; set; } = IdeaStatus.Undefined;
public ICollection<IdeaImage> Images { get; set; } = [];
}
6 changes: 6 additions & 0 deletions src/Backend/Services/Polls/Polls.Domain/Images/CityImage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Polls.Domain.Images;

public class CityImage : Image
{
public Guid CityId { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Polls.Domain.Images;

public class DeletedImage
{
public Guid Id { get; set; }
public required string FileName { get; set; }
public DateTimeOffset QueuedAt { get; set; } = DateTimeOffset.UtcNow;
}
6 changes: 6 additions & 0 deletions src/Backend/Services/Polls/Polls.Domain/Images/IdeaImage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Polls.Domain.Images;

public class IdeaImage : Image
{
public Guid IdeaId { get; set; }
}
8 changes: 8 additions & 0 deletions src/Backend/Services/Polls/Polls.Domain/Images/Image.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Polls.Domain.Images;

public class Image
{
public Guid Id { get; set; }
public required string FileName { get; set; }
public int Order { get; set; }
}
21 changes: 21 additions & 0 deletions src/Backend/Services/Polls/Polls.Domain/Images/ImageErrors.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Polls.Domain.Common;

namespace Polls.Domain.Images;

public static class ImageErrors
{
public static Error NotFound(Guid id) =>
Error.NotFound($"Image with id '{id}' was not found");

public static Error NotBelongsToEntity(Guid id) =>
Error.Conflict($"Image with id '{id}' does not belong to this entity");

public static Error MaxImagesExceeded(int maxImagesCount) =>
Error.Conflict($"Maximum number of images ({maxImagesCount}) exceeded");

public static Error InvalidFormat(string[] allowedFormats) =>
Error.Conflict($"Invalid image format. Allowed: {string.Join(", ", allowedFormats)}");

public static Error FileTooLarge(int maxImageSize) =>
Error.Conflict($"Image size exceeds {maxImageSize}MB limit");
}
6 changes: 6 additions & 0 deletions src/Backend/Services/Polls/Polls.Domain/Images/PollImage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Polls.Domain.Images;

public class PollImage : Image
{
public Guid PollId { get; set; }
}
2 changes: 2 additions & 0 deletions src/Backend/Services/Polls/Polls.Domain/Polls/Poll.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Polls.Domain.Common;
using Polls.Domain.Ideas;
using Polls.Domain.Images;
using Polls.Domain.Polls.Enums;

namespace Polls.Domain.Polls;
Expand All @@ -12,4 +13,5 @@ public class Poll : EntityBase
public decimal BudgetAmount { get; set; }
public PollStatus Status { get; set; } = PollStatus.Undefined;
public ICollection<Idea> Ideas { get; set; } = [];
public ICollection<PollImage> Images { get; set; } = [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Minio;
using Polls.Application.Common.Interfaces;
using Polls.Application.Jobs;
using Polls.Domain.Images;
using Polls.Infrastructure.Jobs;
using Polls.Infrastructure.Persistence;
using Polls.Infrastructure.Persistence.Interceptors;
using Polls.Infrastructure.Persistence.Options;
using Polls.Infrastructure.Persistence.Repositories;
using Polls.Infrastructure.Storage;

namespace Polls.Infrastructure;

Expand All @@ -27,6 +30,11 @@ public static IServiceCollection AddInfrastructure(
.ValidateDataAnnotations()
.ValidateOnStart();

services.AddOptions<ImageStorageOptions>()
.Bind(configuration.GetSection(ImageStorageOptions.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart();

services.AddTransient(typeof(Lazy<>), typeof(LazyResolver<>));

services
Expand Down Expand Up @@ -57,8 +65,29 @@ public static IServiceCollection AddInfrastructure(
.AddScoped<IPollRepository, PollRepository>()
.AddScoped<IIdeaRepository, IdeaRepository>()
.AddScoped<IPollScheduleJobRepository, PollScheduleJobRepository>()
.AddScoped<IImageRepository<CityImage>, ImageRepository<CityImage>>()
.AddScoped<IImageRepository<PollImage>, ImageRepository<PollImage>>()
.AddScoped<IImageRepository<IdeaImage>, ImageRepository<IdeaImage>>()
.AddScoped<IDeletedImageRepository, DeletedImageRepository>()
.AddScoped<RepositoryCollection>()
.AddScoped<IUnitOfWork, UnitOfWork>();

var storageOptions = configuration
.GetSection(ImageStorageOptions.SectionName)
.Get<ImageStorageOptions>()
?? throw new InvalidOperationException(
$"Configuration section '{ImageStorageOptions.SectionName}' is missing or invalid.");

services.AddMinio(client =>
{
client
.WithEndpoint(storageOptions.Endpoint)
.WithCredentials(storageOptions.AccessKey, storageOptions.SecretKey)
.WithSSL(storageOptions.UseSsl);
});

services.AddScoped<IImageStorageService, MinioStorageService>();

services.AddHangfire((serviceProvider, config) =>
{
var dbOptions = serviceProvider.GetRequiredService<IOptions<DatabaseOptions>>().Value;
Expand All @@ -75,23 +104,31 @@ public static IServiceCollection AddInfrastructure(
services
.AddScoped<IPollScheduler, HangfirePollScheduler>()
.AddScoped<PollStatusJob>()
.AddScoped<PollCleanupJob>();
.AddScoped<PollCleanupJob>()
.AddScoped<ImageCleanupJob>();

return services;
}

public static void UseInfrastructure(this WebApplication app)
{
const string hangfireDashboardPath = "/hangfire";
const string pollCleanupJobId = "poll-cleanup";

const string imageCleanupJobId = "image-cleanup";

if (app.Environment.IsDevelopment())
app.UseHangfireDashboard(hangfireDashboardPath);

var recurringJobManager = app.Services.GetRequiredService<IRecurringJobManager>();

recurringJobManager.AddOrUpdate<PollCleanupJob>(
pollCleanupJobId,
job => job.ExecuteAsync(CancellationToken.None),
Cron.Hourly);

recurringJobManager.AddOrUpdate<ImageCleanupJob>(
imageCleanupJobId,
job => job.ExecuteAsync(CancellationToken.None),
Cron.Hourly);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Polls.Application.Common.Interfaces;

namespace Polls.Infrastructure.Jobs;

public class ImageCleanupJob(
IUnitOfWork unitOfWork,
IImageStorageService storageService)
{
public async Task ExecuteAsync(CancellationToken cancellationToken = default)
{
const int batchSize = 100;
var hasMore = true;

while (hasMore)
{
var batch = await unitOfWork.DeletedImages.GetBatchAsync(batchSize, cancellationToken);
hasMore = batch.Count == batchSize;

if (batch.Count == 0)
break;

var fileNames = batch.Select(x => x.FileName).ToList();

await storageService.DeleteByNamesAsync(fileNames, cancellationToken);
unitOfWork.DeletedImages.RemoveRange(batch);
await unitOfWork.SaveChangesAsync(cancellationToken);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using Polls.Domain.Cities;
using Polls.Domain.Ideas;
using Polls.Domain.Images;
using Polls.Domain.Polls;
using Polls.Domain.PollScheduleJob;

Expand All @@ -11,6 +12,10 @@ public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options
public DbSet<City> Cities => Set<City>();
public DbSet<Poll> Polls => Set<Poll>();
public DbSet<Idea> Ideas => Set<Idea>();
public DbSet<CityImage> CityImages => Set<CityImage>();
public DbSet<PollImage> PollImages => Set<PollImage>();
public DbSet<IdeaImage> IdeaImages => Set<IdeaImage>();
public DbSet<DeletedImage> DeletedImages => Set<DeletedImage>();
public DbSet<PollScheduleJob> PollScheduleJobs => Set<PollScheduleJob>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
Expand Down
Loading