diff --git a/Femto.Api/Controllers/Auth/AuthController.cs b/Femto.Api/Controllers/Auth/AuthController.cs new file mode 100644 index 0000000..717d9c2 --- /dev/null +++ b/Femto.Api/Controllers/Auth/AuthController.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Femto.Api.Controllers.Auth; + +[ApiController] +[Route("auth")] +public class AuthController : ControllerBase +{ + [HttpPost("login")] + public async Task> Login([FromBody] LoginRequest request) + { + return new LoginResponse(Guid.Parse("0196960c-6296-7532-ba66-8fabb38c6ae0"), "johnbotris", "token"); + } + + [HttpPost("signup")] + public async Task> Signup([FromBody] SignupRequest request) + { + throw new NotImplementedException(); + } + + [HttpPost("delete-session")] + public async Task DeleteSession([FromBody] DeleteSessionRequest request) + { + // TODO + return Ok(new {}); + } +} \ No newline at end of file diff --git a/Femto.Api/Controllers/Auth/DeleteSessionRequest.cs b/Femto.Api/Controllers/Auth/DeleteSessionRequest.cs new file mode 100644 index 0000000..b4ab6c1 --- /dev/null +++ b/Femto.Api/Controllers/Auth/DeleteSessionRequest.cs @@ -0,0 +1,3 @@ +namespace Femto.Api.Controllers.Auth; + +public record DeleteSessionRequest(string SessionToken); \ No newline at end of file diff --git a/Femto.Api/Controllers/Auth/LoginRequest.cs b/Femto.Api/Controllers/Auth/LoginRequest.cs new file mode 100644 index 0000000..8366d14 --- /dev/null +++ b/Femto.Api/Controllers/Auth/LoginRequest.cs @@ -0,0 +1,3 @@ +namespace Femto.Api.Controllers.Auth; + +public record LoginRequest(string Username, string Password); \ No newline at end of file diff --git a/Femto.Api/Controllers/Auth/LoginResponse.cs b/Femto.Api/Controllers/Auth/LoginResponse.cs new file mode 100644 index 0000000..86ee763 --- /dev/null +++ b/Femto.Api/Controllers/Auth/LoginResponse.cs @@ -0,0 +1,3 @@ +namespace Femto.Api.Controllers.Auth; + +public record LoginResponse(Guid UserId, string Username, string SessionToken); \ No newline at end of file diff --git a/Femto.Api/Controllers/Auth/SignupRequest.cs b/Femto.Api/Controllers/Auth/SignupRequest.cs new file mode 100644 index 0000000..cb811ef --- /dev/null +++ b/Femto.Api/Controllers/Auth/SignupRequest.cs @@ -0,0 +1,3 @@ +namespace Femto.Api.Controllers.Auth; + +public record SignupRequest(string Username, string Password, string SignupCode, string? Email); \ No newline at end of file diff --git a/Femto.Api/Controllers/Auth/SignupResponse.cs b/Femto.Api/Controllers/Auth/SignupResponse.cs new file mode 100644 index 0000000..acfeff6 --- /dev/null +++ b/Femto.Api/Controllers/Auth/SignupResponse.cs @@ -0,0 +1,3 @@ +namespace Femto.Api.Controllers.Auth; + +public record SignupResponse(Guid UserId, string Username, string SessionToken); \ No newline at end of file diff --git a/Femto.Api/Controllers/Authors/AuthorsController.cs b/Femto.Api/Controllers/Authors/AuthorsController.cs deleted file mode 100644 index 8fec530..0000000 --- a/Femto.Api/Controllers/Authors/AuthorsController.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Femto.Api.Controllers.Authors.Dto; -using Femto.Modules.Blog.Domain.Posts.Commands.GetPosts; -using MediatR; -using Microsoft.AspNetCore.Mvc; - -namespace Femto.Api.Controllers.Authors; - -[ApiController] -[Route("authors")] -public class AuthorsController(IMediator mediator) : ControllerBase -{ - [HttpGet("{username}/posts")] - public async Task> GetAuthorPosts( - string username, - [FromQuery] GetAuthorPostsSearchParams searchParams, - CancellationToken cancellationToken - ) - { - var res = await mediator.Send( - new GetPostsQuery - { - Username = username, - Amount = searchParams.Amount ?? 20, - From = searchParams.From, - }, - cancellationToken - ); - - return new GetAuthorPostsResponse( - res.Posts.Select(p => new AuthorPostDto( - p.PostId, - p.Text, - p.Media.Select(m => m.Url), - p.CreatedAt, - new AuthoPostAuthorDto(p.Author.AuthorId, p.Author.Username) - )), - res.Next - ); - } -} diff --git a/Femto.Api/Controllers/Authors/Dto/AuthoPostAuthorDto.cs b/Femto.Api/Controllers/Authors/Dto/AuthoPostAuthorDto.cs deleted file mode 100644 index d6e4f32..0000000 --- a/Femto.Api/Controllers/Authors/Dto/AuthoPostAuthorDto.cs +++ /dev/null @@ -1,6 +0,0 @@ -using JetBrains.Annotations; - -namespace Femto.Api.Controllers.Authors.Dto; - -[PublicAPI] -public record AuthoPostAuthorDto(Guid AuthorId, string Username); \ No newline at end of file diff --git a/Femto.Api/Controllers/Authors/Dto/AuthorPostDto.cs b/Femto.Api/Controllers/Authors/Dto/AuthorPostDto.cs deleted file mode 100644 index 7c62ede..0000000 --- a/Femto.Api/Controllers/Authors/Dto/AuthorPostDto.cs +++ /dev/null @@ -1,6 +0,0 @@ -using JetBrains.Annotations; - -namespace Femto.Api.Controllers.Authors.Dto; - -[PublicAPI] -public record AuthorPostDto(Guid PostId, string Content, IEnumerable Media, DateTimeOffset CreatedAt, AuthoPostAuthorDto Author ); \ No newline at end of file diff --git a/Femto.Api/Controllers/Authors/Dto/GetAuthorPostsResponse.cs b/Femto.Api/Controllers/Authors/Dto/GetAuthorPostsResponse.cs deleted file mode 100644 index 10af47a..0000000 --- a/Femto.Api/Controllers/Authors/Dto/GetAuthorPostsResponse.cs +++ /dev/null @@ -1,6 +0,0 @@ -using JetBrains.Annotations; - -namespace Femto.Api.Controllers.Authors.Dto; - -[PublicAPI] -public record GetAuthorPostsResponse(IEnumerable Posts, Guid? Next); \ No newline at end of file diff --git a/Femto.Api/Controllers/Authors/Dto/GetAuthorPostsSearchParams.cs b/Femto.Api/Controllers/Authors/Dto/GetAuthorPostsSearchParams.cs deleted file mode 100644 index 4c3011f..0000000 --- a/Femto.Api/Controllers/Authors/Dto/GetAuthorPostsSearchParams.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Femto.Api.Controllers.Authors.Dto; - -public record GetAuthorPostsSearchParams(Guid? From, int? Amount); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/CreatePostRequest.cs b/Femto.Api/Controllers/Posts/Dto/CreatePostRequest.cs index 28c7df9..3413de3 100644 --- a/Femto.Api/Controllers/Posts/Dto/CreatePostRequest.cs +++ b/Femto.Api/Controllers/Posts/Dto/CreatePostRequest.cs @@ -1,3 +1,3 @@ namespace Femto.Api.Controllers.Posts.Dto; -public record CreatePostRequest(Guid AuthorId, string Content, IEnumerable Media); +public record CreatePostRequest(Guid AuthorId, string Content, IEnumerable Media); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/CreatePostRequestMedia.cs b/Femto.Api/Controllers/Posts/Dto/CreatePostRequestMedia.cs new file mode 100644 index 0000000..8a855b3 --- /dev/null +++ b/Femto.Api/Controllers/Posts/Dto/CreatePostRequestMedia.cs @@ -0,0 +1,6 @@ +using JetBrains.Annotations; + +namespace Femto.Api.Controllers.Posts.Dto; + +[PublicAPI] +public record CreatePostRequestMedia(Guid MediaId, Uri Url, string? Type, int? Width, int? Height); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/GetPublicPostsSearchParams.cs b/Femto.Api/Controllers/Posts/Dto/GetPublicPostsSearchParams.cs index 228626c..e5155f6 100644 --- a/Femto.Api/Controllers/Posts/Dto/GetPublicPostsSearchParams.cs +++ b/Femto.Api/Controllers/Posts/Dto/GetPublicPostsSearchParams.cs @@ -3,4 +3,4 @@ using JetBrains.Annotations; namespace Femto.Api.Controllers.Posts.Dto; [PublicAPI] -public record GetPublicPostsSearchParams(Guid? From, int? Amount); \ No newline at end of file +public record GetPublicPostsSearchParams(Guid? From, int? Amount, Guid? AuthorId, string? Author); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/PublicPostDto.cs b/Femto.Api/Controllers/Posts/Dto/PublicPostDto.cs index 8eeaecd..d357dd3 100644 --- a/Femto.Api/Controllers/Posts/Dto/PublicPostDto.cs +++ b/Femto.Api/Controllers/Posts/Dto/PublicPostDto.cs @@ -7,6 +7,6 @@ public record PublicPostDto( PublicPostAuthorDto Author, Guid PostId, string Content, - IEnumerable Media, - DateTimeOffset CreatedAt + IEnumerable Media, + DateTimeOffset CreatedAt ); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/Dto/PublicPostMediaDto.cs b/Femto.Api/Controllers/Posts/Dto/PublicPostMediaDto.cs new file mode 100644 index 0000000..e3e0c59 --- /dev/null +++ b/Femto.Api/Controllers/Posts/Dto/PublicPostMediaDto.cs @@ -0,0 +1,6 @@ +using JetBrains.Annotations; + +namespace Femto.Api.Controllers.Posts.Dto; + +[PublicAPI] +public record PublicPostMediaDto(Uri Url, int? Width, int? Height); \ No newline at end of file diff --git a/Femto.Api/Controllers/Posts/PostsController.cs b/Femto.Api/Controllers/Posts/PostsController.cs index 12db869..8e195c5 100644 --- a/Femto.Api/Controllers/Posts/PostsController.cs +++ b/Femto.Api/Controllers/Posts/PostsController.cs @@ -1,6 +1,8 @@ using Femto.Api.Controllers.Posts.Dto; -using Femto.Modules.Blog.Domain.Posts.Commands.CreatePost; -using Femto.Modules.Blog.Domain.Posts.Commands.GetPosts; +using Femto.Modules.Blog; +using Femto.Modules.Blog.Application; +using Femto.Modules.Blog.Application.Commands.CreatePost; +using Femto.Modules.Blog.Application.Queries.GetPosts; using MediatR; using Microsoft.AspNetCore.Mvc; @@ -8,7 +10,7 @@ namespace Femto.Api.Controllers.Posts; [ApiController] [Route("posts")] -public class PostsController(IMediator mediator) : ControllerBase +public class PostsController(IBlogModule blogModule) : ControllerBase { [HttpGet] public async Task> GetAllPublicPosts( @@ -16,11 +18,13 @@ public class PostsController(IMediator mediator) : ControllerBase CancellationToken cancellationToken ) { - var res = await mediator.Send( + var res = await blogModule.PostQuery( new GetPostsQuery { From = searchParams.From, - Amount = searchParams.Amount ?? 20 + Amount = searchParams.Amount ?? 20, + AuthorId = searchParams.AuthorId, + Author = searchParams.Author, }, cancellationToken ); @@ -30,7 +34,7 @@ public class PostsController(IMediator mediator) : ControllerBase new PublicPostAuthorDto(p.Author.AuthorId, p.Author.Username), p.PostId, p.Text, - p.Media.Select(m => m.Url), + p.Media.Select(m => new PublicPostMediaDto(m.Url, m.Width, m.Height)), p.CreatedAt )), res.Next @@ -43,8 +47,22 @@ public class PostsController(IMediator mediator) : ControllerBase CancellationToken cancellationToken ) { - var guid = await mediator.Send( - new CreatePostCommand(req.AuthorId, req.Content, req.Media), + var guid = await blogModule.PostCommand( + new CreatePostCommand( + req.AuthorId, + req.Content, + req.Media.Select( + (media, idx) => + new CreatePostMedia( + media.MediaId, + media.Url, + media.Type, + idx, + media.Width, + media.Height + ) + ) + ), cancellationToken ); diff --git a/Femto.Api/Femto.Api.csproj b/Femto.Api/Femto.Api.csproj index 5b7c903..6617cb2 100644 --- a/Femto.Api/Femto.Api.csproj +++ b/Femto.Api/Femto.Api.csproj @@ -9,6 +9,7 @@ + @@ -17,6 +18,7 @@ + diff --git a/Femto.Api/Program.cs b/Femto.Api/Program.cs index 442a032..3d0af32 100644 --- a/Femto.Api/Program.cs +++ b/Femto.Api/Program.cs @@ -1,8 +1,8 @@ using System.Text.Json; using System.Text.Json.Serialization; -using Femto.Modules.Blog; +using Femto.Modules.Blog.Application; using Femto.Modules.Media; -using Quartz; +using Femto.Modules.Media.Application; var builder = WebApplication.CreateBuilder(args); @@ -16,13 +16,9 @@ var blobStorageRoot = builder.Configuration.GetValue("BlobStorageRoot"); if (blobStorageRoot is null) throw new Exception("no blob storage root found"); -builder.Services.AddQuartzHostedService(options => -{ - options.WaitForJobsToComplete = true; -}); - -builder.Services.UseBlogModule(databaseConnectionString); -builder.Services.UseMediaModule(databaseConnectionString, blobStorageRoot); +builder.Services.InitializeBlogModule(databaseConnectionString); +builder.Services.InitializeMediaModule(databaseConnectionString, blobStorageRoot); +// builder.Services.UseIdentityModule(databaseConnectionString); builder.Services.AddControllers(); @@ -36,6 +32,7 @@ builder.Services.AddCors(options => } ); }); + builder .Services.AddControllers() .AddJsonOptions(options => diff --git a/Femto.Common/Domain/DomainException.cs b/Femto.Common/Domain/DomainException.cs index b8f9665..39dba19 100644 --- a/Femto.Common/Domain/DomainException.cs +++ b/Femto.Common/Domain/DomainException.cs @@ -1,3 +1,7 @@ namespace Femto.Common.Domain; -public class DomainException(string message) : Exception(message); \ No newline at end of file +public class DomainException : Exception +{ + public DomainException(string message, Exception innerException) : base(message, innerException) {} + public DomainException(string message) : base(message) {} +} \ No newline at end of file diff --git a/Femto.Common/Domain/ICommand.cs b/Femto.Common/Domain/ICommand.cs new file mode 100644 index 0000000..39529a7 --- /dev/null +++ b/Femto.Common/Domain/ICommand.cs @@ -0,0 +1,7 @@ +using MediatR; + +namespace Femto.Common.Domain; + +public interface ICommand : IRequest; + +public interface ICommand : IRequest; \ No newline at end of file diff --git a/Femto.Common/Domain/IQuery.cs b/Femto.Common/Domain/IQuery.cs new file mode 100644 index 0000000..beca628 --- /dev/null +++ b/Femto.Common/Domain/IQuery.cs @@ -0,0 +1,5 @@ +using MediatR; + +namespace Femto.Common.Domain; + +public interface IQuery : IRequest; \ No newline at end of file diff --git a/Femto.Common/Femto.Common.csproj b/Femto.Common/Femto.Common.csproj index b123554..269ff83 100644 --- a/Femto.Common/Femto.Common.csproj +++ b/Femto.Common/Femto.Common.csproj @@ -8,9 +8,10 @@ - - - - + + + + + diff --git a/Femto.Modules.Blog/Infrastructure/DbConnection/DbConnectionFactory.cs b/Femto.Common/Infrastructure/DbConnection/DbConnectionFactory.cs similarity index 71% rename from Femto.Modules.Blog/Infrastructure/DbConnection/DbConnectionFactory.cs rename to Femto.Common/Infrastructure/DbConnection/DbConnectionFactory.cs index ee32f96..68ca3c0 100644 --- a/Femto.Modules.Blog/Infrastructure/DbConnection/DbConnectionFactory.cs +++ b/Femto.Common/Infrastructure/DbConnection/DbConnectionFactory.cs @@ -1,8 +1,7 @@ using System.Data; -using Microsoft.Data.SqlClient; using Npgsql; -namespace Femto.Modules.Blog.Infrastructure.DbConnection; +namespace Femto.Common.Infrastructure.DbConnection; public class DbConnectionFactory(string connectionString) : IDbConnectionFactory { diff --git a/Femto.Modules.Blog/Infrastructure/DbConnection/IDbConnectionFactory.cs b/Femto.Common/Infrastructure/DbConnection/IDbConnectionFactory.cs similarity index 51% rename from Femto.Modules.Blog/Infrastructure/DbConnection/IDbConnectionFactory.cs rename to Femto.Common/Infrastructure/DbConnection/IDbConnectionFactory.cs index 0c05f73..8887fc7 100644 --- a/Femto.Modules.Blog/Infrastructure/DbConnection/IDbConnectionFactory.cs +++ b/Femto.Common/Infrastructure/DbConnection/IDbConnectionFactory.cs @@ -1,7 +1,6 @@ using System.Data; -using Microsoft.Data.SqlClient; -namespace Femto.Modules.Blog.Infrastructure.DbConnection; +namespace Femto.Common.Infrastructure.DbConnection; public interface IDbConnectionFactory { diff --git a/Femto.Common/Infrastructure/Outbox/ClrTypenameMessageMapping.cs b/Femto.Common/Infrastructure/Outbox/ClrTypenameMessageMapping.cs new file mode 100644 index 0000000..68abd18 --- /dev/null +++ b/Femto.Common/Infrastructure/Outbox/ClrTypenameMessageMapping.cs @@ -0,0 +1,18 @@ +namespace Femto.Common.Infrastructure.Outbox; + +/// +/// A mapping based on the CLR type name +/// Brittle in the case of types being moved as they will then not be able to be handled by the outbox. but simple in implementation +/// +public class ClrTypenameMessageMapping : IOutboxMessageMapping +{ + public Type? GetTypeOfEvent(string eventName) + { + return Type.GetType(eventName); + } + + public string? GetEventName(Type eventType) + { + return eventType.AssemblyQualifiedName; + } +} \ No newline at end of file diff --git a/Femto.Common/Infrastructure/Outbox/IOutboxContext.cs b/Femto.Common/Infrastructure/Outbox/IOutboxContext.cs new file mode 100644 index 0000000..14a377a --- /dev/null +++ b/Femto.Common/Infrastructure/Outbox/IOutboxContext.cs @@ -0,0 +1,9 @@ +using Microsoft.EntityFrameworkCore; + +namespace Femto.Common.Infrastructure.Outbox; + +public interface IOutboxContext +{ + DbSet Outbox { get; } + +} \ No newline at end of file diff --git a/Femto.Common/Infrastructure/Outbox/IOutboxMessageHandler.cs b/Femto.Common/Infrastructure/Outbox/IOutboxMessageHandler.cs new file mode 100644 index 0000000..c58eee9 --- /dev/null +++ b/Femto.Common/Infrastructure/Outbox/IOutboxMessageHandler.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace Femto.Common.Infrastructure.Outbox; + +public interface IOutboxMessageHandler +{ + Task Publish(TNotification notification, CancellationToken executionContextCancellationToken); +} \ No newline at end of file diff --git a/Femto.Common/Infrastructure/Outbox/IOutboxMessageMapping.cs b/Femto.Common/Infrastructure/Outbox/IOutboxMessageMapping.cs new file mode 100644 index 0000000..a503225 --- /dev/null +++ b/Femto.Common/Infrastructure/Outbox/IOutboxMessageMapping.cs @@ -0,0 +1,7 @@ +namespace Femto.Common.Infrastructure.Outbox; + +public interface IOutboxMessageMapping +{ + Type? GetTypeOfEvent(string eventName); + string? GetEventName(Type eventType); +} \ No newline at end of file diff --git a/Femto.Common/Infrastructure/Outbox/Outbox.cs b/Femto.Common/Infrastructure/Outbox/Outbox.cs new file mode 100644 index 0000000..e2878d5 --- /dev/null +++ b/Femto.Common/Infrastructure/Outbox/Outbox.cs @@ -0,0 +1,29 @@ +using System.Reflection; +using System.Text.Json; +using Femto.Common.Attributes; +using Femto.Common.Integration; +using Microsoft.Extensions.DependencyInjection; + +namespace Femto.Common.Infrastructure.Outbox; + +public class Outbox(TContext context, IOutboxMessageMapping mapping) where TContext : IOutboxContext +{ + public async Task AddMessage( + Guid aggregateId, + TMessage message, + CancellationToken cancellationToken + ) + where TMessage : IIntegrationEvent + { + var eventName = mapping.GetEventName(typeof(TMessage)); + if (eventName is null) + throw new InvalidOperationException( + $"{typeof(TMessage).Name} does not have EventType attribute" + ); + + await context.Outbox.AddAsync( + new(message.EventId, aggregateId, eventName, JsonSerializer.Serialize(message)), + cancellationToken + ); + } +} diff --git a/Femto.Modules.Blog/Infrastructure/Integration/Outbox/OutboxEntry.cs b/Femto.Common/Infrastructure/Outbox/OutboxEntry.cs similarity index 94% rename from Femto.Modules.Blog/Infrastructure/Integration/Outbox/OutboxEntry.cs rename to Femto.Common/Infrastructure/Outbox/OutboxEntry.cs index a7cb47a..b34a4aa 100644 --- a/Femto.Modules.Blog/Infrastructure/Integration/Outbox/OutboxEntry.cs +++ b/Femto.Common/Infrastructure/Outbox/OutboxEntry.cs @@ -1,6 +1,6 @@ -namespace Femto.Modules.Blog.Infrastructure.Integration.Outbox; +namespace Femto.Common.Infrastructure.Outbox; -internal class OutboxEntry +public class OutboxEntry { private const int MaxRetries = 5; diff --git a/Femto.Common/Infrastructure/Outbox/OutboxEventNameRegistry.cs b/Femto.Common/Infrastructure/Outbox/OutboxEventNameRegistry.cs new file mode 100644 index 0000000..f699c26 --- /dev/null +++ b/Femto.Common/Infrastructure/Outbox/OutboxEventNameRegistry.cs @@ -0,0 +1,6 @@ +namespace Femto.Common.Infrastructure.Outbox; + +public static class OutboxEventNameRegistry +{ + +} \ No newline at end of file diff --git a/Femto.Modules.Blog/Infrastructure/Integration/Outbox/MailmanJob.cs b/Femto.Common/Infrastructure/Outbox/OutboxProcessor.cs similarity index 69% rename from Femto.Modules.Blog/Infrastructure/Integration/Outbox/MailmanJob.cs rename to Femto.Common/Infrastructure/Outbox/OutboxProcessor.cs index f5de019..46f72b6 100644 --- a/Femto.Modules.Blog/Infrastructure/Integration/Outbox/MailmanJob.cs +++ b/Femto.Common/Infrastructure/Outbox/OutboxProcessor.cs @@ -1,31 +1,37 @@ using System.Text.Json; -using Femto.Modules.Blog.Data; using MediatR; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Quartz; -namespace Femto.Modules.Blog.Infrastructure.Integration.Outbox; +namespace Femto.Common.Infrastructure.Outbox; [DisallowConcurrentExecution] -internal class MailmanJob( - Outbox outbox, - BlogContext context, - ILogger logger, - IMediator mediator +public class OutboxProcessor( + TContext context, + ILogger> logger, + IOutboxMessageMapping mapping, + IOutboxMessageHandler handler ) : IJob + where TContext : DbContext, IOutboxContext { public async Task Execute(IJobExecutionContext executionContext) { try { - var messages = await outbox.GetPendingMessages(executionContext.CancellationToken); + var now = DateTime.UtcNow; + var messages = await context + .Outbox.Where(message => message.Status == OutboxEntryStatus.Pending) + .Where(message => message.NextRetryAt == null || message.NextRetryAt <= now) + .OrderBy(message => message.CreatedAt) + .ToListAsync(executionContext.CancellationToken); logger.LogTrace("loaded {Count} outbox messages to process", messages.Count); foreach (var message in messages) { try { - var notificationType = OutboxMessageTypeRegistry.GetType(message.EventType); + var notificationType = mapping.GetTypeOfEvent(message.EventType); if (notificationType is null) { @@ -50,7 +56,7 @@ internal class MailmanJob( message.AggregateId ); - await mediator.Publish(notification, executionContext.CancellationToken); + await handler.Publish(notification, executionContext.CancellationToken); message.Succeed(); } diff --git a/Femto.Common/Infrastructure/Outbox/OutboxServiceExtension.cs b/Femto.Common/Infrastructure/Outbox/OutboxServiceExtension.cs new file mode 100644 index 0000000..d2bdfbc --- /dev/null +++ b/Femto.Common/Infrastructure/Outbox/OutboxServiceExtension.cs @@ -0,0 +1,41 @@ +using System.Reflection; +using Femto.Common.Attributes; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Quartz; + +namespace Femto.Common.Infrastructure.Outbox; + +public static class OutboxServiceExtension +{ + public static void AddOutbox( + this IServiceCollection services, + Func? contextFactory = null + ) + where TContext : DbContext, IOutboxContext + { + + services.AddSingleton(); + + services.AddScoped(c => + contextFactory?.Invoke(c) ?? c.GetRequiredService() + ); + + services.AddScoped>(); + + services.AddQuartz(q => + { + var jobKey = JobKey.Create(nameof(OutboxProcessor)); + + q.AddJob>(jobKey) + .AddTrigger(trigger => + trigger + .ForJob(jobKey) + .WithSimpleSchedule(schedule => + schedule.WithIntervalInSeconds(1).RepeatForever() + ) + ); + }); + } +} diff --git a/Femto.Modules.Blog/Infrastructure/PipelineBehaviours/SaveChangesPipelineBehaviour.cs b/Femto.Common/Infrastructure/SaveChangesPipelineBehaviour.cs similarity index 85% rename from Femto.Modules.Blog/Infrastructure/PipelineBehaviours/SaveChangesPipelineBehaviour.cs rename to Femto.Common/Infrastructure/SaveChangesPipelineBehaviour.cs index 82547c6..dc6719d 100644 --- a/Femto.Modules.Blog/Infrastructure/PipelineBehaviours/SaveChangesPipelineBehaviour.cs +++ b/Femto.Common/Infrastructure/SaveChangesPipelineBehaviour.cs @@ -1,12 +1,12 @@ using Femto.Common.Domain; -using Femto.Modules.Blog.Data; using MediatR; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace Femto.Modules.Blog.Infrastructure.PipelineBehaviours; +namespace Femto.Common.Infrastructure; -internal class SaveChangesPipelineBehaviour( - BlogContext context, +public class SaveChangesPipelineBehaviour( + DbContext context, IPublisher publisher, ILogger> logger ) : IPipelineBehavior @@ -22,9 +22,8 @@ internal class SaveChangesPipelineBehaviour( if (context.ChangeTracker.HasChanges()) { + await this.EmitDomainEvents(cancellationToken); - await EmitDomainEvents(cancellationToken); - logger.LogDebug("saving changes"); await context.SaveChangesAsync(cancellationToken); } diff --git a/Femto.Modules.Blog.Contracts/Module.cs b/Femto.Common/Util/EventTypeMapping.cs similarity index 61% rename from Femto.Modules.Blog.Contracts/Module.cs rename to Femto.Common/Util/EventTypeMapping.cs index 10fba4f..bc7a12d 100644 --- a/Femto.Modules.Blog.Contracts/Module.cs +++ b/Femto.Common/Util/EventTypeMapping.cs @@ -1,21 +1,24 @@ -using System.Collections.Immutable; using System.Reflection; using Femto.Common.Attributes; using MediatR; -namespace Femto.Modules.Blog.Contracts; +namespace Femto.Common.Util; -public static class Module +public static class EventTypeMapping { - public static IDictionary GetIntegrationEventTypes() + public static IDictionary GetEventTypeMapping(Assembly assembly) { var mapping = new Dictionary(); - - var types = typeof(Module).Assembly.GetTypes(); + + var types = assembly.GetTypes(); foreach (var type in types) { - if (!typeof(INotification).IsAssignableFrom(type) || type.IsAbstract || type.IsInterface) + if ( + !typeof(INotification).IsAssignableFrom(type) + || type.IsAbstract + || type.IsInterface + ) continue; var attribute = type.GetCustomAttribute(); @@ -31,4 +34,4 @@ public static class Module return mapping; } -} \ No newline at end of file +} diff --git a/Femto.Database/Migrations/20250425121459_Init.sql b/Femto.Database/Migrations/20250425121459_Init.sql index d89c301..bf09859 100644 --- a/Femto.Database/Migrations/20250425121459_Init.sql +++ b/Femto.Database/Migrations/20250425121459_Init.sql @@ -22,6 +22,9 @@ CREATE TABLE blog.post_media id uuid PRIMARY KEY, post_id uuid NOT NULL REFERENCES blog.post (id) ON DELETE CASCADE, url text NOT NULL, + type varchar(64), + width int, + height int, ordering int NOT NULL ); diff --git a/Femto.Modules.Authentication/Contracts/AuthenticationService.cs b/Femto.Modules.Authentication/Contracts/AuthenticationService.cs new file mode 100644 index 0000000..abd2901 --- /dev/null +++ b/Femto.Modules.Authentication/Contracts/AuthenticationService.cs @@ -0,0 +1,23 @@ +using Femto.Modules.Authentication.Data; +using Femto.Modules.Authentication.Models; + +namespace Femto.Modules.Authentication.Contracts; + +internal class AuthenticationService(AuthenticationContext context) : IAuthenticationService +{ + public async Task Register(string username, string password) + { + var user = new UserIdentity(username).WithPassword(password); + await context.AddAsync(user); + await context.SaveChangesAsync(); + + return new(user.Id, user.Username); + } + + public async Task Authenticate(string username, string password) + { + throw new NotImplementedException(); + } +} + +public class AuthenticationError(string message, Exception inner) : Exception(message, inner); diff --git a/Femto.Modules.Authentication/Contracts/IAuthenticationService.cs b/Femto.Modules.Authentication/Contracts/IAuthenticationService.cs new file mode 100644 index 0000000..78651b8 --- /dev/null +++ b/Femto.Modules.Authentication/Contracts/IAuthenticationService.cs @@ -0,0 +1,7 @@ +namespace Femto.Modules.Authentication.Contracts; + +public interface IAuthenticationService +{ + public Task Register(string username, string password); + public Task Authenticate(string username, string password); +} \ No newline at end of file diff --git a/Femto.Modules.Authentication/Contracts/UserInfo.cs b/Femto.Modules.Authentication/Contracts/UserInfo.cs new file mode 100644 index 0000000..014ebc1 --- /dev/null +++ b/Femto.Modules.Authentication/Contracts/UserInfo.cs @@ -0,0 +1,3 @@ +namespace Femto.Modules.Authentication.Contracts; + +public record UserInfo(Guid UserId, string Username); \ No newline at end of file diff --git a/Femto.Modules.Authentication/Data/AuthenticationContext.cs b/Femto.Modules.Authentication/Data/AuthenticationContext.cs new file mode 100644 index 0000000..6d1b1e4 --- /dev/null +++ b/Femto.Modules.Authentication/Data/AuthenticationContext.cs @@ -0,0 +1,16 @@ +using Femto.Common.Infrastructure.Outbox; +using Microsoft.EntityFrameworkCore; + +namespace Femto.Modules.Authentication.Data; + +internal class AuthenticationContext : DbContext, IOutboxContext +{ + public virtual DbSet Outbox { get; } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + builder.HasDefaultSchema("authn"); + builder.ApplyConfigurationsFromAssembly(typeof(AuthenticationContext).Assembly); + } +} \ No newline at end of file diff --git a/Femto.Modules.Authentication/Data/Configurations/UserIdentityTypeConfiguration.cs b/Femto.Modules.Authentication/Data/Configurations/UserIdentityTypeConfiguration.cs new file mode 100644 index 0000000..43d1650 --- /dev/null +++ b/Femto.Modules.Authentication/Data/Configurations/UserIdentityTypeConfiguration.cs @@ -0,0 +1,14 @@ +using Femto.Modules.Authentication.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Femto.Modules.Authentication.Data.Configurations; + +internal class UserIdentityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("user_identity"); + builder.OwnsOne(u => u.Password).WithOwner().HasForeignKey("user_id"); + } +} diff --git a/Femto.Modules.Authentication/Femto.Modules.Authentication.csproj b/Femto.Modules.Authentication/Femto.Modules.Authentication.csproj new file mode 100644 index 0000000..bc3de4c --- /dev/null +++ b/Femto.Modules.Authentication/Femto.Modules.Authentication.csproj @@ -0,0 +1,23 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/Femto.Modules.Authentication/Models/Events/UserWasCreatedEvent.cs b/Femto.Modules.Authentication/Models/Events/UserWasCreatedEvent.cs new file mode 100644 index 0000000..a3b44c7 --- /dev/null +++ b/Femto.Modules.Authentication/Models/Events/UserWasCreatedEvent.cs @@ -0,0 +1,5 @@ +using Femto.Common.Domain; + +namespace Femto.Modules.Authentication.Models.Events; + +internal record UserWasCreatedEvent(UserIdentity User) : DomainEvent; \ No newline at end of file diff --git a/Femto.Modules.Authentication/Models/UserIdentity.cs b/Femto.Modules.Authentication/Models/UserIdentity.cs new file mode 100644 index 0000000..e3e1d23 --- /dev/null +++ b/Femto.Modules.Authentication/Models/UserIdentity.cs @@ -0,0 +1,52 @@ +using System.Text; +using Femto.Common.Domain; +using Femto.Modules.Authentication.Contracts; +using Femto.Modules.Authentication.Models.Events; +using Geralt; + +namespace Femto.Modules.Authentication.Models; + +internal class UserIdentity : Entity +{ + public Guid Id { get; private set; } + + public string Username { get; private set; } + + public UserPassword Password { get; private set; } + + private UserIdentity() + { + + } + + public UserIdentity(string username) + { + this.Id = Guid.CreateVersion7(); + this.Username = username; + + this.AddDomainEvent(new UserWasCreatedEvent(this)); + } + + public UserIdentity WithPassword(string password) + { + this.SetPassword(password); + return this; + } + + public void SetPassword(string password) + { + var hash = new byte[128]; + try + { + Argon2id.ComputeHash(hash, Encoding.UTF8.GetBytes(password), 3, 67108864); + } + catch (Exception e) + { + throw new SetPasswordError("Failed to hash password", e); + } + + this.Password = new UserPassword(this.Id, hash, new byte[128]); + } +} + +public class SetPasswordError(string message, Exception inner) : DomainException(message, inner); diff --git a/Femto.Modules.Authentication/Models/UserPassword.cs b/Femto.Modules.Authentication/Models/UserPassword.cs new file mode 100644 index 0000000..0ddb247 --- /dev/null +++ b/Femto.Modules.Authentication/Models/UserPassword.cs @@ -0,0 +1,19 @@ +namespace Femto.Modules.Authentication.Models; + +internal class UserPassword +{ + public Guid Id { get; private set; } + + public byte[] Hash { get; private set; } + + public byte[] Salt { get; private set; } + + private UserPassword() {} + + public UserPassword(Guid id, byte[] hash, byte[] salt) + { + Id = id; + Hash = hash; + Salt = salt; + } +} \ No newline at end of file diff --git a/Femto.Modules.Authentication/Module.cs b/Femto.Modules.Authentication/Module.cs new file mode 100644 index 0000000..9f46401 --- /dev/null +++ b/Femto.Modules.Authentication/Module.cs @@ -0,0 +1,57 @@ +using Femto.Common.Infrastructure.Outbox; +using Femto.Modules.Authentication.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Femto.Modules.Authentication; + +public static class Module +{ + public static void UseIdentityModule(this IServiceCollection services, string connectionString) + { + services.AddDbContext( + builder => + { + builder.UseNpgsql(connectionString); + builder.UseSnakeCaseNamingConvention(); + }); + + services.AddMediatR(c => c.RegisterServicesFromAssembly(typeof(Module).Assembly)); + + services.AddDbContext(builder => + { + builder.UseNpgsql( + connectionString, + o => + { + o.MapEnum("outbox_status"); + } + ); + + builder.UseSnakeCaseNamingConvention(); + + var loggerFactory = LoggerFactory.Create(b => + { + // b.AddConsole(); + // .AddFilter( + // (category, level) => + // category == DbLoggerCategory.Database.Command.Name + // && level == LogLevel.Debug + // ); + }); + + builder.UseLoggerFactory(loggerFactory); + builder.EnableSensitiveDataLogging(); + }); + + // services.AddOutbox(); + + services.AddMediatR(c => + { + c.RegisterServicesFromAssembly(typeof(Module).Assembly); + }); + + services.AddTransient, Outbox>(); + } +} \ No newline at end of file diff --git a/Femto.Modules.Blog.Contracts/Femto.Modules.Blog.Contracts.csproj b/Femto.Modules.Blog.Contracts/Femto.Modules.Blog.Contracts.csproj deleted file mode 100644 index 828552d..0000000 --- a/Femto.Modules.Blog.Contracts/Femto.Modules.Blog.Contracts.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - net9.0 - enable - enable - - - - - - - - - - - - - - diff --git a/Femto.Modules.Blog/Application/BlogApplication.cs b/Femto.Modules.Blog/Application/BlogApplication.cs new file mode 100644 index 0000000..70ed624 --- /dev/null +++ b/Femto.Modules.Blog/Application/BlogApplication.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.Hosting; + +namespace Femto.Modules.Blog.Application; + +public class BlogApplication(IHost host) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await host.RunAsync(stoppingToken); + } +} \ No newline at end of file diff --git a/Femto.Modules.Blog/Data/BlogContext.cs b/Femto.Modules.Blog/Application/BlogContext.cs similarity index 74% rename from Femto.Modules.Blog/Data/BlogContext.cs rename to Femto.Modules.Blog/Application/BlogContext.cs index 5e7baaa..b90f59a 100644 --- a/Femto.Modules.Blog/Data/BlogContext.cs +++ b/Femto.Modules.Blog/Application/BlogContext.cs @@ -1,11 +1,10 @@ +using Femto.Common.Infrastructure.Outbox; using Femto.Modules.Blog.Domain.Posts; -using Femto.Modules.Blog.Infrastructure.Integration; -using Femto.Modules.Blog.Infrastructure.Integration.Outbox; using Microsoft.EntityFrameworkCore; -namespace Femto.Modules.Blog.Data; +namespace Femto.Modules.Blog.Application; -internal class BlogContext(DbContextOptions options) : DbContext(options) +internal class BlogContext(DbContextOptions options) : DbContext(options), IOutboxContext { public virtual DbSet Posts { get; set; } public virtual DbSet Outbox { get; set; } diff --git a/Femto.Modules.Blog/Application/BlogModule.cs b/Femto.Modules.Blog/Application/BlogModule.cs new file mode 100644 index 0000000..d96228c --- /dev/null +++ b/Femto.Modules.Blog/Application/BlogModule.cs @@ -0,0 +1,38 @@ +using Femto.Common.Domain; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Femto.Modules.Blog.Application; + +internal class BlogModule(IHost host) : IBlogModule +{ + public async Task PostCommand(ICommand command, CancellationToken cancellationToken = default) + { + using var scope = host.Services.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + await mediator.Send(command, cancellationToken); + } + + public async Task PostCommand( + ICommand command, + CancellationToken cancellationToken = default + ) + { + using var scope = host.Services.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + var response = await mediator.Send(command, cancellationToken); + return response; + } + + public async Task PostQuery( + IQuery query, + CancellationToken cancellationToken = default + ) + { + using var scope = host.Services.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + var response = await mediator.Send(query, cancellationToken); + return response; + } +} diff --git a/Femto.Modules.Blog/Application/BlogStartup.cs b/Femto.Modules.Blog/Application/BlogStartup.cs new file mode 100644 index 0000000..0ef415b --- /dev/null +++ b/Femto.Modules.Blog/Application/BlogStartup.cs @@ -0,0 +1,62 @@ +using Femto.Common.Infrastructure; +using Femto.Common.Infrastructure.DbConnection; +using Femto.Common.Infrastructure.Outbox; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Quartz; + +namespace Femto.Modules.Blog.Application; + +public static class BlogStartup +{ + public static void InitializeBlogModule( + this IServiceCollection rootContainer, + string connectionString + ) + { + var hostBuilder = Host.CreateDefaultBuilder(); + + hostBuilder.ConfigureServices(services => ConfigureServices(services, connectionString)); + + var host = hostBuilder.Build(); + + rootContainer.AddHostedService(services => new BlogApplication(host)); + + rootContainer.AddScoped(_ => new BlogModule(host)); + } + + private static void ConfigureServices(this IServiceCollection services, string connectionString) + { + services.AddTransient(_ => new DbConnectionFactory(connectionString)); + + services.AddDbContext(builder => + { + builder.UseNpgsql( + connectionString, + o => + { + o.MapEnum("outbox_status"); + } + ); + builder.UseSnakeCaseNamingConvention(); + var loggerFactory = LoggerFactory.Create(b => { }); + builder.UseLoggerFactory(loggerFactory); + builder.EnableSensitiveDataLogging(); + }); + + services.AddMediatR(c => + { + c.RegisterServicesFromAssembly(typeof(BlogStartup).Assembly); + }); + + services.AddScoped(s => s.GetRequiredService()); + + services.AddTransient( + typeof(IPipelineBehavior<,>), + typeof(SaveChangesPipelineBehaviour<,>) + ); + } +} diff --git a/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommand.cs b/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommand.cs new file mode 100644 index 0000000..1ada9a7 --- /dev/null +++ b/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommand.cs @@ -0,0 +1,8 @@ +using Femto.Common.Domain; + +namespace Femto.Modules.Blog.Application.Commands.CreatePost; + +public record CreatePostCommand(Guid AuthorId, string Content, IEnumerable Media) + : ICommand; + +public record CreatePostMedia(Guid MediaId, Uri Url, string? Type, int Order, int? Width, int? Height); diff --git a/Femto.Modules.Blog/Domain/Posts/Commands/CreatePost/CreatePostCommandHandler.cs b/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommandHandler.cs similarity index 50% rename from Femto.Modules.Blog/Domain/Posts/Commands/CreatePost/CreatePostCommandHandler.cs rename to Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommandHandler.cs index 3158a43..6867dbc 100644 --- a/Femto.Modules.Blog/Domain/Posts/Commands/CreatePost/CreatePostCommandHandler.cs +++ b/Femto.Modules.Blog/Application/Commands/CreatePost/CreatePostCommandHandler.cs @@ -1,7 +1,7 @@ -using Femto.Modules.Blog.Data; +using Femto.Modules.Blog.Domain.Posts; using MediatR; -namespace Femto.Modules.Blog.Domain.Posts.Commands.CreatePost; +namespace Femto.Modules.Blog.Application.Commands.CreatePost; internal class CreatePostCommandHandler(BlogContext context) : IRequestHandler @@ -11,7 +11,16 @@ internal class CreatePostCommandHandler(BlogContext context) var post = new Post( request.AuthorId, request.Content, - request.Media.Select((url, idx) => new PostMedia(Guid.CreateVersion7(), url, idx)).ToList() + request + .Media.Select(media => new PostMedia( + media.MediaId, + media.Url, + media.Type, + media.Order, + media.Width, + media.Height + )) + .ToList() ); await context.AddAsync(post, cancellationToken); diff --git a/Femto.Modules.Blog/Data/Configurations/OutboxEntryConfiguration.cs b/Femto.Modules.Blog/Application/Configurations/OutboxEntryConfiguration.cs similarity index 54% rename from Femto.Modules.Blog/Data/Configurations/OutboxEntryConfiguration.cs rename to Femto.Modules.Blog/Application/Configurations/OutboxEntryConfiguration.cs index a851ce8..854903e 100644 --- a/Femto.Modules.Blog/Data/Configurations/OutboxEntryConfiguration.cs +++ b/Femto.Modules.Blog/Application/Configurations/OutboxEntryConfiguration.cs @@ -1,9 +1,8 @@ -using Femto.Modules.Blog.Infrastructure.Integration; -using Femto.Modules.Blog.Infrastructure.Integration.Outbox; +using Femto.Common.Infrastructure.Outbox; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Femto.Modules.Blog.Data.Configurations; +namespace Femto.Modules.Blog.Application.Configurations; internal class OutboxEntryConfiguration : IEntityTypeConfiguration { @@ -11,7 +10,6 @@ internal class OutboxEntryConfiguration : IEntityTypeConfiguration { builder.ToTable("outbox"); - builder.Property(x => x.Payload) - .HasColumnType("jsonb"); + builder.Property(x => x.Payload).HasColumnType("jsonb"); } -} \ No newline at end of file +} diff --git a/Femto.Modules.Blog/Data/Configurations/PostConfiguration.cs b/Femto.Modules.Blog/Application/Configurations/PostConfiguration.cs similarity index 86% rename from Femto.Modules.Blog/Data/Configurations/PostConfiguration.cs rename to Femto.Modules.Blog/Application/Configurations/PostConfiguration.cs index 089a111..8cb2a64 100644 --- a/Femto.Modules.Blog/Data/Configurations/PostConfiguration.cs +++ b/Femto.Modules.Blog/Application/Configurations/PostConfiguration.cs @@ -2,7 +2,7 @@ using Femto.Modules.Blog.Domain.Posts; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Femto.Modules.Blog.Data.Configurations; +namespace Femto.Modules.Blog.Application.Configurations; internal class PostConfiguration : IEntityTypeConfiguration { diff --git a/Femto.Modules.Blog/Application/IBlogModule.cs b/Femto.Modules.Blog/Application/IBlogModule.cs new file mode 100644 index 0000000..941e1e2 --- /dev/null +++ b/Femto.Modules.Blog/Application/IBlogModule.cs @@ -0,0 +1,18 @@ +using Femto.Common.Domain; + +namespace Femto.Modules.Blog.Application; + +public interface IBlogModule +{ + Task PostCommand(ICommand command, CancellationToken cancellationToken = default); + + Task PostCommand( + ICommand command, + CancellationToken cancellationToken = default + ); + + Task PostQuery( + IQuery query, + CancellationToken cancellationToken = default + ); +} diff --git a/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/GetPostsQueryResult.cs b/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/GetPostsQueryResult.cs similarity index 51% rename from Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/GetPostsQueryResult.cs rename to Femto.Modules.Blog/Application/Queries/GetPosts/Dto/GetPostsQueryResult.cs index 9ad67e2..be8157a 100644 --- a/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/GetPostsQueryResult.cs +++ b/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/GetPostsQueryResult.cs @@ -1,3 +1,3 @@ -namespace Femto.Modules.Blog.Domain.Posts.Commands.GetPosts.Dto; +namespace Femto.Modules.Blog.Application.Queries.GetPosts.Dto; public record GetPostsQueryResult(IList Posts, Guid? Next); \ No newline at end of file diff --git a/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostAuthorDto.cs b/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostAuthorDto.cs new file mode 100644 index 0000000..2af929e --- /dev/null +++ b/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostAuthorDto.cs @@ -0,0 +1,3 @@ +namespace Femto.Modules.Blog.Application.Queries.GetPosts.Dto; + +public record PostAuthorDto(Guid AuthorId, string Username); \ No newline at end of file diff --git a/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostDto.cs b/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostDto.cs similarity index 65% rename from Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostDto.cs rename to Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostDto.cs index 765bb76..584d1f5 100644 --- a/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostDto.cs +++ b/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostDto.cs @@ -1,3 +1,3 @@ -namespace Femto.Modules.Blog.Domain.Posts.Commands.GetPosts.Dto; +namespace Femto.Modules.Blog.Application.Queries.GetPosts.Dto; public record PostDto(Guid PostId, string Text, IList Media, DateTimeOffset CreatedAt, PostAuthorDto Author); \ No newline at end of file diff --git a/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostMediaDto.cs b/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostMediaDto.cs new file mode 100644 index 0000000..48c8d81 --- /dev/null +++ b/Femto.Modules.Blog/Application/Queries/GetPosts/Dto/PostMediaDto.cs @@ -0,0 +1,3 @@ +namespace Femto.Modules.Blog.Application.Queries.GetPosts.Dto; + +public record PostMediaDto(Uri Url, int? Width, int? Height); \ No newline at end of file diff --git a/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/GetPostsQuery.cs b/Femto.Modules.Blog/Application/Queries/GetPosts/GetPostsQuery.cs similarity index 59% rename from Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/GetPostsQuery.cs rename to Femto.Modules.Blog/Application/Queries/GetPosts/GetPostsQuery.cs index 0322b29..f202c85 100644 --- a/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/GetPostsQuery.cs +++ b/Femto.Modules.Blog/Application/Queries/GetPosts/GetPostsQuery.cs @@ -1,21 +1,21 @@ -using Femto.Modules.Blog.Domain.Posts.Commands.GetPosts.Dto; -using MediatR; +using Femto.Common.Domain; +using Femto.Modules.Blog.Application.Queries.GetPosts.Dto; -namespace Femto.Modules.Blog.Domain.Posts.Commands.GetPosts; +namespace Femto.Modules.Blog.Application.Queries.GetPosts; -public class GetPostsQuery : IRequest +public class GetPostsQuery : IQuery { - - public string? Username { get; init; } public Guid? From { get; init; } public int Amount { get; init; } = 20; - public Guid? AuthorGuid { get; init; } + public Guid? AuthorId { get; init; } + public string? Author { get; set; } /// /// Default is to load in reverse chronological order /// TODO this is not exposed on the client as it probably wouldn't work that well /// public GetPostsDirection Direction { get; init; } = GetPostsDirection.Backward; + } public enum GetPostsDirection diff --git a/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/GetPostsQueryHandler.cs b/Femto.Modules.Blog/Application/Queries/GetPosts/GetPostsQueryHandler.cs similarity index 73% rename from Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/GetPostsQueryHandler.cs rename to Femto.Modules.Blog/Application/Queries/GetPosts/GetPostsQueryHandler.cs index 896b2d1..a731f66 100644 --- a/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/GetPostsQueryHandler.cs +++ b/Femto.Modules.Blog/Application/Queries/GetPosts/GetPostsQueryHandler.cs @@ -1,9 +1,9 @@ using Dapper; -using Femto.Modules.Blog.Domain.Posts.Commands.GetPosts.Dto; -using Femto.Modules.Blog.Infrastructure.DbConnection; +using Femto.Common.Infrastructure.DbConnection; +using Femto.Modules.Blog.Application.Queries.GetPosts.Dto; using MediatR; -namespace Femto.Modules.Blog.Domain.Posts.Commands.GetPosts; +namespace Femto.Modules.Blog.Application.Queries.GetPosts; public class GetPostsQueryHandler(IDbConnectionFactory connectionFactory) : IRequestHandler @@ -15,15 +15,9 @@ public class GetPostsQueryHandler(IDbConnectionFactory connectionFactory) { using var conn = connectionFactory.GetConnection(); - if (query.Username is not null && query.AuthorGuid is not null) - throw new ArgumentException( - "Cannot specify both username and authorGuid", - nameof(query) - ); - var orderBy = query.Direction is GetPostsDirection.Backward ? "desc" : "asc"; var pageFilter = query.Direction is GetPostsDirection.Backward ? "<=" : ">="; - + // lang=sql var sql = $$""" with page as ( @@ -40,6 +34,8 @@ public class GetPostsQueryHandler(IDbConnectionFactory connectionFactory) page.id as PostId, page.content as Content, blog.post_media.url as MediaUrl, + blog.post_media.width as MediaWidth, + blog.post_media.height as MediaHeight, page.posted_on as PostedOn, page.Username, page.AuthorId @@ -52,8 +48,8 @@ public class GetPostsQueryHandler(IDbConnectionFactory connectionFactory) sql, new { - username = query.Username, - authorGuid = query.AuthorGuid, + username = query.Author, + authorGuid = query.AuthorId, cursor = query.From, // load an extra one to take for the curst amount = query.Amount + 1, @@ -68,9 +64,20 @@ public class GetPostsQueryHandler(IDbConnectionFactory connectionFactory) var postId = group.Key; var post = group.First(); var media = group - .Select(row => row.MediaUrl) - .OfType() - .Select(url => new PostMediaDto(new Uri(url))) + .Select(row => + { + if (row.MediaUrl is not null) + { + return new PostMediaDto( + new Uri(row.MediaUrl), + row.MediaHeight, + row.MediaHeight + ); + } + else + return null; + }) + .OfType() .ToList(); return new PostDto( postId, @@ -92,6 +99,9 @@ public class GetPostsQueryHandler(IDbConnectionFactory connectionFactory) public Guid PostId { get; set; } public string Content { get; set; } public string? MediaUrl { get; set; } + public string? MediaType { get; set; } + public int? MediaWidth { get; set; } + public int? MediaHeight { get; set; } public DateTimeOffset PostedOn { get; set; } public Guid AuthorId { get; set; } public string Username { get; set; } diff --git a/Femto.Modules.Blog/Domain/Authors/Author.cs b/Femto.Modules.Blog/Domain/Authors/Author.cs deleted file mode 100644 index 7f796ce..0000000 --- a/Femto.Modules.Blog/Domain/Authors/Author.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Femto.Modules.Blog.Domain.Authors; - -internal class Author -{ - public string Id { get; private set; } = null!; - public string Name { get; private set; } = null!; -} \ No newline at end of file diff --git a/Femto.Modules.Blog/Domain/Posts/Commands/CreatePost/CreatePostCommand.cs b/Femto.Modules.Blog/Domain/Posts/Commands/CreatePost/CreatePostCommand.cs deleted file mode 100644 index d99bf88..0000000 --- a/Femto.Modules.Blog/Domain/Posts/Commands/CreatePost/CreatePostCommand.cs +++ /dev/null @@ -1,6 +0,0 @@ -using MediatR; - -namespace Femto.Modules.Blog.Domain.Posts.Commands.CreatePost; - -public record CreatePostCommand(Guid AuthorId, string Content, IEnumerable Media) - : IRequest; \ No newline at end of file diff --git a/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostAuthorDto.cs b/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostAuthorDto.cs deleted file mode 100644 index b057fe8..0000000 --- a/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostAuthorDto.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Femto.Modules.Blog.Domain.Posts.Commands.GetPosts.Dto; - -public record PostAuthorDto(Guid AuthorId, string Username); \ No newline at end of file diff --git a/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostMediaDto.cs b/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostMediaDto.cs deleted file mode 100644 index 568cb27..0000000 --- a/Femto.Modules.Blog/Domain/Posts/Commands/GetPosts/Dto/PostMediaDto.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Femto.Modules.Blog.Domain.Posts.Commands.GetPosts.Dto; - -public record PostMediaDto(Uri Url); \ No newline at end of file diff --git a/Femto.Modules.Blog/Domain/Posts/Handlers/PostCreatedHandler.cs b/Femto.Modules.Blog/Domain/Posts/Handlers/PostCreatedHandler.cs deleted file mode 100644 index d93e5cc..0000000 --- a/Femto.Modules.Blog/Domain/Posts/Handlers/PostCreatedHandler.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Femto.Modules.Blog.Contracts.Events; -using Femto.Modules.Blog.Domain.Posts.Events; -using Femto.Modules.Blog.Infrastructure.Integration; -using Femto.Modules.Blog.Infrastructure.Integration.Outbox; -using MediatR; - -namespace Femto.Modules.Blog.Domain.Posts.Handlers; - -internal class PostCreatedHandler(Outbox outbox) : INotificationHandler -{ - public async Task Handle(PostCreated notification, CancellationToken cancellationToken) - { - var post = notification.Post; - await outbox.AddMessage( - post.Id, - new PostCreatedIntegrationEvent(Guid.CreateVersion7(), post.Id, post.Media.Select(m => m.Id)), - cancellationToken - ); - } -} diff --git a/Femto.Modules.Blog/Domain/Posts/PostMedia.cs b/Femto.Modules.Blog/Domain/Posts/PostMedia.cs index 927dfff..a9d4c5c 100644 --- a/Femto.Modules.Blog/Domain/Posts/PostMedia.cs +++ b/Femto.Modules.Blog/Domain/Posts/PostMedia.cs @@ -4,14 +4,20 @@ internal class PostMedia { public Guid Id { get; private set; } public Uri Url { get; private set; } + public string? Type { get; private set; } public int Ordering { get; private set; } + public int? Width { get; private set; } + public int? Height { get; private set; } private PostMedia() {} - public PostMedia(Guid id, Uri url, int ordering) + public PostMedia(Guid id, Uri url, string type, int ordering, int? width, int? height) { this.Id = id; this.Url = url; + this.Type = type; this.Ordering = ordering; + this.Width = width; + this.Height = height; } } \ No newline at end of file diff --git a/Femto.Modules.Blog.Contracts/Events/PostCreatedIntegrationEvent.cs b/Femto.Modules.Blog/Events/PostCreatedIntegrationEvent.cs similarity index 82% rename from Femto.Modules.Blog.Contracts/Events/PostCreatedIntegrationEvent.cs rename to Femto.Modules.Blog/Events/PostCreatedIntegrationEvent.cs index 0c4bf4f..ef339bb 100644 --- a/Femto.Modules.Blog.Contracts/Events/PostCreatedIntegrationEvent.cs +++ b/Femto.Modules.Blog/Events/PostCreatedIntegrationEvent.cs @@ -1,7 +1,7 @@ using Femto.Common.Attributes; using Femto.Common.Integration; -namespace Femto.Modules.Blog.Contracts.Events; +namespace Femto.Modules.Blog.Events; [EventType("post.created")] public record PostCreatedIntegrationEvent(Guid EventId, Guid PostId, IEnumerable MediaIds) diff --git a/Femto.Modules.Blog/Femto.Modules.Blog.csproj b/Femto.Modules.Blog/Femto.Modules.Blog.csproj index 94383f1..7944afa 100644 --- a/Femto.Modules.Blog/Femto.Modules.Blog.csproj +++ b/Femto.Modules.Blog/Femto.Modules.Blog.csproj @@ -9,12 +9,15 @@ + + + @@ -41,7 +44,6 @@ - diff --git a/Femto.Modules.Blog/Handlers/PostCreatedIntegrationEventHandler.cs b/Femto.Modules.Blog/Handlers/PostCreatedIntegrationEventHandler.cs index cc4cb7f..c9c0be0 100644 --- a/Femto.Modules.Blog/Handlers/PostCreatedIntegrationEventHandler.cs +++ b/Femto.Modules.Blog/Handlers/PostCreatedIntegrationEventHandler.cs @@ -1,4 +1,4 @@ -using Femto.Modules.Blog.Contracts.Events; +using Femto.Modules.Blog.Events; using MediatR; namespace Femto.Modules.Blog.Handlers; diff --git a/Femto.Modules.Blog/Infrastructure/Integration/Outbox/Outbox.cs b/Femto.Modules.Blog/Infrastructure/Integration/Outbox/Outbox.cs deleted file mode 100644 index 851a687..0000000 --- a/Femto.Modules.Blog/Infrastructure/Integration/Outbox/Outbox.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Reflection; -using System.Text.Json; -using Femto.Common.Attributes; -using Femto.Common.Integration; -using Femto.Modules.Blog.Data; -using Microsoft.EntityFrameworkCore; - -namespace Femto.Modules.Blog.Infrastructure.Integration.Outbox; - -internal class Outbox(BlogContext context) -{ - public async Task AddMessage(Guid aggregateId, TMessage message, CancellationToken cancellationToken) - where TMessage : IIntegrationEvent - { - var eventType = typeof(TMessage).GetCustomAttribute(); - if (eventType is null) - throw new InvalidOperationException($"{typeof(TMessage).Name} does not have EventType attribute"); - - await context.Outbox.AddAsync( - new( - message.EventId, - aggregateId, - eventType.Name, - JsonSerializer.Serialize(message) - ), - cancellationToken - ); - } - - public async Task> GetPendingMessages(CancellationToken cancellationToken) - { - var now = DateTime.UtcNow; - - return await context - .Outbox.Where(message => message.Status == OutboxEntryStatus.Pending) - .Where(message => message.NextRetryAt == null || message.NextRetryAt <= now) - .OrderBy(message => message.CreatedAt) - .ToListAsync(cancellationToken); - } -} diff --git a/Femto.Modules.Blog/Infrastructure/Integration/Outbox/OutboxMessageTypeRegistry.cs b/Femto.Modules.Blog/Infrastructure/Integration/Outbox/OutboxMessageTypeRegistry.cs deleted file mode 100644 index 96b0179..0000000 --- a/Femto.Modules.Blog/Infrastructure/Integration/Outbox/OutboxMessageTypeRegistry.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Concurrent; -using System.Collections.Immutable; - -namespace Femto.Modules.Blog.Infrastructure.Integration.Outbox; - -internal static class OutboxMessageTypeRegistry -{ - private static readonly ConcurrentDictionary Mapping = new(); - - public static void RegisterOutboxMessages(IImmutableDictionary mapping) - { - foreach (var (key, value) in mapping) - { - Mapping.TryAdd(key, value); - } - } - - public static Type? GetType(string eventName) => Mapping.GetValueOrDefault(eventName); - -} \ No newline at end of file diff --git a/Femto.Modules.Blog/Module.cs b/Femto.Modules.Blog/Module.cs deleted file mode 100644 index e32319b..0000000 --- a/Femto.Modules.Blog/Module.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Collections.Immutable; -using Femto.Modules.Blog.Data; -using Femto.Modules.Blog.Infrastructure; -using Femto.Modules.Blog.Infrastructure.DbConnection; -using Femto.Modules.Blog.Infrastructure.Integration; -using Femto.Modules.Blog.Infrastructure.Integration.Outbox; -using Femto.Modules.Blog.Infrastructure.PipelineBehaviours; -using MediatR; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Quartz; - -namespace Femto.Modules.Blog; - -public static class Module -{ - public static void UseBlogModule(this IServiceCollection services, string connectionString) - { - OutboxMessageTypeRegistry.RegisterOutboxMessages( - Contracts.Module.GetIntegrationEventTypes().ToImmutableDictionary() - ); - - services.AddDbContext(builder => - { - builder.UseNpgsql( - connectionString, - o => - { - o.MapEnum("outbox_status"); - } - ); - ; - builder.UseSnakeCaseNamingConvention(); - - var loggerFactory = LoggerFactory.Create(b => - { - // b.AddConsole(); - // .AddFilter( - // (category, level) => - // category == DbLoggerCategory.Database.Command.Name - // && level == LogLevel.Debug - // ); - }); - - builder.UseLoggerFactory(loggerFactory); - builder.EnableSensitiveDataLogging(); - }); - - services.AddMediatR(c => - { - c.RegisterServicesFromAssembly(typeof(Module).Assembly); - }); - - services.AddQuartz(q => - { - q.AddMailmanJob(); - }); - - services.AddQuartzHostedService(options => - { - options.WaitForJobsToComplete = true; - }); - - services.SetupMediatrPipeline(); - - services.AddTransient(); - services.AddTransient(_ => new DbConnectionFactory(connectionString)); - } - - private static void SetupMediatrPipeline(this IServiceCollection services) - { - services.AddTransient( - typeof(IPipelineBehavior<,>), - typeof(SaveChangesPipelineBehaviour<,>) - ); - } - - private static void AddMailmanJob(this IServiceCollectionQuartzConfigurator q) - { - var jobKey = JobKey.Create(nameof(MailmanJob)); - - q.AddJob(jobKey) - .AddTrigger(trigger => - trigger - .ForJob(jobKey) - .WithSimpleSchedule(schedule => - schedule.WithIntervalInSeconds(1).RepeatForever() - ) - ); - } -} diff --git a/Femto.Modules.Media/Application/IMediaModule.cs b/Femto.Modules.Media/Application/IMediaModule.cs new file mode 100644 index 0000000..6d6d14b --- /dev/null +++ b/Femto.Modules.Media/Application/IMediaModule.cs @@ -0,0 +1,9 @@ +using Femto.Common.Domain; + +namespace Femto.Modules.Media.Application; + +public interface IMediaModule +{ + Task PostCommand(ICommand command); + Task PostQuery(IQuery query); +} \ No newline at end of file diff --git a/Femto.Modules.Media/Application/MediaApplication.cs b/Femto.Modules.Media/Application/MediaApplication.cs new file mode 100644 index 0000000..f2ca715 --- /dev/null +++ b/Femto.Modules.Media/Application/MediaApplication.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.Hosting; + +namespace Femto.Modules.Media.Application; + +public class MediaApplication(IHost host) : BackgroundService +{ + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + await host.RunAsync(stoppingToken); + } +} \ No newline at end of file diff --git a/Femto.Modules.Media/Application/MediaModule.cs b/Femto.Modules.Media/Application/MediaModule.cs new file mode 100644 index 0000000..b22a1d8 --- /dev/null +++ b/Femto.Modules.Media/Application/MediaModule.cs @@ -0,0 +1,25 @@ +using Femto.Common.Domain; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Femto.Modules.Media.Application; + +public class MediaModule(IHost host) : IMediaModule +{ + public async Task PostCommand(ICommand command) + { + using var scope = host.Services.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + var response = await mediator.Send(command); + return response; + } + + public async Task PostQuery(IQuery query) + { + using var scope = host.Services.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + var response = await mediator.Send(query); + return response; + } +} \ No newline at end of file diff --git a/Femto.Modules.Media/Application/Startup.cs b/Femto.Modules.Media/Application/Startup.cs new file mode 100644 index 0000000..bb6e41d --- /dev/null +++ b/Femto.Modules.Media/Application/Startup.cs @@ -0,0 +1,30 @@ +using Femto.Modules.Media.Data; +using Femto.Modules.Media.Infrastructure; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Femto.Modules.Media.Application; + +public static class Startup +{ + public static void InitializeMediaModule(this IServiceCollection rootContainer, string connectionString, string storageRoot) + { + var hostBuilder = Host.CreateDefaultBuilder(); + + hostBuilder.ConfigureServices(services => + { + services.AddDbContext(builder => + { + builder.UseNpgsql(connectionString); + builder.UseSnakeCaseNamingConvention(); + }); + services.AddTransient(s => new FilesystemStorageProvider(storageRoot)); + services.AddMediatR(c => c.RegisterServicesFromAssembly(typeof(Startup).Assembly)); + }); + + var host = hostBuilder.Build(); + + rootContainer.AddTransient(_ => new MediaModule(host)); + } +} diff --git a/Femto.Modules.Media/Femto.Modules.Media.csproj b/Femto.Modules.Media/Femto.Modules.Media.csproj index 5ad52a1..c03581e 100644 --- a/Femto.Modules.Media/Femto.Modules.Media.csproj +++ b/Femto.Modules.Media/Femto.Modules.Media.csproj @@ -20,9 +20,11 @@ + + diff --git a/Femto.Modules.Media/Module.cs b/Femto.Modules.Media/Module.cs deleted file mode 100644 index 124d7ed..0000000 --- a/Femto.Modules.Media/Module.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Femto.Modules.Media.Data; -using Femto.Modules.Media.Infrastructure; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Femto.Modules.Media; - -public static class Module -{ - public static void UseMediaModule(this IServiceCollection services, string connectionString, string storageRoot) - { - services.AddDbContext(builder => - { - builder.UseNpgsql(connectionString); - builder.UseSnakeCaseNamingConvention(); - }); - - services.AddTransient(s => new FilesystemStorageProvider(storageRoot)); - services.AddMediatR(c => c.RegisterServicesFromAssembly(typeof(Module).Assembly)); - } -} diff --git a/FemtoBackend.sln b/FemtoBackend.sln index 1867584..85c939a 100644 --- a/FemtoBackend.sln +++ b/FemtoBackend.sln @@ -10,10 +10,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Femto.Docs", "Femto.Docs\Fe EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Femto.Common", "Femto.Common\Femto.Common.csproj", "{52A086BD-AF2F-463F-A6A9-5FC1343C0E28}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Femto.Modules.Blog.Contracts", "Femto.Modules.Blog.Contracts\Femto.Modules.Blog.Contracts.csproj", "{35C42036-D53B-42EB-9A1C-B540E55F4FD0}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Femto.Modules.Media", "Femto.Modules.Media\Femto.Modules.Media.csproj", "{AC9FBF11-FF29-4A80-B9EA-AFDF1E3DCA80}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Femto.Modules.Authentication", "Femto.Modules.Authentication\Femto.Modules.Authentication.csproj", "{7E138EF6-E075-4896-93C0-923024F0CA78}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -40,14 +40,14 @@ Global {52A086BD-AF2F-463F-A6A9-5FC1343C0E28}.Debug|Any CPU.Build.0 = Debug|Any CPU {52A086BD-AF2F-463F-A6A9-5FC1343C0E28}.Release|Any CPU.ActiveCfg = Release|Any CPU {52A086BD-AF2F-463F-A6A9-5FC1343C0E28}.Release|Any CPU.Build.0 = Release|Any CPU - {35C42036-D53B-42EB-9A1C-B540E55F4FD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {35C42036-D53B-42EB-9A1C-B540E55F4FD0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {35C42036-D53B-42EB-9A1C-B540E55F4FD0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {35C42036-D53B-42EB-9A1C-B540E55F4FD0}.Release|Any CPU.Build.0 = Release|Any CPU {AC9FBF11-FF29-4A80-B9EA-AFDF1E3DCA80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {AC9FBF11-FF29-4A80-B9EA-AFDF1E3DCA80}.Debug|Any CPU.Build.0 = Debug|Any CPU {AC9FBF11-FF29-4A80-B9EA-AFDF1E3DCA80}.Release|Any CPU.ActiveCfg = Release|Any CPU {AC9FBF11-FF29-4A80-B9EA-AFDF1E3DCA80}.Release|Any CPU.Build.0 = Release|Any CPU + {7E138EF6-E075-4896-93C0-923024F0CA78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7E138EF6-E075-4896-93C0-923024F0CA78}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7E138EF6-E075-4896-93C0-923024F0CA78}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7E138EF6-E075-4896-93C0-923024F0CA78}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution EndGlobalSection