Adds streaming to GetContents

This commit is contained in:
Jonathan Bourdon
2024-07-17 21:47:31 -04:00
parent 25ea9b50c7
commit bc87331903
12 changed files with 201 additions and 42 deletions

View File

@@ -14,7 +14,7 @@ namespace Hutopy.Infrastructure.Data;
public static class InitializerExtensions public static class InitializerExtensions
{ {
public static async Task InitialiseDatabaseAsync(this WebApplication app) public static async Task InitialiseApplicationDatabaseAsync(this WebApplication app)
{ {
using var scope = app.Services.CreateScope(); using var scope = app.Services.CreateScope();

View File

@@ -2,8 +2,6 @@
namespace Hutopy.Web.Common; namespace Hutopy.Web.Common;
public class Shared(string claimName) : Exception;
public static class ClaimsPrincipalExtensions public static class ClaimsPrincipalExtensions
{ {
public static Guid GetUserId(this ClaimsPrincipal claims) public static Guid GetUserId(this ClaimsPrincipal claims)
@@ -30,7 +28,7 @@ public static class ClaimsPrincipalExtensions
{ {
var claim = claims.FindFirst(key); var claim = claims.FindFirst(key);
if (claim is null) throw new Shared(key); if (claim is null) throw new MissingClaimException(key);
if (typeof(TValue) == typeof(Guid)) if (typeof(TValue) == typeof(Guid))
{ {

View File

@@ -0,0 +1,30 @@
using System.Buffers.Binary;
using System.Security.Cryptography;
namespace Hutopy.Web.Common;
public static class GuidHelper
{
// TODO: Delete when NET9 is release!
public static Guid GenerateUuidV7()
{
Span<byte> uuidv7 = stackalloc byte[16];
ulong unixTimeTicks = (ulong)DateTimeOffset.UtcNow.Subtract(DateTimeOffset.UnixEpoch).Ticks;
ulong unixTsMs = (unixTimeTicks & 0x0FFFFFFFFFFFF000) << 4;
ulong unixTsMsVer = unixTsMs | 0b0111UL << 12;
ulong randA = unixTimeTicks & 0x0000000000000FFF;
// merge "unix_ts_ms", "ver" and "rand_a"
ulong hi = unixTsMsVer | randA;
BinaryPrimitives.WriteUInt64BigEndian(uuidv7, hi);
// fill "rand_b" and "var"
RandomNumberGenerator.Fill(uuidv7[8..]);
// set "var"
byte varOctet = uuidv7[8];
varOctet = (byte)(varOctet & 0b00111111);
varOctet = (byte)(varOctet | 0b10111111);
uuidv7[8] = varOctet;
var value = Convert.ToHexString(uuidv7);
return Guid.Parse(value);
}
}

View File

@@ -0,0 +1,3 @@
namespace Hutopy.Web.Common;
public class MissingClaimException(string claimName) : Exception;

View File

@@ -0,0 +1,71 @@
using Hutopy.Infrastructure.Identity;
using Hutopy.Web.Common;
using Microsoft.EntityFrameworkCore;
namespace Hutopy.Web.Contents.Data;
public static class InitializerExtensions
{
public static async Task InitialiseContentDatabaseAsync(this WebApplication app, CancellationToken ct = default)
{
using var scope = app.Services.CreateScope();
var initializer = scope.ServiceProvider.GetRequiredService<ContentDbContextInitializer>();
await initializer.InitialiseAsync(ct);
await initializer.SeedAsync(ct);
}
}
internal class ContentDbContextInitializer(
ILogger<ContentDbContextInitializer> logger,
ApplicationUserManager userManager,
ContentDbContext context)
{
public async Task InitialiseAsync(CancellationToken ct = default)
{
try
{
await context.Database.MigrateAsync(ct);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while initialising the database.");
throw;
}
}
public async Task SeedAsync(CancellationToken ct = default)
{
try
{
var administratorUser = await userManager.FindByNameAsync("administrator@localhost");
var administratorId= Guid.Parse(administratorUser!.Id);
await TrySeedAsync(administratorId, 100, ct);
await context.SaveChangesAsync(ct);
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while seeding the database.");
throw;
}
}
private async Task TrySeedAsync(Guid creatorId, int contentCount, CancellationToken ct = default)
{
for (var c = 0; c < contentCount; c++)
{
await context.Contents.AddAsync(
new Content
{
Id = GuidHelper.GenerateUuidV7(),
CreatedBy = creatorId,
Title = $"Title {c}",
Description = $"Description {c}"
},
ct);
}
}
}

View File

@@ -0,0 +1,18 @@
using Hutopy.Web.Contents.Data;
using Microsoft.EntityFrameworkCore;
namespace Hutopy.Web.Contents;
public static class DependencyInjection
{
public static IServiceCollection AddContentModule(
this IServiceCollection services,
Action<DbContextOptionsBuilder>? configureAction = null)
{
services.AddDbContext<ContentDbContext>(configureAction);
services.AddScoped<ContentDbContextInitializer>();
return services;
}
}

View File

@@ -4,9 +4,14 @@ using Microsoft.EntityFrameworkCore;
namespace Hutopy.Web.Contents.Handlers; namespace Hutopy.Web.Contents.Handlers;
public sealed class GetContentsRequest
{
public Guid ContentId { get; set; }
}
public class GetContents( public class GetContents(
ContentDbContext context) ContentDbContext context)
: EndpointWithoutRequest<Content> : Endpoint<GetContentsRequest, Content>
{ {
public override void Configure() public override void Configure()
{ {
@@ -16,15 +21,18 @@ public class GetContents(
} }
public override async Task HandleAsync( public override async Task HandleAsync(
GetContentsRequest req,
CancellationToken ct) CancellationToken ct)
{ {
var contentId = Route<Guid>("ContentId"); var content = await context
var comments = await context
.Contents .Contents
.Where(c => c.Id == contentId) .FirstOrDefaultAsync(
.ToListAsync(cancellationToken: ct); c => c.Id == req.ContentId,
cancellationToken: ct);
await SendAsync(comments.First(), cancellation: ct); if (content is null)
await SendNotFoundAsync(cancellation: ct);
else
await SendAsync(content, cancellation: ct);
} }
} }

View File

@@ -4,9 +4,16 @@ using Microsoft.EntityFrameworkCore;
namespace Hutopy.Web.Contents.Handlers; namespace Hutopy.Web.Contents.Handlers;
public sealed class GetContentsByUserRequest
{
public Guid UserId { get; set; }
[BindFrom("max_items")] public int MaxItems { get; set; } = 10;
[BindFrom("last_id")] public Guid? LastId { get; set; }
}
public class GetContentsByUser( public class GetContentsByUser(
ContentDbContext context) ContentDbContext context)
: EndpointWithoutRequest<List<Content>> : Endpoint<GetContentsByUserRequest, List<Content>>
{ {
public override void Configure() public override void Configure()
{ {
@@ -16,14 +23,21 @@ public class GetContentsByUser(
} }
public override async Task HandleAsync( public override async Task HandleAsync(
GetContentsByUserRequest req,
CancellationToken ct) CancellationToken ct)
{ {
var userId = Route<Guid>("UserId"); var query = context
var posts = await context
.Contents .Contents
.Where(c => c.CreatedBy == userId) .Where(c => c.CreatedBy == req.UserId);
.ToListAsync(cancellationToken: ct);
if (req.LastId is not null)
query = query.OrderBy(c => c.Id).Where(c => c.Id > req.LastId.Value);
else
query = query.OrderBy(c => c.Id);
query = query.Take(req.MaxItems);
var posts = await query.ToListAsync(cancellationToken: ct);
await SendAsync(posts, cancellation: ct); await SendAsync(posts, cancellation: ct);
} }

View File

@@ -0,0 +1,16 @@
using Hutopy.Web.Messages.Data;
using Microsoft.EntityFrameworkCore;
namespace Hutopy.Web.Messages;
public static class DependencyInjection
{
public static IServiceCollection AddMessagingModule(
this IServiceCollection services,
Action<DbContextOptionsBuilder>? configureAction = null)
{
services.AddDbContext<MessagingDbContext>(configureAction);
return services;
}
}

View File

@@ -4,9 +4,14 @@ using Microsoft.EntityFrameworkCore;
namespace Hutopy.Web.Messages.Handlers; namespace Hutopy.Web.Messages.Handlers;
public sealed class GetMessagesRequest
{
public Guid SubjectId { get; set; }
}
public class GetMessages( public class GetMessages(
MessagingDbContext context) MessagingDbContext context)
: EndpointWithoutRequest<List<Message>> : Endpoint<GetMessagesRequest, List<Message>>
{ {
public override void Configure() public override void Configure()
{ {
@@ -16,13 +21,12 @@ public class GetMessages(
} }
public override async Task HandleAsync( public override async Task HandleAsync(
GetMessagesRequest req,
CancellationToken ct) CancellationToken ct)
{ {
var subjectId = Route<Guid>("SubjectId");
var comments = await context var comments = await context
.Messages .Messages
.Where(c => c.SubjectId == subjectId) .Where(c => c.SubjectId == req.SubjectId)
.ToListAsync(cancellationToken: ct); .ToListAsync(cancellationToken: ct);
await SendAsync(comments, cancellation: ct); await SendAsync(comments, cancellation: ct);

View File

@@ -1,13 +1,17 @@
using FastEndpoints; using FastEndpoints;
using Hutopy.Web.Messages.Data; using Hutopy.Web.Messages.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
namespace Hutopy.Web.Messages.Handlers; namespace Hutopy.Web.Messages.Handlers;
public sealed class GetMessagesByUserRequest
{
public Guid UserId { get; set; }
}
public class GetMessagesByUser( public class GetMessagesByUser(
MessagingDbContext context) MessagingDbContext context)
: EndpointWithoutRequest<List<Message>> : Endpoint<GetMessagesByUserRequest, List<Message>>
{ {
public override void Configure() public override void Configure()
{ {
@@ -16,13 +20,12 @@ public class GetMessagesByUser(
} }
public override async Task HandleAsync( public override async Task HandleAsync(
GetMessagesByUserRequest req,
CancellationToken ct) CancellationToken ct)
{ {
var userId = Route<Guid>("UserId");
var posts = await context var posts = await context
.Messages .Messages
.Where(c => c.CreatedBy == userId) .Where(c => c.CreatedBy == req.UserId)
.ToListAsync(cancellationToken: ct); .ToListAsync(cancellationToken: ct);
await SendAsync(posts, cancellation: ct); await SendAsync(posts, cancellation: ct);

View File

@@ -5,8 +5,9 @@ using Hutopy.Infrastructure;
using Hutopy.Infrastructure.Data; using Hutopy.Infrastructure.Data;
using Hutopy.Infrastructure.Identity; using Hutopy.Infrastructure.Identity;
using Hutopy.Web; using Hutopy.Web;
using Hutopy.Web.Contents;
using Hutopy.Web.Contents.Data; using Hutopy.Web.Contents.Data;
using Hutopy.Web.Messages.Data; using Hutopy.Web.Messages;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using NSwag; using NSwag;
@@ -78,20 +79,12 @@ builder.Services.AddOpenApiDocument((configure, sp) =>
configure.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT")); configure.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor("JWT"));
}); });
builder.Services.AddFastEndpoints();
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") var connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Missing ConnectionStrings:DefaultConnection"); ?? throw new InvalidOperationException("Missing ConnectionStrings:DefaultConnection");
builder.Services.AddDbContext<MessagingDbContext>((_, options) => builder.Services.AddFastEndpoints();
{ builder.Services.AddContentModule(options => options.UseSqlServer(connectionString));
options.UseSqlServer(connectionString); builder.Services.AddMessagingModule(options => options.UseSqlServer(connectionString));
});
builder.Services.AddDbContext<ContentDbContext>((_, options) =>
{
options.UseSqlServer(connectionString);
});
builder.Services.Configure<JwtOptions>(builder.Configuration.GetRequiredSection(JwtOptions.SectionName)); builder.Services.Configure<JwtOptions>(builder.Configuration.GetRequiredSection(JwtOptions.SectionName));
@@ -109,7 +102,8 @@ app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
// Initialize and seed the db. // Initialize and seed the db.
await app.InitialiseDatabaseAsync(); await app.InitialiseApplicationDatabaseAsync();
await app.InitialiseContentDatabaseAsync();
// Configure the HTTP request pipeline. // Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())