wip composing threads - nested service provider is busted
This commit is contained in:
parent
806bab368e
commit
7b401d84ea
|
@ -1,7 +1,11 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Internal;
|
||||
using PostLab.Data.Model.Abstract;
|
||||
using PostLab.Data.Model.Forum;
|
||||
using PostLab.Data.Model.Framework;
|
||||
using PostLab.Data.Services.Live;
|
||||
using PostLab.Data.Services.Realms;
|
||||
using PostLab.Pages.Controls;
|
||||
using PostLab.Util;
|
||||
|
||||
namespace PostLab.Data.Services.Forum
|
||||
|
@ -14,10 +18,12 @@ namespace PostLab.Data.Services.Forum
|
|||
|
||||
private ForumBoard boardData;
|
||||
|
||||
private IServiceProvider serviceProvider;
|
||||
|
||||
public Guid Id => boardData.Id;
|
||||
public string Name => boardData.Name;
|
||||
|
||||
public ActiveBoard(IDbContextFactory<ApplicationDbContext> dbContext, Guid boardId)
|
||||
public ActiveBoard(IDbContextFactory<ApplicationDbContext> dbContext, Guid boardId, IServiceProvider serviceProvider)
|
||||
{
|
||||
this.dbContext = new AsyncMonitor<ApplicationDbContext>(dbContext.CreateDbContext());
|
||||
this.threadsChannel = new ThreadCollectionChannel(this.dbContext);
|
||||
|
@ -25,6 +31,58 @@ namespace PostLab.Data.Services.Forum
|
|||
boardData = context.Value.ForumBoards.Where(b => b.Id == boardId).FirstOrDefault() ?? throw new ArgumentException("board doesn't exist");
|
||||
}
|
||||
|
||||
public async Task NewThreadAsync(NewThreadModel model)
|
||||
{
|
||||
var globalServices = serviceProvider.GetRequiredService<GlobalServices>();
|
||||
var userProfileService = globalServices.ServiceProvider.GetRequiredService<UserProfileService>();
|
||||
|
||||
var posts = new List<ForumPost>();
|
||||
|
||||
using (var context = dbContext.Obtain())
|
||||
{
|
||||
var user = context.Value.Profiles.Where(p => p.Id == userProfileService.ActiveProfile.Id).FirstOrDefault() ?? throw new InvalidOperationException("User id doesn't exist");
|
||||
|
||||
|
||||
var content = new RichContent()
|
||||
{
|
||||
Content = model.Content,
|
||||
ContentType = ContentType.Markdown,
|
||||
CreatedUtc = ConstructionUtil.GetDateTimeUtc(),
|
||||
Id = ConstructionUtil.NewId(),
|
||||
OwnerId = userProfileService.ActiveProfile.Id,
|
||||
Owner = user
|
||||
};
|
||||
|
||||
|
||||
var op = new ForumPost
|
||||
{
|
||||
Id = ConstructionUtil.NewId(),
|
||||
CreatedUtc = ConstructionUtil.GetDateTimeUtc(),
|
||||
OwnerId = userProfileService.ActiveProfile.Id,
|
||||
Owner = user,
|
||||
ReplyTo = null,
|
||||
History = new List<Versioned<RichContent>.TrackedVersion>()
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = ConstructionUtil.NewId(),
|
||||
CreatedUtc = ConstructionUtil.GetDateTimeUtc(),
|
||||
Content = content,
|
||||
}
|
||||
},
|
||||
};
|
||||
posts.Add(op);
|
||||
}
|
||||
|
||||
await threadsChannel.CreateAsync(new ForumThread
|
||||
{
|
||||
Title = model.Title,
|
||||
Id = ConstructionUtil.NewId(),
|
||||
CreatedUtc = ConstructionUtil.GetDateTimeUtc(),
|
||||
Posts = posts
|
||||
});
|
||||
}
|
||||
|
||||
public class ThreadCollectionChannel : PersistentEntityCollectionChannel<ForumThread>
|
||||
{
|
||||
public ThreadCollectionChannel(AsyncMonitor<ApplicationDbContext> dbContext) : base(dbContext)
|
||||
|
@ -66,5 +124,12 @@ namespace PostLab.Data.Services.Forum
|
|||
return await context.ForumThreads.ToListAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public class NewThreadModel : Composer.IComposerModel
|
||||
{
|
||||
public string Title { get; set; }
|
||||
|
||||
public string Content { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Internal;
|
||||
using PostLab.Data.Model.Forum;
|
||||
using PostLab.Data.Services.Realms;
|
||||
using PostLab.Data.Services.Live;
|
||||
using PostLab.Util;
|
||||
|
||||
namespace PostLab.Data.Services.Forum
|
||||
|
@ -14,15 +14,18 @@ namespace PostLab.Data.Services.Forum
|
|||
|
||||
private ForumThread threadData;
|
||||
|
||||
private IServiceProvider serviceProvider;
|
||||
|
||||
public Guid Id => threadData.Id;
|
||||
public string Title => threadData.Title;
|
||||
|
||||
public ActiveThread(IDbContextFactory<ApplicationDbContext> dbContext, Guid threadId)
|
||||
public ActiveThread(IDbContextFactory<ApplicationDbContext> dbContext, Guid threadId, IServiceProvider serviceProvider)
|
||||
{
|
||||
this.dbContext = new AsyncMonitor<ApplicationDbContext>(dbContext.CreateDbContext());
|
||||
this.postsChannel = new PostsCollectionChannel(this.dbContext, threadId);
|
||||
using var context = this.dbContext.Obtain();
|
||||
threadData = context.Value.ForumThreads.Where(t => t.Id == threadId).FirstOrDefault() ?? throw new ArgumentException("thread doesn't exist");
|
||||
this.serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public class PostsCollectionChannel : PersistentEntityCollectionChannel<ForumPost>
|
||||
|
@ -67,7 +70,7 @@ namespace PostLab.Data.Services.Forum
|
|||
protected override Task<IEnumerable<ForumPost>> InnerReadAllAsync(ApplicationDbContext context)
|
||||
{
|
||||
var thread = context.ForumThreads.Where(t => t.Id == threadId).FirstOrDefault() ?? throw new InvalidOperationException("thread doesn't exist?");
|
||||
return Task.FromResult(thread.Posts as IEnumerable<ForumPost>);
|
||||
return Task.FromResult(thread.Posts as IEnumerable<ForumPost> ?? throw new InvalidOperationException("thread is missing posts?"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
using PostLab.Data.Model.Abstract;
|
||||
|
||||
namespace PostLab.Data.Services.Live
|
||||
{
|
||||
public record EntityNotification<T> where T: TimestampedUnique
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
public required DateTime WhenUtc { get; init; }
|
||||
public required SnapshotId SnapshotId { get; init; }
|
||||
}
|
||||
|
||||
public record AddEntityNotification<T> : EntityNotification<T> where T: TimestampedUnique
|
||||
{
|
||||
public required T Value { get; init; }
|
||||
}
|
||||
|
||||
public record UpdateEntityNotification<T> : EntityNotification<T> where T: TimestampedUnique
|
||||
{
|
||||
public Guid OldId { get; init; }
|
||||
public required T NewValue { get; init; }
|
||||
}
|
||||
|
||||
public record DeleteEntityNotification<T> : EntityNotification<T> where T: TimestampedUnique
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
using PostLab.Data.Model.Abstract;
|
||||
using PostLab.Util;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace PostLab.Data.Services.Live
|
||||
{
|
||||
public class ObservableCollectionEntityNotificationReader<T>: INotifyCollectionChanged, INotifyPropertyChanged, IReadOnlyCollection<T> where T: TimestampedUnique
|
||||
{
|
||||
private readonly ChannelReader<EntityNotification<T>> reader;
|
||||
private readonly Action stateChangedCallback;
|
||||
private ImmutableArray<T> items = new();
|
||||
|
||||
private ObservableCollectionEntityNotificationReader(IEnumerable<T> initialItems, ChannelReader<EntityNotification<T>> reader, Action stateChangedCallback)
|
||||
{
|
||||
this.reader = reader;
|
||||
this.stateChangedCallback = stateChangedCallback;
|
||||
items = initialItems.ToImmutableArray();
|
||||
}
|
||||
|
||||
public int Count => ((IReadOnlyCollection<T>)items).Count;
|
||||
|
||||
public event NotifyCollectionChangedEventHandler? CollectionChanged;
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public static async Task<ObservableCollectionEntityNotificationReader<T>> CreateAsync(IEnumerable<T> initialItems, ChannelReader<EntityNotification<T>> reader, Action stateChangedCallback)
|
||||
{
|
||||
var result = new ObservableCollectionEntityNotificationReader<T>(initialItems, reader, stateChangedCallback);
|
||||
await result.StartReadingChannelAsync();
|
||||
return result;
|
||||
}
|
||||
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
return ((IEnumerable<T>)items).GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return ((IEnumerable)items).GetEnumerator();
|
||||
}
|
||||
|
||||
private async Task StartReadingChannelAsync()
|
||||
{
|
||||
var _ = Task.Run(async () =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var notification = await reader.ReadAsync();
|
||||
|
||||
switch (notification)
|
||||
{
|
||||
case AddEntityNotification<T> add:
|
||||
items = items.Add(add.Value);
|
||||
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, add.Value));
|
||||
break;
|
||||
case UpdateEntityNotification<T> update:
|
||||
var oldValue = items.First(x => x.Id == update.OldId);
|
||||
items = items.Replace(oldValue, update.NewValue);
|
||||
|
||||
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, update.NewValue, update.OldId));
|
||||
break;
|
||||
case DeleteEntityNotification<T> delete:
|
||||
var removedValue = items.First(x => x.Id == delete.Id);
|
||||
items = items.Remove(removedValue);
|
||||
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, delete.Id));
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unknown notification type {notification.GetType().Name}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
using PostLab.Util;
|
||||
using System.Threading.Channels;
|
||||
|
||||
namespace PostLab.Data.Services.Realms
|
||||
namespace PostLab.Data.Services.Live
|
||||
{
|
||||
/// <summary>
|
||||
/// Notifies of updates and manages a long-lived DB context.
|
||||
|
@ -10,9 +10,9 @@ namespace PostLab.Data.Services.Realms
|
|||
/// <typeparam name="T"></typeparam>
|
||||
public abstract class PersistentEntityCollectionChannel<T> where T : TimestampedUnique
|
||||
{
|
||||
private Channel<EntityNotification> notificationChannels = Channel.CreateUnbounded<EntityNotification>();
|
||||
public ChannelReader<EntityNotification> Reader => notificationChannels.Reader;
|
||||
|
||||
private Channel<EntityNotification<T>> notificationChannels = Channel.CreateUnbounded<EntityNotification<T>>();
|
||||
public ChannelReader<EntityNotification<T>> Reader => notificationChannels.Reader;
|
||||
|
||||
private int snapshotId = 0;
|
||||
|
||||
private readonly AsyncMonitor<ApplicationDbContext> dbContext;
|
||||
|
@ -20,7 +20,7 @@ namespace PostLab.Data.Services.Realms
|
|||
{
|
||||
this.dbContext = dbContext;
|
||||
}
|
||||
|
||||
|
||||
private SnapshotId NextSnapshotId()
|
||||
{
|
||||
return new SnapshotId(Interlocked.Increment(ref snapshotId));
|
||||
|
@ -31,7 +31,7 @@ namespace PostLab.Data.Services.Realms
|
|||
using var context = await dbContext.ObtainAsync();
|
||||
await CommitCreateAsync(context.Value, newValue);
|
||||
|
||||
await notificationChannels.Writer.WriteAsync(new AddEntityNotification
|
||||
await notificationChannels.Writer.WriteAsync(new AddEntityNotification<T>
|
||||
{
|
||||
Id = newValue.Id,
|
||||
WhenUtc = newValue.CreatedUtc,
|
||||
|
@ -45,7 +45,7 @@ namespace PostLab.Data.Services.Realms
|
|||
using var context = await dbContext.ObtainAsync();
|
||||
await CommitUpdateAsync(context.Value, oldId, newValue);
|
||||
|
||||
await notificationChannels.Writer.WriteAsync(new UpdateEntityNotification()
|
||||
await notificationChannels.Writer.WriteAsync(new UpdateEntityNotification<T>()
|
||||
{
|
||||
Id = newValue.Id,
|
||||
WhenUtc = newValue.CreatedUtc,
|
||||
|
@ -60,7 +60,7 @@ namespace PostLab.Data.Services.Realms
|
|||
using var context = await dbContext.ObtainAsync();
|
||||
await CommitDeleteAsync(context.Value, targetId);
|
||||
|
||||
await notificationChannels.Writer.WriteAsync(new DeleteEntityNotification()
|
||||
await notificationChannels.Writer.WriteAsync(new DeleteEntityNotification<T>()
|
||||
{
|
||||
Id = targetId,
|
||||
WhenUtc = DateTime.UtcNow,
|
||||
|
@ -79,27 +79,6 @@ namespace PostLab.Data.Services.Realms
|
|||
protected abstract Task<bool> CommitDeleteAsync(ApplicationDbContext context, Guid targetId);
|
||||
protected abstract Task<IEnumerable<T>> InnerReadAllAsync(ApplicationDbContext context);
|
||||
|
||||
public record EntityNotification
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
public required DateTime WhenUtc { get; init; }
|
||||
public required SnapshotId SnapshotId { get; init; }
|
||||
}
|
||||
|
||||
public record AddEntityNotification : EntityNotification
|
||||
{
|
||||
public required T Value { get; init; }
|
||||
}
|
||||
|
||||
public record UpdateEntityNotification : EntityNotification
|
||||
{
|
||||
public Guid OldId { get; init; }
|
||||
public required T NewValue { get; init; }
|
||||
}
|
||||
|
||||
public record DeleteEntityNotification : EntityNotification
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public record SnapshotId(int id);
|
|
@ -9,7 +9,7 @@ namespace PostLab.Data.Services.Realms
|
|||
{
|
||||
public class ActiveRealm
|
||||
{
|
||||
public static async Task<ActiveRealm?> ActivateRealm(IDbContextFactory<ApplicationDbContext> dbContextFactory, Guid realmId)
|
||||
public static async Task<ActiveRealm?> ActivateRealm(IDbContextFactory<ApplicationDbContext> dbContextFactory, Guid realmId, IServiceProvider globalServiceProvider)
|
||||
{
|
||||
using var context = await dbContextFactory.CreateDbContextAsync();
|
||||
var realm = await context.Realm.FindAsync(realmId);
|
||||
|
@ -19,7 +19,7 @@ namespace PostLab.Data.Services.Realms
|
|||
return null;
|
||||
}
|
||||
|
||||
var activeRealm = new ActiveRealm(dbContextFactory, realm);
|
||||
var activeRealm = new ActiveRealm(dbContextFactory, realm, new GlobalServices(globalServiceProvider));
|
||||
|
||||
await activeRealm.realmServiceHost.StartAsync();
|
||||
|
||||
|
@ -32,17 +32,19 @@ namespace PostLab.Data.Services.Realms
|
|||
|
||||
public string Url => $"/realm/{Id}";
|
||||
|
||||
private ActiveRealm(IDbContextFactory<ApplicationDbContext> dbContextFactory, Realm realmData)
|
||||
private ActiveRealm(IDbContextFactory<ApplicationDbContext> dbContextFactory, Realm realmData, GlobalServices globalServices)
|
||||
{
|
||||
var builder = new HostBuilder();
|
||||
builder.ConfigureServices(c =>
|
||||
{
|
||||
c.AddSingleton(dbContextFactory);
|
||||
c.AddSingleton(globalServices);
|
||||
c.AddSingleton(new AsyncMonitor<Realm>(realmData));
|
||||
c.AddSingleton(new AsyncMonitor<ApplicationDbContext>(dbContextFactory.CreateDbContext()));
|
||||
c.AddSingleton(new PersistentObservableCollection<ForumBoard>(dbContextFactory, c => c.ForumBoards));
|
||||
c.AddSingleton(new ConcurrentFactoryDictionary<Guid, ActiveBoard>(k => new ActiveBoard(dbContextFactory, k)));
|
||||
c.AddSingleton(new ConcurrentFactoryDictionary<Guid, ActiveThread>(k => new ActiveThread(dbContextFactory, k)));
|
||||
|
||||
c.AddSingleton(new ConcurrentFactoryDictionary<Guid, ActiveBoard>(k => new ActiveBoard(dbContextFactory, k, c.BuildServiceProvider())));
|
||||
c.AddSingleton(new ConcurrentFactoryDictionary<Guid, ActiveThread>(k => new ActiveThread(dbContextFactory, k, c.BuildServiceProvider())));
|
||||
});
|
||||
realmServiceHost = builder.Build();
|
||||
|
||||
|
@ -90,4 +92,6 @@ namespace PostLab.Data.Services.Realms
|
|||
|
||||
}
|
||||
}
|
||||
|
||||
public record GlobalServices(IServiceProvider ServiceProvider);
|
||||
}
|
||||
|
|
|
@ -7,10 +7,11 @@ namespace PostLab.Data.Services.Realms
|
|||
{
|
||||
private AsyncMonitor<Dictionary<Guid, ActiveRealm>> _activeRealms = new(new());
|
||||
private IDbContextFactory<ApplicationDbContext> _dbContextFactory;
|
||||
public RealmService(IDbContextFactory<ApplicationDbContext> dbContextFactory)
|
||||
private IServiceProvider _serviceProvider;
|
||||
public RealmService(IDbContextFactory<ApplicationDbContext> dbContextFactory, IServiceProvider serviceProvider)
|
||||
{
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task<ActiveRealm?> GetActiveRealmAsync(Guid realmId)
|
||||
|
@ -22,7 +23,7 @@ namespace PostLab.Data.Services.Realms
|
|||
return realm;
|
||||
}
|
||||
|
||||
var newRealm = await ActiveRealm.ActivateRealm(_dbContextFactory, realmId);
|
||||
var newRealm = await ActiveRealm.ActivateRealm(_dbContextFactory, realmId, _serviceProvider);
|
||||
if (newRealm == null)
|
||||
{
|
||||
return null;
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
<EditForm Model="@Model" OnSubmit="@HandleSubmitClicked">
|
||||
<InputText id="content" @bind-Value="Model.Content" />
|
||||
<button type="submit">Create</button>
|
||||
</EditForm>
|
||||
|
||||
@code {
|
||||
|
||||
[Parameter]
|
||||
public IComposerModel Model { get; init; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback HandleSubmitClicked { get; init; }
|
||||
|
||||
public interface IComposerModel
|
||||
{
|
||||
public string Content { get; set; }
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
@page "/realm/{RealmId}/board/{BoardId}/thread/new";
|
||||
@using PostLab.Data.Services;
|
||||
@using PostLab.Data.Services.Forum;
|
||||
@using PostLab.Data.Services.Realms;
|
||||
@using PostLab.Pages.Controls;
|
||||
|
||||
@inject UserProfileService UserProfileService
|
||||
@inject ProfileRealmService ProfileRealmService
|
||||
|
||||
@if (activeRealm == null && board == null)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
}
|
||||
else if (board == null)
|
||||
{
|
||||
<p><em>Board not found :(</em></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<InputText id="title" @bind-Value="newThreadModel.Title" />
|
||||
<Composer Model="@newThreadModel" HandleSubmitClicked="@OnSubmitClickedAsync" />
|
||||
}
|
||||
|
||||
@code {
|
||||
|
||||
private ActiveRealm? activeRealm;
|
||||
private ActiveBoard? board;
|
||||
|
||||
[Parameter]
|
||||
public string RealmId { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string BoardId { get; set; }
|
||||
|
||||
private ActiveBoard.NewThreadModel newThreadModel = new();
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
activeRealm = await ProfileRealmService.GetProfileRealmAsync(Guid.Parse(RealmId));
|
||||
board = activeRealm.GetActiveBoard(Guid.Parse(BoardId));
|
||||
}
|
||||
|
||||
private async Task OnSubmitClickedAsync()
|
||||
{
|
||||
if (board == null)
|
||||
{
|
||||
throw new InvalidOperationException("Board is null, cannot create thread");
|
||||
}
|
||||
|
||||
await board.NewThreadAsync(newThreadModel);
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue