Skip to content
This repository has been archived by the owner on Dec 11, 2024. It is now read-only.

Fix/datetimekind utc #148

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions Backend.Fx.sln
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "messagebus", "messagebus",
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dependencyinjection", "dependencyinjection", "{22E4DE95-C3E5-49E6-83BF-BF30905A746B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend.Fx.EfCore6Persistence", "src\implementations\Backend.Fx.EfCore6Persistence\Backend.Fx.EfCore6Persistence.csproj", "{38034961-CE3B-4286-A9EB-496DECA39632}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Backend.Fx.EfCore6Persistence.Tests", "tests\Backend.Fx.EfCore6Persistence.Tests\Backend.Fx.EfCore6Persistence.Tests.csproj", "{E50D7E8D-D012-4683-BA05-C877BAA25230}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -107,6 +111,14 @@ Global
{DF40E1E8-FB19-455E-9CED-212C544AA8BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DF40E1E8-FB19-455E-9CED-212C544AA8BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DF40E1E8-FB19-455E-9CED-212C544AA8BC}.Release|Any CPU.Build.0 = Release|Any CPU
{38034961-CE3B-4286-A9EB-496DECA39632}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{38034961-CE3B-4286-A9EB-496DECA39632}.Debug|Any CPU.Build.0 = Debug|Any CPU
{38034961-CE3B-4286-A9EB-496DECA39632}.Release|Any CPU.ActiveCfg = Release|Any CPU
{38034961-CE3B-4286-A9EB-496DECA39632}.Release|Any CPU.Build.0 = Release|Any CPU
{E50D7E8D-D012-4683-BA05-C877BAA25230}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E50D7E8D-D012-4683-BA05-C877BAA25230}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E50D7E8D-D012-4683-BA05-C877BAA25230}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E50D7E8D-D012-4683-BA05-C877BAA25230}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -131,6 +143,8 @@ Global
{2C826FC0-443A-4874-B213-C35BFDEA200A} = {8BC1C02F-0785-4161-BC37-7D462BD6F42D}
{22E4DE95-C3E5-49E6-83BF-BF30905A746B} = {739A7296-579F-4D9A-BC73-DCECD260D7A0}
{FF042FB5-BA44-4655-8903-2644FE549810} = {22E4DE95-C3E5-49E6-83BF-BF30905A746B}
{38034961-CE3B-4286-A9EB-496DECA39632} = {ADC35CAD-F5B1-42B6-A0CC-B96974C11F11}
{E50D7E8D-D012-4683-BA05-C877BAA25230} = {C7885592-A4B8-4BA8-8D3A-1EDA4025D17A}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {45648557-C751-44AD-9C87-0F12EB673969}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Runtime.InteropServices;
using Backend.Fx.Logging;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;
Expand All @@ -21,8 +22,8 @@ public AdjustableClock(IClock clockImplementation)

public void OverrideUtcNow(DateTime utcNow)
{
Logger.LogTrace("Adjusting clock to {UtcNow}", utcNow);
_overriddenUtcNow = utcNow;
Logger.LogTrace("Adjusting clock to {UtcNow} UTC", utcNow);
_overriddenUtcNow = new DateTime(utcNow.Ticks, DateTimeKind.Utc);
}

public DateTime Advance(TimeSpan timespan)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class FrozenClock : IClock
public FrozenClock(IClock clock)
{
UtcNow = DateTime.UtcNow;
Logger.LogTrace("Freezing clock at {UtcNow}", UtcNow);
Logger.LogTrace("Freezing clock at {UtcNow} UTC", UtcNow);
}

