Skip to content

Commit

Permalink
feat(EntityFrameworkCore): add extensions for entity configurations f…
Browse files Browse the repository at this point in the history
…rom assemblies

- Added a new file `BuilderExtensions.cs` which includes two extension methods: `WithEntityConfigurationsFromAssembliesExtension` and `ApplyConfigurationsFromAssembliesExtension`.
- These methods allow registering and applying `IEntityTypeConfiguration<TEntity>` implementations from provided assemblies.
- Created another new file `EntityConfigurationsFromAssembliesExtension.cs` that defines a custom DbContextOptionsExtension to load entity configurations from assemblies.
- This is useful when the DbContext is in a separate assembly from the entity configurations and cannot reference the entity configurations assemblies directly.
- Refactored method `AddOutboxBehavior` in `OutboxExtensions.cs`, simplifying its implementation.
  • Loading branch information
winromulus committed Sep 8, 2024
1 parent abf1e2a commit c334452
Show file tree
Hide file tree
Showing 16 changed files with 202 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MassTransit.RabbitMQ" Version="8.2.5" />
<PackageReference Include="MediatR" Version="12.4.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.8" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.3" />
Expand All @@ -32,7 +34,6 @@
<ProjectReference Include="..\..\src\ES.FX.Ignite.OpenTelemetry.Exporter.Seq\ES.FX.Ignite.OpenTelemetry.Exporter.Seq.csproj" />
<ProjectReference Include="..\..\src\ES.FX.Ignite.Serilog\ES.FX.Ignite.Serilog.csproj" />
<ProjectReference Include="..\..\src\ES.FX.Ignite.StackExchange.Redis\ES.FX.Ignite.StackExchange.Redis.csproj" />
<ProjectReference Include="..\..\src\ES.FX.Ignite.Swashbuckle\ES.FX.Ignite.Swashbuckle.csproj" />
<ProjectReference Include="..\..\src\ES.FX.Ignite\ES.FX.Ignite.csproj" />
<ProjectReference Include="..\..\src\ES.FX.Serilog\ES.FX.Serilog.csproj" />
<ProjectReference Include="..\..\src\ES.FX.TransactionalOutbox.EntityFrameworkCore.SqlServer\ES.FX.TransactionalOutbox.EntityFrameworkCore.SqlServer.csproj" />
Expand Down
53 changes: 48 additions & 5 deletions playground/Playground.Microservice.Api.Host/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,22 @@
using ES.FX.TransactionalOutbox.EntityFrameworkCore.SqlServer;
using FluentValidation;
using HealthChecks.UI.Client;
using MassTransit;
using MassTransit.Configuration;
using MassTransit.Logging;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Playground.Microservice.Api.Host.HostedServices;
using Playground.Microservice.Api.Host.Testing;
using Playground.Shared.Data.Simple.EntityFrameworkCore;
using Playground.Shared.Data.Simple.EntityFrameworkCore.SqlServer;
using SharpGrip.FluentValidation.AutoValidation.Endpoints.Extensions;
using System.Windows.Input;
using ES.FX.Microsoft.EntityFrameworkCore;
using ES.FX.Microsoft.EntityFrameworkCore.Extensions;
using MediatR;
using Google.Protobuf;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.Options;

