Skip to content

Commit

Permalink
IHS-56 Adding tags feature (#59)
Browse files Browse the repository at this point in the history
* rename image tags table

* add adding tags feature
  • Loading branch information
adedw authored Mar 9, 2024
1 parent a2d1bba commit 9960ddc
Show file tree
Hide file tree
Showing 12 changed files with 155 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public class ImageHostingDbContext(DbContextOptions<ImageHostingDbContext> optio
: DbContext(options), IImageHostingDbContext
{
public DbSet<Image> Images => Set<Image>();
public DbSet<ImageTag> ImageTags => Set<ImageTag>();
public DbSet<ForbiddenCategory> ForbiddenCategories => Set<ForbiddenCategory>();

public void Migrate() => Database.Migrate();
Expand Down
15 changes: 14 additions & 1 deletion ImageHosting.Persistence/Entities/Image.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,18 @@ public class Image
public bool Hidden { get; set; }
public DateTime UploadedAt { get; set; }

public HashSet<ImageTag>? Tags { get; set; }
public HashSet<ImageTag> Tags { get; } = [];

public void AddTag(string tagName)
{
Tags.Add(new ImageTag { Image = this, ImageId = Id, TagName = tagName });
}

public void AddTags(IEnumerable<string> tags)
{
foreach (var tag in tags)
{
AddTag(tag);
}
}
}
24 changes: 22 additions & 2 deletions ImageHosting.Persistence/Entities/ImageTag.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,29 @@
namespace ImageHosting.Persistence.Entities;

[EntityTypeConfiguration(typeof(ImageTagConfiguration))]
public class ImageTag
public class ImageTag : IEquatable<ImageTag>
{
public required Image Image { get; set; }
public required Image Image { get; init; }
public ImageId ImageId { get; init; }
public required string TagName { get; init; }

public bool Equals(ImageTag? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return ImageId.Equals(other.ImageId) && TagName == other.TagName;
}

public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((ImageTag)obj);
}

public override int GetHashCode()
{
return HashCode.Combine(ImageId, TagName);
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ protected override void Up(MigrationBuilder migrationBuilder)
table: "Images");

migrationBuilder.CreateTable(
name: "ImageTag",
name: "ImageTags",
columns: table => new
{
ImageId = table.Column<Guid>(type: "uuid", nullable: false),
TagName = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ImageTag", x => new { x.ImageId, x.TagName });
table.PrimaryKey("PK_ImageTags", x => new { x.ImageId, x.TagName });
table.ForeignKey(
name: "FK_ImageTag_Images_ImageId",
name: "FK_ImageTags_Images_ImageId",
column: x => x.ImageId,
principalTable: "Images",
principalColumn: "Id",
Expand All @@ -39,7 +39,7 @@ protected override void Up(MigrationBuilder migrationBuilder)
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ImageTag");
name: "ImageTags");

migrationBuilder.AddColumn<List<string>>(
name: "Categories",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)

b.HasKey("ImageId", "TagName");

b.ToTable("ImageTag");
b.ToTable("ImageTags");
});

modelBuilder.Entity("ImageHosting.Persistence.Entities.ImageTag", b =>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ImageHosting.Storage.Extensions.DependencyInjection;

public static class QueryableExtensions
{
public static async Task<HashSet<TSource>> ToHashSetAsync<TSource>(
this IQueryable<TSource> source,
CancellationToken cancellationToken = default)
{
var set = new HashSet<TSource>();
await foreach (var element in source.AsAsyncEnumerable().WithCancellation(cancellationToken)
.ConfigureAwait(false))
{
set.Add(element);
}

return set;
}
}
37 changes: 28 additions & 9 deletions ImageHosting.Storage/Features/Images/Endpoints/Images.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public static RouteGroupBuilder MapImagesEndpoints(this IEndpointRouteBuilder ro
})
.DisableAntiforgery()
.WithName("PostImage")
.WithTags("Images")
.MapToApiVersion(1);

images.MapGet(pattern: "{id}/asset", handler: async ([AsParameters] GetImageAssetParams @params,
Expand All @@ -55,6 +56,7 @@ public static RouteGroupBuilder MapImagesEndpoints(this IEndpointRouteBuilder ro
})
.AddEndpointFilter<ValidationFilter<GetImageAssetParams>>()
.WithName("GetImageAsset")
.WithTags("Images")
.MapToApiVersion(1)
.Produces(StatusCodes.Status200OK)
.ProducesValidationProblem(StatusCodes.Status422UnprocessableEntity);
Expand All @@ -67,19 +69,36 @@ public static RouteGroupBuilder MapImagesEndpoints(this IEndpointRouteBuilder ro
return TypedResults.Ok(readImage);
})
.WithName("GetImage")
.WithTags("Images")
.MapToApiVersion(1);

images.MapPut(pattern: "{id}/name", async ([FromRoute] ImageId id, [FromBody] UpdateNameCommand command,
[FromServices] IUpdateNameHandler updateNameHandler, CancellationToken cancellationToken) =>
{
var readImage = await updateNameHandler.UpdateAsync(id, command.NewName, cancellationToken);
[FromServices] IUpdateNameHandler updateNameHandler, CancellationToken cancellationToken) =>
{
var readImage = await updateNameHandler.UpdateAsync(id, command.NewName, cancellationToken);

return Results.Ok(readImage);
})
.AddEndpointFilter<ValidationFilter<UpdateNameCommand>>()
.WithName("UpdateName")
.WithTags("Images")
.Produces<ReadImage>()
.ProducesValidationProblem(StatusCodes.Status422UnprocessableEntity)
.MapToApiVersion(1);

var tags = images.MapGroup(prefix: "{id}/tags");

tags.MapPost(pattern: "", handler: async ([FromRoute] ImageId id, [FromBody] AddTagsCommand addTagsCommand,
[FromServices] IAddTagsHandler addTagsHandler, CancellationToken cancellationToken) =>
{
var response = await addTagsHandler.AddAsync(id, addTagsCommand.Tags, cancellationToken);

return TypedResults.Ok(response);
})
.WithName("AddTag")
.WithTags("Tags")
.MapToApiVersion(1);

return TypedResults.Ok(readImage);
})
.AddEndpointFilter<ValidationFilter<UpdateNameCommand>>()
.WithName("UpdateName")
.MapToApiVersion(1);