public DateTime UtcNow { get; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using Backend.Fx.BuildingBlocks;
using Microsoft.EntityFrameworkCore;

namespace Backend.Fx.EfCorePersistence
{
public abstract class AggregateMapping<T> : IAggregateMapping<T> where T : AggregateRoot
{
public abstract IEnumerable<Expression<Func<T, object>>> IncludeDefinitions { get; }

public abstract void ApplyEfMapping(ModelBuilder modelBuilder);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
<GenerateAssemblyVersionAttribute>false</GenerateAssemblyVersionAttribute>
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
<GenerateAssemblyInformationalVersionAttribute>false</GenerateAssemblyInformationalVersionAttribute>
</PropertyGroup>

<PropertyGroup>
<Authors>Marc Wittke</Authors>
<Company>anic GmbH</Company>
<Copyright>All rights reserved. Distributed under the terms of the MIT License.</Copyright>
<Description>Persistence implementation for Backend.Fx using Entity Framework Core 6</Description>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild>
<PackageLicense>MIT</PackageLicense>
<PackageProjectUrl>https://github.com/marcwittke/Backend.Fx</PackageProjectUrl>
<Product>Backend.Fx</Product>
<RepositoryType>Git</RepositoryType>
<RepositoryUrl>https://github.com/marcwittke/Backend.Fx.git</RepositoryUrl>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\abstractions\Backend.Fx\Backend.Fx.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.Data;
using System.Data.Common;
using Backend.Fx.Environment.Persistence;
using Backend.Fx.Patterns.DependencyInjection;
using Microsoft.EntityFrameworkCore;

namespace Backend.Fx.EfCorePersistence.Bootstrapping
{
public class DbContextTransactionOperationDecorator : DbTransactionOperationDecorator
{
private readonly DbContext _dbContext;

public DbContextTransactionOperationDecorator(DbContext dbContext, IDbConnection dbConnection, IOperation operation)
: base(dbConnection, operation)
{
_dbContext = dbContext;
}

public override void Begin()
{
base.Begin();
_dbContext.Database.UseTransaction((DbTransaction) CurrentTransaction);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
using System;
using System.Data;
using System.Linq;
using System.Reflection;
using Backend.Fx.BuildingBlocks;
using Backend.Fx.Environment.Persistence;
using Backend.Fx.Patterns.DependencyInjection;
using Backend.Fx.Patterns.EventAggregation.Domain;
using Backend.Fx.Patterns.IdGeneration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace Backend.Fx.EfCorePersistence.Bootstrapping
{
public class EfCorePersistenceModule<TDbContext> : IModule
where TDbContext : DbContext
{
private readonly ILoggerFactory _loggerFactory;
private readonly Action<DbContextOptionsBuilder<TDbContext>, IDbConnection> _configure;
private readonly IDbConnectionFactory _dbConnectionFactory;
private readonly IEntityIdGenerator _entityIdGenerator;
private readonly Assembly[] _assemblies;

public EfCorePersistenceModule(IDbConnectionFactory dbConnectionFactory, IEntityIdGenerator entityIdGenerator,
ILoggerFactory loggerFactory, Action<DbContextOptionsBuilder<TDbContext>, IDbConnection> configure, params Assembly[] assemblies)
{
_dbConnectionFactory = dbConnectionFactory;
_entityIdGenerator = entityIdGenerator;
_loggerFactory = loggerFactory;
_configure = configure;
_assemblies = assemblies;
}

public void Register(ICompositionRoot compositionRoot)
{
// by letting the container create the connection we can be sure, that only one connection per scope is used, and disposing is done accordingly
compositionRoot.InfrastructureModule.RegisterScoped(() => _dbConnectionFactory.Create());

// singleton id generator
compositionRoot.InfrastructureModule.RegisterInstance(_entityIdGenerator);

// EF core requires us to flush frequently, because of a missing identity map
compositionRoot.InfrastructureModule.RegisterScoped<ICanFlush, EfFlush>();

// EF Repositories
compositionRoot.InfrastructureModule.RegisterScoped(typeof(IRepository<>), typeof(EfRepository<>));

// IQueryable is supported, but should be use with caution, since it bypasses authorization
compositionRoot.InfrastructureModule.RegisterScoped(typeof(IQueryable<>), typeof(EntityQueryable<>));

// DbContext is injected into repositories
compositionRoot.InfrastructureModule.RegisterScoped(() => CreateDbContextOptions(compositionRoot.InstanceProvider.GetInstance<IDbConnection>()));
compositionRoot.InfrastructureModule.RegisterScoped<DbContext, TDbContext>();

// wrapping the operation: connection.open - transaction.begin - operation - (flush) - transaction.commit - connection.close
compositionRoot.InfrastructureModule.RegisterDecorator<IOperation, FlushOperationDecorator>();
compositionRoot.InfrastructureModule.RegisterDecorator<IOperation, DbContextTransactionOperationDecorator>();
compositionRoot.InfrastructureModule.RegisterDecorator<IOperation, DbConnectionOperationDecorator>();

// ensure everything dirty is flushed to the db before handling domain events
compositionRoot.InfrastructureModule.RegisterDecorator<IDomainEventAggregator, FlushDomainEventAggregatorDecorator>();

compositionRoot.InfrastructureModule.RegisterScoped(typeof(IAggregateMapping<>), _assemblies);
}

protected virtual DbContextOptions<TDbContext> CreateDbContextOptions(IDbConnection connection)
{
var dbContextOptionsBuilder = new DbContextOptionsBuilder<TDbContext>();
_configure.Invoke(dbContextOptionsBuilder, connection);
return dbContextOptionsBuilder.UseLoggerFactory(_loggerFactory).Options;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using System.Data;

namespace Backend.Fx.EfCorePersistence.Bootstrapping
{
public interface IDbConnectionFactory
{
IDbConnection Create();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
using System;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Backend.Fx.BuildingBlocks;
using Backend.Fx.Extensions;
using Backend.Fx.Logging;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;

namespace Backend.Fx.EfCorePersistence
{
public static class DbContextExtensions
{
private static readonly ILogger Logger = Log.Create(typeof(DbContextExtensions));

public static void DisableChangeTracking(this DbContext dbContext)
{
Logger.LogDebug("Disabling change tracking on {DbContextTypeName} instance", dbContext.GetType().Name);
dbContext.ChangeTracker.AutoDetectChangesEnabled = false;
dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}

public static void RegisterRowVersionProperty(this ModelBuilder modelBuilder)
{
modelBuilder.Model
.GetEntityTypes()
.Where(mt => typeof(Entity).GetTypeInfo().IsAssignableFrom(mt.ClrType.GetTypeInfo()))
.ForAll(mt => modelBuilder.Entity(mt.ClrType).Property<byte[]>("RowVersion").IsRowVersion());
}

public static void RegisterEntityIdAsNeverGenerated(this ModelBuilder modelBuilder)
{
modelBuilder.Model
.GetEntityTypes()
.Where(mt => typeof(Entity).GetTypeInfo().IsAssignableFrom(mt.ClrType.GetTypeInfo()))
.ForAll(mt => modelBuilder.Entity(mt.ClrType).Property(nameof(Entity.Id)).ValueGeneratedNever());
}

public static void ApplyAggregateMappings(this DbContext dbContext, ModelBuilder modelBuilder)
{
//CAVE: IAggregateMapping implementations must reside in the same assembly as the Applications DbContext-type
var aggregateDefinitionTypeInfos = dbContext
.GetType()
.GetTypeInfo()
.Assembly
.ExportedTypes
.Select(t => t.GetTypeInfo())
.Where(t => t.IsClass && !t.IsAbstract && !t.IsGenericType && typeof(IAggregateMapping).GetTypeInfo().IsAssignableFrom(t));
foreach (TypeInfo typeInfo in aggregateDefinitionTypeInfos)
{
var aggregateMapping = (IAggregateMapping) Activator.CreateInstance(typeInfo.AsType());
aggregateMapping.ApplyEfMapping(modelBuilder);
}
}



public static void TraceChangeTrackerState(this DbContext dbContext)
{
if (Logger.IsEnabled(LogLevel.Trace))
try
{
var changeTrackerState = new
{
added = dbContext.ChangeTracker.Entries().Where(entry => entry.State == EntityState.Added).ToArray(),
modified = dbContext.ChangeTracker.Entries().Where(entry => entry.State == EntityState.Modified).ToArray(),
deleted = dbContext.ChangeTracker.Entries().Where(entry => entry.State == EntityState.Deleted).ToArray(),
unchanged = dbContext.ChangeTracker.Entries().Where(entry => entry.State == EntityState.Unchanged).ToArray()
};

Logger.LogTrace("Change tracker state: {@ChangeTrackerState}", changeTrackerState);
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Change tracker state could not be dumped");
}
}

private static string GetPrimaryKeyValue(EntityEntry entry)
{
return (entry.Entity as Entity)?.Id.ToString(CultureInfo.InvariantCulture) ?? "?";
}
}
}
Loading