Adds streaming to GetContents
This commit is contained in:
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
{
|
{
|
||||||
30
src/Web/Common/GuidExtensions.cs
Normal file
30
src/Web/Common/GuidExtensions.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/Web/Common/MissingClaimException.cs
Normal file
3
src/Web/Common/MissingClaimException.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace Hutopy.Web.Common;
|
||||||
|
|
||||||
|
public class MissingClaimException(string claimName) : Exception;
|
||||||
71
src/Web/Contents/Data/ContentDbContextInitializer.cs
Normal file
71
src/Web/Contents/Data/ContentDbContextInitializer.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/Web/Contents/DependencyInjection.cs
Normal file
18
src/Web/Contents/DependencyInjection.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
16
src/Web/Messages/DependencyInjection.cs
Normal file
16
src/Web/Messages/DependencyInjection.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
Reference in New Issue
Block a user