return await ProgramEntry.CreateBuilder(args).UseSerilog().Build().RunAsync(async _ =>
{
Expand All @@ -38,7 +48,6 @@
builder.Ignite(settings =>
{
settings.HealthChecks.ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse;
//settings.OpenTelemetry.AspNetCoreTracingHealthChecksRequestsFiltered = true;
});

//Add Seq
Expand Down Expand Up @@ -84,6 +93,7 @@
configureDbContextOptionsBuilder: dbContextOptionsBuilder =>
{
dbContextOptionsBuilder.ConfigureWarnings(w => w.Ignore(SqlServerEventId.SavepointsDisabledBecauseOfMARS));
dbContextOptionsBuilder.WithEntityConfigurationsFromAssembliesExtension(typeof(SimpleDbContext).Assembly);
},
configureSqlServerDbContextOptionsBuilder: sqlServerDbContextOptionsBuilder =>
{
Expand All @@ -103,18 +113,51 @@
builder.IgniteRedisClient();





builder.Services.AddHostedService<TestHostedService>();
builder.Services.AddScoped<IValidator<TestRequest>, TestValidator>();
builder.Services.AddScoped<IValidator<TestComplexRequest>, TestComplexRequestValidator>();


builder.Services.AddOutboxMessageType<OutboxTestMessage>();
builder.Services.AddOutboxDeliveryService<SimpleDbContext, OutboxMessageHandler>();

builder.Services.AddOutboxDeliveryService<SimpleDbContext, MassTransitOutboxRelay>();
builder.Services.AddOpenTelemetry().WithTracing(traceBuilder =>
traceBuilder.AddTransactionalOutboxInstrumentation());


builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssemblyContaining<Program>();
});



builder.Services.AddMassTransit(x =>
{
x.AddConsumer<MediatorConsumer<OutboxTestMessage>>(c =>
{
});

x.UsingRabbitMq((context, cfg) =>
{
cfg.Host("rabbitmq://rabbitmq.localenv.io/localenv", h => {
h.Username("admin");
h.Password("SuperPass#");
h.ConnectionName(builder.Environment.ApplicationName);
});
cfg.SendTopology.ConfigureErrorSettings =
settings => settings.SetQueueArgument("x-message-ttl", TimeSpan.FromDays(7));
cfg.Publish<INotification>(p => p.Exclude = true);

cfg.ConfigureEndpoints(context,
new DefaultEndpointNameFormatter(
$"{context.GetRequiredService<IHostEnvironment>().ApplicationName}__"));
});
});
builder.Services.AddOpenTelemetry().WithTracing(traceBuilder =>
traceBuilder.AddSource(DiagnosticHeaders.DefaultListenerName));


var app = builder.Build();
app.Ignite();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"launchBrowser": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"ASPNETCORE_HTTP_PORTS": "8080"
"ASPNETCORE_HTTP_PORTS": "8080",
"OTEL_RESOURCE_ATTRIBUTES":"service.instance.id=spn-updates-api-5966db5b75-b6rpm,k8s.pod.name=spn-updates-api-5966db5b75-b6rpm,k8s.pod.uid=4b7c958a-d120-4f94-b9bf-4236ebd385e7,k8s.namespace.name=latest,k8s.node.name=aks-nodepool1-32158127-vmss000000"
},
"applicationUrl": "http://0.0.0.0:50001",
"dotnetRunMessages": false,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using ES.FX.TransactionalOutbox.EntityFrameworkCore.Delivery;
using MassTransit;

namespace Playground.Microservice.Api.Host.Testing;