return images;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public static IServiceCollection AddImageServices(this IServiceCollection servic
.AddTransient<IUploadFileHandler, UploadFileHandler>()
.AddTransient<IGetImageAssetHandler, GetImageAssetHandler>()
.AddTransient<IGetImageHandler, GetImageHandler>()
.AddTransient<IUpdateNameHandler, UpdateNameHandler>();
.AddTransient<IUpdateNameHandler, UpdateNameHandler>()
.AddTransient<IAddTagsHandler, AddTagsHandler>();
}
}
41 changes: 41 additions & 0 deletions ImageHosting.Storage/Features/Images/Handlers/AddTagsHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ImageHosting.Persistence.DbContexts;
using ImageHosting.Persistence.ValueTypes;
using ImageHosting.Storage.Features.Images.Exceptions;
using ImageHosting.Storage.Features.Images.Models;
using Microsoft.EntityFrameworkCore;

namespace ImageHosting.Storage.Features.Images.Handlers;

public interface IAddTagsHandler
{
Task<AddTagsResponse> AddAsync(ImageId id, IEnumerable<string> tags,
CancellationToken cancellationToken = default);
}

public class AddTagsHandler(IImageHostingDbContext dbContext) : IAddTagsHandler
{
public async Task<AddTagsResponse> AddAsync(ImageId id, IEnumerable<string> tags,
CancellationToken cancellationToken = default)
{
var image = await dbContext.Images
.Include(i => i.Tags)
.AsSplitQuery()
.FirstOrDefaultAsync(i => i.Id == id, cancellationToken);
if (image is null)
{
throw new ImageMetadataNotFoundException(id);
}

image.AddTags(tags);

await dbContext.SaveChangesAsync(cancellationToken);
return new AddTagsResponse
{
Tags = image.Tags.Select(t => t.TagName).ToList()
};
}
}
8 changes: 8 additions & 0 deletions ImageHosting.Storage/Features/Images/Models/AddTagsCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Collections.Generic;

namespace ImageHosting.Storage.Features.Images.Models;

public class AddTagsCommand
{
public required IEnumerable<string> Tags { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Collections.Generic;

namespace ImageHosting.Storage.Features.Images.Models;

public class AddTagsResponse
{
public required IEnumerable<string> Tags { get; init; }
}

0 comments on commit 9960ddc

Please sign in to comment.