hopefully not a horribly foolish refactoring

This commit is contained in:
john 2025-05-11 23:26:09 +02:00
parent 59d660165f
commit 1ecaf64dea
82 changed files with 782 additions and 398 deletions

View file

@ -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);
}
}

View file

@ -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<BlogContext> options) : DbContext(options)
internal class BlogContext(DbContextOptions<BlogContext> options) : DbContext(options), IOutboxContext
{
public virtual DbSet<Post> Posts { get; set; }
public virtual DbSet<OutboxEntry> Outbox { get; set; }

View file

@ -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<IMediator>();
await mediator.Send(command, cancellationToken);
}
public async Task<TResponse> PostCommand<TResponse>(
ICommand<TResponse> command,
CancellationToken cancellationToken = default
)
{
using var scope = host.Services.CreateScope();
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
var response = await mediator.Send(command, cancellationToken);
return response;
}
public async Task<TResponse> PostQuery<TResponse>(
IQuery<TResponse> query,
CancellationToken cancellationToken = default
)
{
using var scope = host.Services.CreateScope();
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
var response = await mediator.Send(query, cancellationToken);
return response;
}
}

View file

@ -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<IBlogModule>(_ => new BlogModule(host));
}
private static void ConfigureServices(this IServiceCollection services, string connectionString)
{
services.AddTransient<IDbConnectionFactory>(_ => new DbConnectionFactory(connectionString));
services.AddDbContext<BlogContext>(builder =>
{
builder.UseNpgsql(
connectionString,
o =>
{
o.MapEnum<OutboxEntryStatus>("outbox_status");
}
);
builder.UseSnakeCaseNamingConvention();
var loggerFactory = LoggerFactory.Create(b => { });
builder.UseLoggerFactory(loggerFactory);
builder.EnableSensitiveDataLogging();
});
services.AddMediatR(c =>
{
c.RegisterServicesFromAssembly(typeof(BlogStartup).Assembly);
});
services.AddScoped<DbContext>(s => s.GetRequiredService<BlogContext>());
services.AddTransient(
typeof(IPipelineBehavior<,>),
typeof(SaveChangesPipelineBehaviour<,>)
);
}
}

View file

@ -0,0 +1,8 @@
using Femto.Common.Domain;
namespace Femto.Modules.Blog.Application.Commands.CreatePost;
public record CreatePostCommand(Guid AuthorId, string Content, IEnumerable<CreatePostMedia> Media)
: ICommand<Guid>;
public record CreatePostMedia(Guid MediaId, Uri Url, string? Type, int Order, int? Width, int? Height);

View file

@ -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<CreatePostCommand, Guid>
@ -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);

View file

@ -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<OutboxEntry>
{
@ -11,7 +10,6 @@ internal class OutboxEntryConfiguration : IEntityTypeConfiguration<OutboxEntry>
{
builder.ToTable("outbox");
builder.Property(x => x.Payload)
.HasColumnType("jsonb");
builder.Property(x => x.Payload).HasColumnType("jsonb");
}
}
}

View file

@ -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<Post>
{

View file

@ -0,0 +1,18 @@
using Femto.Common.Domain;
namespace Femto.Modules.Blog.Application;
public interface IBlogModule
{
Task PostCommand(ICommand command, CancellationToken cancellationToken = default);
Task<TResponse> PostCommand<TResponse>(
ICommand<TResponse> command,
CancellationToken cancellationToken = default
);
Task<TResponse> PostQuery<TResponse>(
IQuery<TResponse> query,
CancellationToken cancellationToken = default
);
}

View file

@ -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<PostDto> Posts, Guid? Next);

View file

@ -0,0 +1,3 @@
namespace Femto.Modules.Blog.Application.Queries.GetPosts.Dto;
public record PostAuthorDto(Guid AuthorId, string Username);

View file

@ -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<PostMediaDto> Media, DateTimeOffset CreatedAt, PostAuthorDto Author);

View file

@ -0,0 +1,3 @@
namespace Femto.Modules.Blog.Application.Queries.GetPosts.Dto;
public record PostMediaDto(Uri Url, int? Width, int? Height);

View file

@ -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<GetPostsQueryResult>
public class GetPostsQuery : IQuery<GetPostsQueryResult>
{
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; }
/// <summary>
/// Default is to load in reverse chronological order
/// TODO this is not exposed on the client as it probably wouldn't work that well
/// </summary>
public GetPostsDirection Direction { get; init; } = GetPostsDirection.Backward;
}
public enum GetPostsDirection

View file

@ -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<GetPostsQuery, GetPostsQueryResult>
@ -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<string>()
.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<PostMediaDto>()
.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; }

View file

@ -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!;
}

View file

@ -1,6 +0,0 @@
using MediatR;
namespace Femto.Modules.Blog.Domain.Posts.Commands.CreatePost;
public record CreatePostCommand(Guid AuthorId, string Content, IEnumerable<Uri> Media)
: IRequest<Guid>;

View file

@ -1,3 +0,0 @@
namespace Femto.Modules.Blog.Domain.Posts.Commands.GetPosts.Dto;
public record PostAuthorDto(Guid AuthorId, string Username);

View file

@ -1,3 +0,0 @@
namespace Femto.Modules.Blog.Domain.Posts.Commands.GetPosts.Dto;
public record PostMediaDto(Uri Url);

View file

@ -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<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
);
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,8 @@
using Femto.Common.Attributes;
using Femto.Common.Integration;
namespace Femto.Modules.Blog.Events;
[EventType("post.created")]
public record PostCreatedIntegrationEvent(Guid EventId, Guid PostId, IEnumerable<Guid> MediaIds)
: IIntegrationEvent;

View file

@ -9,12 +9,15 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Autofac" Version="8.3.0" />
<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="Microsoft.Extensions.Hosting" Version="9.0.4" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" 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" />
@ -41,7 +44,6 @@
<ItemGroup>
<ProjectReference Include="..\Femto.Common\Femto.Common.csproj" />
<ProjectReference Include="..\Femto.Modules.Blog.Contracts\Femto.Modules.Blog.Contracts.csproj" />
</ItemGroup>
</Project>

View file

@ -1,4 +1,4 @@
using Femto.Modules.Blog.Contracts.Events;
using Femto.Modules.Blog.Events;
using MediatR;
namespace Femto.Modules.Blog.Handlers;

View file

@ -1,13 +0,0 @@
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);
}
}

View file

@ -1,9 +0,0 @@
using System.Data;
using Microsoft.Data.SqlClient;
namespace Femto.Modules.Blog.Infrastructure.DbConnection;
public interface IDbConnectionFactory
{
IDbConnection GetConnection();
}

View file

@ -1,78 +0,0 @@
using System.Text.Json;
using Femto.Modules.Blog.Data;
using MediatR;
using Microsoft.Extensions.Logging;
using Quartz;
namespace Femto.Modules.Blog.Infrastructure.Integration.Outbox;
[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;
}
}
}

View file

@ -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<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);
}
}

View file

@ -1,59 +0,0 @@
namespace Femto.Modules.Blog.Infrastructure.Integration.Outbox;
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
}

View file

@ -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<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);
}

View file

@ -1,58 +0,0 @@
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);
}
}
}

View file

@ -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<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()
)
);
}
}