public class MassTransitOutboxRelay(IBusControl busControl, IPublishEndpoint publishEndpoint) : IOutboxMessageHandler
{
public async ValueTask<bool> IsReadyAsync() =>
await busControl.WaitForHealthStatus(BusHealthStatus.Healthy, TimeSpan.FromSeconds(5)).ConfigureAwait(false) ==
BusHealthStatus.Healthy;

public async ValueTask<bool> HandleAsync(OutboxMessageHandlerContext context,
CancellationToken cancellationToken = default)
{
await publishEndpoint.Publish(context.Message, cancellationToken);
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using MassTransit;
using MediatR;

namespace Playground.Microservice.Api.Host.Testing;

public class MediatorConsumer<TMessage>(IMediator mediator) : IConsumer<TMessage> where TMessage : class
{
public async Task Consume(ConsumeContext<TMessage> context)
{
await mediator.Publish(context.Message);
}
}

public class MediatorConsumerDefinition<TMessage> :
ConsumerDefinition<MediatorConsumer<TMessage>> where TMessage : class
{
public MediatorConsumerDefinition()
{
// override the default endpoint name, for whatever reason
EndpointName = "ha-submit-order";

// limit the number of messages consumed concurrently
// this applies to the consumer only, not the endpoint
ConcurrentMessageLimit = 4;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using ES.FX.TransactionalOutbox.EntityFrameworkCore.Messages;
using MassTransit;
using MediatR;

namespace Playground.Microservice.Api.Host.Testing;

[OutboxMessageType("SomeTestMessage")]
public record OutboxTestMessage(string SomeProp);
[OutboxMessageType("OutboxTextMessage.v1")]
[EntityName("OutboxTextMessage.v1")]
public record OutboxTestMessage(string SomeProp) : INotification;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using MediatR;

namespace Playground.Microservice.Api.Host.Testing;

public class OutboxTestMessageHandler() : INotificationHandler<OutboxTestMessage>
{
public async Task Handle(OutboxTestMessage request, CancellationToken cancellationToken)
{
await Task.CompletedTask;

}
}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using ES.FX.TransactionalOutbox.EntityFrameworkCore;
using ES.FX.Microsoft.EntityFrameworkCore.Extensions;
using ES.FX.TransactionalOutbox.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Playground.Shared.Data.Simple.EntityFrameworkCore.Entities;

Expand All @@ -13,10 +14,7 @@ public class SimpleDbContext(
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.AddOutboxEntities();

modelBuilder.ApplyConfigurationsFromAssembly(
typeof(SimpleDbContext).Assembly);

modelBuilder.ApplyConfigurationsFromAssembliesExtension(dbContextOptions);
base.OnModelCreating(modelBuilder);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace ES.FX.Microsoft.EntityFrameworkCore.Extensions;

public static class BuilderExtensions
{
/// <summary>
/// Registers an <see cref="EntityConfigurationsFromAssembliesExtension" /> to the
/// <see cref="DbContextOptionsBuilder" />.
/// Provides a list of assemblies to scan for <see cref="IEntityTypeConfiguration{TEntity}" /> implementations.
/// Requires the model builder to be configured with />
/// </summary>
/// <param name="builder">The <see cref="DbContextOptionsBuilder" /></param>
/// <param name="assemblies">List of <see cref="Assembly" /> to scan</param>
public static void WithEntityConfigurationsFromAssembliesExtension(this DbContextOptionsBuilder builder,
params Assembly[] assemblies)
{
((IDbContextOptionsBuilderInfrastructure)builder).AddOrUpdateExtension(
new EntityConfigurationsFromAssembliesExtension
{ Assemblies = assemblies });
}


/// <summary>
/// Applies the <see cref="IEntityTypeConfiguration{TEntity}" /> implementations from the assemblies registered in the
/// <see cref="EntityConfigurationsFromAssembliesExtension" /> to the <see cref="ModelBuilder" />.
/// </summary>
/// <param name="builder">The <see cref="ModelBuilder" /></param>
/// <param name="options">
/// The <see cref="DbContextOptions" /> to load the
/// <see cref="EntityConfigurationsFromAssembliesExtension" />> from
/// </param>
public static void ApplyConfigurationsFromAssembliesExtension(this ModelBuilder builder, DbContextOptions options)
{
var extension = options.FindExtension<EntityConfigurationsFromAssembliesExtension>();
if (extension is null) return;
foreach (var assembly in extension.Assemblies) builder.ApplyConfigurationsFromAssembly(assembly);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.Extensions.DependencyInjection;

namespace ES.FX.Microsoft.EntityFrameworkCore.Extensions;

/// <summary>
/// Custom extension to load <see cref="IEntityTypeConfiguration{TEntity}" /> implementations from assemblies.
/// This is useful when the DbContext is in a separate assembly from the entity configurations and cannot reference the
/// entity configurations assemblies directly.
/// </summary>
public class EntityConfigurationsFromAssembliesExtension : IDbContextOptionsExtension
{
public EntityConfigurationsFromAssembliesExtension() => Info = new ExtensionInfo(this);
public required Assembly[] Assemblies { get; init; }

public void ApplyServices(IServiceCollection services)
{
//No services required
}

public void Validate(IDbContextOptions options)
{
// No-op. No validation required
}

public DbContextOptionsExtensionInfo Info { get; }


public sealed class ExtensionInfo(IDbContextOptionsExtension extension)
: DbContextOptionsExtensionInfo(extension)
{
public override bool IsDatabaseProvider => false;
public override string LogFragment => string.Empty;
public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other) => true;
public override int GetServiceProviderHashCode() => 0;

public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,8 @@ public static void AddOutboxEntities(this ModelBuilder modelBuilder)
/// grouping of messages in outboxes.
/// </summary>
/// <param name="optionsBuilder"></param>
public static void AddOutboxBehavior(this DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.AddInterceptors(
new OutboxDbContextInterceptor());
}
public static void AddOutboxBehavior(this DbContextOptionsBuilder optionsBuilder) =>
optionsBuilder.AddInterceptors(new OutboxDbContextInterceptor());


/// <summary>
Expand Down

0 comments on commit c334452

Please sign in to comment.