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 async Task InitialiseDatabaseAsync(this WebApplication app)
public static async Task InitialiseApplicationDatabaseAsync(this WebApplication app)
{
using var scope = app.Services.CreateScope();

View File

@@ -2,8 +2,6 @@
namespace Hutopy.Web.Common;
public class Shared(string claimName) : Exception;
public static class ClaimsPrincipalExtensions
{
public static Guid GetUserId(this ClaimsPrincipal claims)
@@ -30,7 +28,7 @@ public static class ClaimsPrincipalExtensions
{
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))
{

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,27 +4,35 @@ using Microsoft.EntityFrameworkCore;
namespace Hutopy.Web.Contents.Handlers;
public sealed class GetContentsRequest
{
public Guid ContentId { get; set; }
}
public class GetContents(
ContentDbContext context)
: EndpointWithoutRequest<Content>
: Endpoint<GetContentsRequest, Content>
{
public override void Configure()
{
Get("/api/contents/{ContentId:guid}");
Options( o => o.WithTags("Contents"));
Options(o => o.WithTags("Contents"));
AllowAnonymous();
}
public override async Task HandleAsync(
GetContentsRequest req,
CancellationToken ct)
{
var contentId = Route<Guid>("ContentId");
var comments = await context
var content = await context
.Contents
.Where(c => c.Id == contentId)
.ToListAsync(cancellationToken: ct);
.FirstOrDefaultAsync(
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,26 +4,40 @@ using Microsoft.EntityFrameworkCore;
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(
ContentDbContext context)
: EndpointWithoutRequest<List<Content>>
: Endpoint<GetContentsByUserRequest, List<Content>>
{
public override void Configure()
{
Get("/api/contents/user/{UserId:guid}");
Options( o => o.WithTags("Contents"));
Options(o => o.WithTags("Contents"));
AllowAnonymous();
}
public override async Task HandleAsync(
GetContentsByUserRequest req,
CancellationToken ct)
{
var userId = Route<Guid>("UserId");
var posts = await context
var query = context
.Contents
.Where(c => c.CreatedBy == userId)
.ToListAsync(cancellationToken: ct);
.Where(c => c.CreatedBy == req.UserId);
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);
}

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;
public sealed class GetMessagesRequest
{
public Guid SubjectId { get; set; }
}
public class GetMessages(
MessagingDbContext context)
: EndpointWithoutRequest<List<Message>>
: Endpoint<GetMessagesRequest, List<Message>>
{
public override void Configure()
{
@@ -16,13 +21,12 @@ public class GetMessages(
}
public override async Task HandleAsync(
GetMessagesRequest req,
CancellationToken ct)
{
var subjectId = Route<Guid>("SubjectId");
var comments = await context
.Messages
.Where(c => c.SubjectId == subjectId)
.Where(c => c.SubjectId == req.SubjectId)
.ToListAsync(cancellationToken: ct);
await SendAsync(comments, cancellation: ct);

View File

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

View File

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