init
This commit is contained in:
commit
ab2e20f7e1
72 changed files with 2000 additions and 0 deletions
18
Femto.Modules.Blog/Data/BlogContext.cs
Normal file
18
Femto.Modules.Blog/Data/BlogContext.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using Femto.Modules.Blog.Domain.Posts;
|
||||
using Femto.Modules.Blog.Infrastructure.Integration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Femto.Modules.Blog.Data;
|
||||
|
||||
internal class BlogContext(DbContextOptions<BlogContext> options) : DbContext(options)
|
||||
{
|
||||
public virtual DbSet<Post> Posts { get; set; }
|
||||
public virtual DbSet<OutboxEntry> Outbox { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder builder)
|
||||
{
|
||||
base.OnModelCreating(builder);
|
||||
builder.HasDefaultSchema("blog");
|
||||
builder.ApplyConfigurationsFromAssembly(typeof(BlogContext).Assembly);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
using Femto.Modules.Blog.Infrastructure.Integration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Femto.Modules.Blog.Data.Configurations;
|
||||
|
||||
internal class OutboxEntryConfiguration : IEntityTypeConfiguration<OutboxEntry>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<OutboxEntry> builder)
|
||||
{
|
||||
builder.ToTable("outbox");
|
||||
|
||||
builder.Property(x => x.Payload)
|
||||
.HasColumnType("jsonb");
|
||||
}
|
||||
}
|
14
Femto.Modules.Blog/Data/Configurations/PostConfiguration.cs
Normal file
14
Femto.Modules.Blog/Data/Configurations/PostConfiguration.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
using Femto.Modules.Blog.Domain.Posts;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Femto.Modules.Blog.Data.Configurations;
|
||||
|
||||
internal class PostConfiguration : IEntityTypeConfiguration<Post>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Post> table)
|
||||
{
|
||||
table.ToTable("post");
|
||||
table.OwnsMany(post => post.Media).WithOwner();
|
||||
}
|
||||
}
|
7
Femto.Modules.Blog/Domain/Authors/Author.cs
Normal file
7
Femto.Modules.Blog/Domain/Authors/Author.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace Femto.Modules.Blog.Domain.Authors;
|
||||
|
||||
internal class Author
|
||||
{
|
||||
public string Id { get; private set; } = null!;
|
||||
public string Name { get; private set; } = null!;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
using MediatR;
|
||||
|
||||
namespace Femto.Modules.Blog.Domain.Posts.Commands.CreatePost;
|
||||
|
||||
public record CreatePostCommand(Guid AuthorId, string Content, IEnumerable<Uri> Media)
|
||||
: IRequest<Guid>;
|
|
@ -0,0 +1,21 @@
|
|||
using Femto.Modules.Blog.Data;
|
||||
using MediatR;
|
||||
|
||||
namespace Femto.Modules.Blog.Domain.Posts.Commands.CreatePost;
|
||||
|
||||
internal class CreatePostCommandHandler(BlogContext context)
|
||||
: IRequestHandler<CreatePostCommand, Guid>
|
||||
{
|
||||
public async Task<Guid> Handle(CreatePostCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var post = new Post(
|
||||
request.AuthorId,
|
||||
request.Content,
|
||||
request.Media.Select((url, idx) => new PostMedia(Guid.CreateVersion7(), url, idx)).ToList()
|
||||
);
|
||||
|
||||
await context.AddAsync(post, cancellationToken);
|
||||
|
||||
return post.Id;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
using Femto.Modules.Blog.Contracts.Dto;
|
||||
using MediatR;
|
||||
|
||||
namespace Femto.Modules.Blog.Domain.Posts.Commands.GetAuthorPosts;
|
||||
|
||||
public record GetAuthorPostsQuery(Guid AuthorId, Guid? Cursor, int? Count) : IRequest<IList<GetAuthorPostsDto>>;
|
|
@ -0,0 +1,61 @@
|
|||
using Dapper;
|
||||
using Femto.Modules.Blog.Contracts.Dto;
|
||||
using Femto.Modules.Blog.Infrastructure.DbConnection;
|
||||
using MediatR;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace Femto.Modules.Blog.Domain.Posts.Commands.GetAuthorPosts;
|
||||
|
||||
public class GetAuthorPostsQueryHandler(IDbConnectionFactory connectionFactory)
|
||||
: IRequestHandler<GetAuthorPostsQuery, IList<GetAuthorPostsDto>>
|
||||
{
|
||||
public async Task<IList<GetAuthorPostsDto>> Handle(
|
||||
GetAuthorPostsQuery query,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
using var conn = connectionFactory.GetConnection();
|
||||
|
||||
var sql = $$"""
|
||||
with post_page as (
|
||||
select * from blog.post
|
||||
where blog.post.author_id = @authorId
|
||||
and (@cursor is null or blog.post.id < @cursor)
|
||||
order by blog.post.id desc
|
||||
limit @count
|
||||
)
|
||||
select
|
||||
p.id as PostId,
|
||||
p.content as Content,
|
||||
pm.url as MediaUrl
|
||||
from post_page p
|
||||
left join blog.post_media pm on pm.post_id = p.id
|
||||
order by p.id desc
|
||||
""";
|
||||
|
||||
var result = await conn.QueryAsync<QueryResult>(
|
||||
sql,
|
||||
new { authorId = query.AuthorId, cursor = query.Cursor, count = query.Count }
|
||||
);
|
||||
|
||||
return result
|
||||
.GroupBy(row => row.PostId)
|
||||
.Select(group => new GetAuthorPostsDto(
|
||||
group.Key,
|
||||
group.First().Content,
|
||||
group
|
||||
.Select(row => row.MediaUrl)
|
||||
.OfType<string>()
|
||||
.Select(url => new GetAuthorPostsMediaDto(new Uri(url)))
|
||||
.ToList()
|
||||
))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
internal class QueryResult
|
||||
{
|
||||
public Guid PostId { get; set; }
|
||||
public string Content { get; set; }
|
||||
public string? MediaUrl { get; set; }
|
||||
}
|
||||
}
|
5
Femto.Modules.Blog/Domain/Posts/Events/PostCreated.cs
Normal file
5
Femto.Modules.Blog/Domain/Posts/Events/PostCreated.cs
Normal file
|
@ -0,0 +1,5 @@
|
|||
using Femto.Common.Domain;
|
||||
|
||||
namespace Femto.Modules.Blog.Domain.Posts.Events;
|
||||
|
||||
internal record PostCreated(Post Post) : DomainEvent;
|
|
@ -0,0 +1,19 @@
|
|||
using Femto.Modules.Blog.Contracts.Events;
|
||||
using Femto.Modules.Blog.Domain.Posts.Events;
|
||||
using Femto.Modules.Blog.Infrastructure.Integration;
|
||||
using MediatR;
|
||||
|
||||
namespace Femto.Modules.Blog.Domain.Posts.Handlers;
|
||||
|
||||
internal class PostCreatedHandler(Outbox outbox) : INotificationHandler<PostCreated>
|
||||
{
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
25
Femto.Modules.Blog/Domain/Posts/Post.cs
Normal file
25
Femto.Modules.Blog/Domain/Posts/Post.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
using Femto.Common.Domain;
|
||||
using Femto.Modules.Blog.Domain.Posts.Events;
|
||||
using Femto.Modules.Blog.Domain.Posts.Rules;
|
||||
|
||||
namespace Femto.Modules.Blog.Domain.Posts;
|
||||
|
||||
internal class Post : Entity
|
||||
{
|
||||
public Guid Id { get; private set; }
|
||||
public Guid AuthorId { get; private set; }
|
||||
public string Content { get; private set; } = null!;
|
||||
public IList<PostMedia> Media { get; private set; }
|
||||
|
||||
private Post() { }
|
||||
|
||||
public Post(Guid authorId,string content, IList<PostMedia> media)
|
||||
{
|
||||
this.Id = Guid.CreateVersion7();
|
||||
this.AuthorId = authorId;
|
||||
this.Content = content;
|
||||
this.Media = media;
|
||||
|
||||
this.AddDomainEvent(new PostCreated(this));
|
||||
}
|
||||
}
|
17
Femto.Modules.Blog/Domain/Posts/PostMedia.cs
Normal file
17
Femto.Modules.Blog/Domain/Posts/PostMedia.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
namespace Femto.Modules.Blog.Domain.Posts;
|
||||
|
||||
internal class PostMedia
|
||||
{
|
||||
public Guid Id { get; private set; }
|
||||
public Uri Url { get; private set; }
|
||||
public int Ordering { get; private set; }
|
||||
|
||||
private PostMedia() {}
|
||||
|
||||
public PostMedia(Guid id, Uri url, int ordering)
|
||||
{
|
||||
this.Id = id;
|
||||
this.Url = url;
|
||||
this.Ordering = ordering;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using Femto.Common.Domain;
|
||||
|
||||
namespace Femto.Modules.Blog.Domain.Posts.Rules;
|
||||
|
||||
internal class PostMustHaveSomeMediaRule(int mediaCount) : IRule
|
||||
{
|
||||
public bool Check() => true || mediaCount > 0;
|
||||
public string Message => "Post must contain some media";
|
||||
}
|
47
Femto.Modules.Blog/Femto.Modules.Blog.csproj
Normal file
47
Femto.Modules.Blog/Femto.Modules.Blog.csproj
Normal file
|
@ -0,0 +1,47 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AssemblyName>Femto.Modules.Blog</AssemblyName>
|
||||
<RootNamespace>Femto.Modules.Blog</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
<PackageReference Include="EFCore.NamingConventions" Version="9.0.0" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore" Version="2.3.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.4" />
|
||||
<PackageReference Include="Quartz" Version="3.14.0" />
|
||||
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.14.0" />
|
||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.14.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="MediatR">
|
||||
<HintPath>..\..\..\..\.nuget\packages\mediatr\12.5.0\lib\net6.0\MediatR.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.Data.SqlClient">
|
||||
<HintPath>..\..\..\..\.nuget\packages\microsoft.data.sqlclient\6.0.1\ref\net9.0\Microsoft.Data.SqlClient.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Npgsql">
|
||||
<HintPath>..\..\..\..\.nuget\packages\npgsql\9.0.3\lib\net8.0\Npgsql.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Npgsql.EntityFrameworkCore.PostgreSQL">
|
||||
<HintPath>..\..\..\..\.nuget\packages\npgsql.entityframeworkcore.postgresql\9.0.4\lib\net8.0\Npgsql.EntityFrameworkCore.PostgreSQL.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Domain\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Femto.Common\Femto.Common.csproj" />
|
||||
<ProjectReference Include="..\Femto.Modules.Blog.Contracts\Femto.Modules.Blog.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,12 @@
|
|||
using Femto.Modules.Blog.Contracts.Events;
|
||||
using MediatR;
|
||||
|
||||
namespace Femto.Modules.Blog.Handlers;
|
||||
|
||||
public class PostCreatedIntegrationEventHandler : INotificationHandler<PostCreatedIntegrationEvent>
|
||||
{
|
||||
public async Task Handle(PostCreatedIntegrationEvent notification, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
using System.Data;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Npgsql;
|
||||
|
||||
namespace Femto.Modules.Blog.Infrastructure.DbConnection;
|
||||
|
||||
public class DbConnectionFactory(string connectionString) : IDbConnectionFactory
|
||||
{
|
||||
public IDbConnection GetConnection()
|
||||
{
|
||||
return new NpgsqlConnection(connectionString);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using System.Data;
|
||||
using Microsoft.Data.SqlClient;
|
||||
|
||||
namespace Femto.Modules.Blog.Infrastructure.DbConnection;
|
||||
|
||||
public interface IDbConnectionFactory
|
||||
{
|
||||
IDbConnection GetConnection();
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
using System.Text.Json;
|
||||
using Femto.Modules.Blog.Data;
|
||||
using Femto.Modules.Blog.Infrastructure.Integration;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Quartz;
|
||||
|
||||
namespace Femto.Modules.Blog;
|
||||
|
||||
[DisallowConcurrentExecution]
|
||||
internal class MailmanJob(
|
||||
Outbox outbox,
|
||||
BlogContext context,
|
||||
ILogger<MailmanJob> logger,
|
||||
IMediator mediator
|
||||
) : IJob
|
||||
{
|
||||
public async Task Execute(IJobExecutionContext executionContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
var messages = await outbox.GetPendingMessages(executionContext.CancellationToken);
|
||||
|
||||
logger.LogTrace("loaded {Count} outbox messages to process", messages.Count);
|
||||
foreach (var message in messages)
|
||||
{
|
||||
try
|
||||
{
|
||||
var notificationType = OutboxMessageTypeRegistry.GetType(message.EventType);
|
||||
|
||||
if (notificationType is null)
|
||||
{
|
||||
logger.LogWarning(
|
||||
"unmapped event type {Type}. skipping.",
|
||||
message.EventType
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
var notification =
|
||||
JsonSerializer.Deserialize(message.Payload, notificationType)
|
||||
as INotification;
|
||||
|
||||
if (notification is null)
|
||||
throw new Exception("notification is null");
|
||||
|
||||
logger.LogTrace(
|
||||
"publishing outbox message {EventType}. Id: {Id}, AggregateId: {AggregateId}",
|
||||
message.EventType,
|
||||
message.Id,
|
||||
message.AggregateId
|
||||
);
|
||||
|
||||
await mediator.Publish(notification, executionContext.CancellationToken);
|
||||
|
||||
message.Succeed();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(
|
||||
e,
|
||||
"Error processing event {EventId} for aggregate {AggregateId}",
|
||||
message.Id,
|
||||
message.AggregateId
|
||||
);
|
||||
|
||||
message.Fail(e.ToString());
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync(executionContext.CancellationToken);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError(e, "Error while processing outbox");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
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;
|
||||
|
||||
internal class Outbox(BlogContext context)
|
||||
{
|
||||
public async Task AddMessage<TMessage>(Guid aggregateId, TMessage message, CancellationToken cancellationToken)
|
||||
where TMessage : IIntegrationEvent
|
||||
{
|
||||
var eventType = typeof(TMessage).GetCustomAttribute<EventTypeAttribute>();
|
||||
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<IList<OutboxEntry>> 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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
namespace Femto.Modules.Blog.Infrastructure.Integration;
|
||||
|
||||
internal class OutboxEntry
|
||||
{
|
||||
private const int MaxRetries = 5;
|
||||
|
||||
public Guid Id { get; private set; }
|
||||
|
||||
public string EventType { get; private set; } = null!;
|
||||
public Guid AggregateId { get; private set; }
|
||||
|
||||
public string Payload { get; private set; } = null!;
|
||||
|
||||
public DateTime CreatedAt { get; private set; }
|
||||
|
||||
public DateTime? ProcessedAt { get; private set; }
|
||||
public DateTime? NextRetryAt { get; private set; }
|
||||
public int RetryCount { get; private set; } = 0;
|
||||
public string? LastError { get; private set; }
|
||||
public OutboxEntryStatus Status { get; private set; }
|
||||
|
||||
private OutboxEntry() { }
|
||||
|
||||
public OutboxEntry(Guid eventId, Guid aggregateId, string eventType, string payload)
|
||||
{
|
||||
this.Id = eventId;
|
||||
this.EventType = eventType;
|
||||
this.AggregateId = aggregateId;
|
||||
this.Payload = payload;
|
||||
this.CreatedAt = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public void Succeed()
|
||||
{
|
||||
this.ProcessedAt = DateTime.UtcNow;
|
||||
this.Status = OutboxEntryStatus.Completed;
|
||||
}
|
||||
|
||||
public void Fail(string error)
|
||||
{
|
||||
if (this.RetryCount >= MaxRetries)
|
||||
{
|
||||
this.Status = OutboxEntryStatus.Failed;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.LastError = error;
|
||||
this.NextRetryAt = DateTime.UtcNow.AddSeconds(Math.Pow(2, this.RetryCount));
|
||||
this.RetryCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum OutboxEntryStatus
|
||||
{
|
||||
Pending,
|
||||
Completed,
|
||||
Failed
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Reflection;
|
||||
using Femto.Common.Attributes;
|
||||
using MediatR;
|
||||
|
||||
namespace Femto.Modules.Blog.Infrastructure.Integration;
|
||||
|
||||
internal static class OutboxMessageTypeRegistry
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, Type> Mapping = new();
|
||||
|
||||
public static void RegisterOutboxMessages(IImmutableDictionary<string, Type> mapping)
|
||||
{
|
||||
foreach (var (key, value) in mapping)
|
||||
{
|
||||
Mapping.TryAdd(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
public static Type? GetType(string eventName) => Mapping.GetValueOrDefault(eventName);
|
||||
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
using Femto.Common.Domain;
|
||||
using Femto.Modules.Blog.Data;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Femto.Modules.Blog.Infrastructure.PipelineBehaviours;
|
||||
|
||||
internal class SaveChangesPipelineBehaviour<TRequest, TResponse>(
|
||||
BlogContext context,
|
||||
IPublisher publisher,
|
||||
ILogger<SaveChangesPipelineBehaviour<TRequest, TResponse>> logger
|
||||
) : IPipelineBehavior<TRequest, TResponse>
|
||||
where TRequest : notnull
|
||||
{
|
||||
public async Task<TResponse> Handle(
|
||||
TRequest request,
|
||||
RequestHandlerDelegate<TResponse> next,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var response = await next(cancellationToken);
|
||||
|
||||
if (context.ChangeTracker.HasChanges())
|
||||
{
|
||||
|
||||
await EmitDomainEvents(cancellationToken);
|
||||
|
||||
logger.LogDebug("saving changes");
|
||||
await context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
private async Task EmitDomainEvents(CancellationToken cancellationToken)
|
||||
{
|
||||
var domainEvents = context
|
||||
.ChangeTracker.Entries<Entity>()
|
||||
.SelectMany(e =>
|
||||
{
|
||||
var events = e.Entity.DomainEvents;
|
||||
e.Entity.ClearDomainEvents();
|
||||
return events;
|
||||
})
|
||||
.ToList();
|
||||
|
||||
logger.LogTrace("loaded {Count} domain events", domainEvents.Count);
|
||||
|
||||
foreach (var domainEvent in domainEvents)
|
||||
{
|
||||
logger.LogTrace(
|
||||
"publishing {Type} domain event {Id}",
|
||||
domainEvent.GetType().Name,
|
||||
domainEvent.EventId
|
||||
);
|
||||
await publisher.Publish(domainEvent, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
91
Femto.Modules.Blog/Module.cs
Normal file
91
Femto.Modules.Blog/Module.cs
Normal file
|
@ -0,0 +1,91 @@
|
|||
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.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<BlogContext>(builder =>
|
||||
{
|
||||
builder.UseNpgsql(
|
||||
connectionString,
|
||||
o =>
|
||||
{
|
||||
o.MapEnum<OutboxEntryStatus>("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<Outbox, Outbox>();
|
||||
services.AddTransient<IDbConnectionFactory>(_ => 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<MailmanJob>(jobKey)
|
||||
.AddTrigger(trigger =>
|
||||
trigger
|
||||
.ForJob(jobKey)
|
||||
.WithSimpleSchedule(schedule =>
|
||||
schedule.WithIntervalInSeconds(1).RepeatForever()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue