Skip to content

Commit

Permalink
Merge pull request #65 from marcwittke/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
marcwittke committed May 10, 2019
2 parents b569fc5 + 8104772 commit 8e7eab0
Show file tree
Hide file tree
Showing 29 changed files with 416 additions and 370 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ namespace Backend.Fx.BuildingBlocks
{
using System.Linq;

public interface IFullTextSearchService<out TAggregateRoot> : IDomainService where TAggregateRoot : AggregateRoot
public interface IFullTextSearchService<out TAggregateRoot> where TAggregateRoot : AggregateRoot
{
IQueryable<TAggregateRoot> Search(string searchQuery);
}
Expand Down
2 changes: 1 addition & 1 deletion src/abstractions/Backend.Fx/BuildingBlocks/IView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
using System.Linq;
using System.Linq.Expressions;

public interface IView<out T> : IQueryable<T>, IDomainService
public interface IView<out T> : IQueryable<T>
{}

public abstract class View<T> : IView<T>
Expand Down
20 changes: 20 additions & 0 deletions src/abstractions/Backend.Fx/Hacking/PrivateUtil.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.Linq;
using System.Reflection;

namespace Backend.Fx.Hacking
{
public static class PrivateUtil
{
public static T CreateInstanceFromPrivateDefaultConstructor<T>()
{
var constructor = typeof(T).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance).SingleOrDefault(ci => ci.GetParameters().Length == 0);
if (constructor == null)
{
throw new InvalidOperationException($"No private default constructor found in {typeof(T).Name}");
}
T instance = (T)constructor.Invoke(null);
return instance;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
{
using System.Collections.Concurrent;
using System.Threading.Tasks;
using BuildingBlocks;

public interface IEventBusScope : IDomainService

public interface IEventBusScope
{
/// <summary>
/// Enqueue an event to be raised later.
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Security.Principal;
using Backend.Fx.Patterns.DependencyInjection;

namespace Backend.Fx.Patterns.UnitOfWork
{
public class ReadonlyDecorator : IUnitOfWork
{
private readonly IUnitOfWork _unitOfWorkImplementation;

public ReadonlyDecorator(IUnitOfWork unitOfWorkImplementation)
{
_unitOfWorkImplementation = unitOfWorkImplementation;
}

public void Dispose()
{
_unitOfWorkImplementation.Dispose();
}

public void Begin()
{
_unitOfWorkImplementation.Begin();
}

public void Complete()
{
//skip
}

public ICurrentTHolder<IIdentity> IdentityHolder => _unitOfWorkImplementation.IdentityHolder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ public void OnActionExecuting(ActionExecutingContext context)

public void OnActionExecuted(ActionExecutedContext context)
{
// only unsafe requests need flush
if (context.HttpContext.Request.IsSafe())
{
return;
}

// that's all:
_backendFxApplication.CompositionRoot.GetInstance<ICanFlush>().Flush();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class WaitForBootMiddleware
private readonly IBackendFxApplication _application;

[UsedImplicitly]
protected WaitForBootMiddleware(RequestDelegate next, IBackendFxApplication application)
public WaitForBootMiddleware(RequestDelegate next, IBackendFxApplication application)
{
_next = next;
_application = application;
Expand Down
13 changes: 13 additions & 0 deletions src/environments/Backend.Fx.AspNetCore/HttpRequestEx.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Http;

namespace Backend.Fx.AspNetCore
{
public static class HttpRequestEx
{
public static bool IsSafe(this HttpRequest request)
{
string method = request.Method.ToUpperInvariant();
return method == "OPTIONS" || method == "GET" || method == "HEAD";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,22 @@ public async Task Invoke(HttpContext context)
Logger.Info("Queuing Request while application is booting...");
}

var asReadonly = context.Request.Method.ToUpperInvariant() == "GET";
using (var unitOfWork = asReadonly
? _application.CompositionRoot.GetInstance<IReadonlyUnitOfWork>()
: _application.CompositionRoot.GetInstance<IUnitOfWork>())
IUnitOfWork unitOfWork = _application.CompositionRoot.GetInstance<IUnitOfWork>();
try
{
if (context.Request.Method.ToUpperInvariant() == "GET")
{
unitOfWork = new ReadonlyDecorator(unitOfWork);
}

unitOfWork.Begin();
await _next.Invoke(context);
unitOfWork.Complete();
}
finally
{
unitOfWork.Dispose();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
namespace Backend.Fx.EfCorePersistence
using System;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using Backend.Fx.BuildingBlocks;
using Backend.Fx.Extensions;
using Backend.Fx.Logging;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Metadata;
using System.Diagnostics;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage;

namespace Backend.Fx.EfCorePersistence
{
using System;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text;
using BuildingBlocks;
using Extensions;
using Logging;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Metadata;

public static class DbContextExtensions
{
private static readonly ILogger Logger = LogManager.Create(typeof(DbContextExtensions));
Expand Down Expand Up @@ -108,6 +111,15 @@ public static void UpdateTrackingProperties(this DbContext dbContext, string use
}
}

public static void ResetTransactions(this DbContext dbContext)
{
// big fat HACK: until EF Core allows to change the transaction and/or connection on an existing instance of DbContext...
RelationalConnection relationalConnection = (RelationalConnection)dbContext.Database.GetService<IDbContextTransactionManager>();
MethodInfo methodInfo = typeof(RelationalConnection).GetMethod("ClearTransactions", BindingFlags.Instance | BindingFlags.NonPublic);
Debug.Assert(methodInfo != null, nameof(methodInfo) + " != null");
methodInfo.Invoke(relationalConnection, new object[0]);
}

/// <summary>
/// This method finds the EntityEntry&lt;AggregateRoot&gt; of an EntityEntry&lt;Entity&gt;
/// assuming it has been loaded and is being tracked by the change tracker.
Expand Down Expand Up @@ -168,25 +180,25 @@ public static void TraceChangeTrackerState(this DbContext dbContext)
var unchanged = dbContext.ChangeTracker.Entries().Where(entry => entry.State == EntityState.Unchanged).ToArray();

var stateDumpBuilder = new StringBuilder();
stateDumpBuilder.AppendFormat("{0} entities added{1}{2}", added.Length, deleted.Length == 0 ? "." : ":", Environment.NewLine);
stateDumpBuilder.AppendFormat("{0} entities added{1}{2}", added.Length, deleted.Length == 0 ? "." : ":", System.Environment.NewLine);
foreach (var entry in added)
{
stateDumpBuilder.AppendFormat("added: {0}[{1}]{2}", entry.Entity.GetType().Name, GetPrimaryKeyValue(entry), Environment.NewLine);
stateDumpBuilder.AppendFormat("added: {0}[{1}]{2}", entry.Entity.GetType().Name, GetPrimaryKeyValue(entry), System.Environment.NewLine);
}
stateDumpBuilder.AppendFormat("{0} entities modified{1}{2}", modified.Length, deleted.Length == 0 ? "." : ":", Environment.NewLine);
stateDumpBuilder.AppendFormat("{0} entities modified{1}{2}", modified.Length, deleted.Length == 0 ? "." : ":", System.Environment.NewLine);
foreach (var entry in modified)
{
stateDumpBuilder.AppendFormat("modified: {0}[{1}]{2}", entry.Entity.GetType().Name, GetPrimaryKeyValue(entry), Environment.NewLine);
stateDumpBuilder.AppendFormat("modified: {0}[{1}]{2}", entry.Entity.GetType().Name, GetPrimaryKeyValue(entry), System.Environment.NewLine);
}
stateDumpBuilder.AppendFormat("{0} entities deleted{1}{2}", deleted.Length, deleted.Length == 0 ? "." : ":", Environment.NewLine);
stateDumpBuilder.AppendFormat("{0} entities deleted{1}{2}", deleted.Length, deleted.Length == 0 ? "." : ":", System.Environment.NewLine);
foreach (var entry in deleted)
{
stateDumpBuilder.AppendFormat("deleted: {0}[{1}]{2}", entry.Entity.GetType().Name, GetPrimaryKeyValue(entry), Environment.NewLine);
stateDumpBuilder.AppendFormat("deleted: {0}[{1}]{2}", entry.Entity.GetType().Name, GetPrimaryKeyValue(entry), System.Environment.NewLine);
}
stateDumpBuilder.AppendFormat("{0} entities unchanged{1}{2}", unchanged.Length, deleted.Length == 0 ? "." : ":", Environment.NewLine);
stateDumpBuilder.AppendFormat("{0} entities unchanged{1}{2}", unchanged.Length, deleted.Length == 0 ? "." : ":", System.Environment.NewLine);
foreach (var entry in unchanged)
{
stateDumpBuilder.AppendFormat("unchanged: {0}[{1}]{2}", entry.Entity.GetType().Name, GetPrimaryKeyValue(entry), Environment.NewLine);
stateDumpBuilder.AppendFormat("unchanged: {0}[{1}]{2}", entry.Entity.GetType().Name, GetPrimaryKeyValue(entry), System.Environment.NewLine);
}
Logger.Trace(stateDumpBuilder.ToString());
}
Expand Down
59 changes: 38 additions & 21 deletions src/implementations/Backend.Fx.EfCorePersistence/EfRepository.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
namespace Backend.Fx.EfCorePersistence
{
using System.Linq;
using System.Security;
using BuildingBlocks;
using Environment.MultiTenancy;
using Logging;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Patterns.Authorization;
using Patterns.DependencyInjection;
using System;
using System.Linq;
using System.Security;
using Backend.Fx.BuildingBlocks;
using Backend.Fx.Environment.MultiTenancy;
using Backend.Fx.Logging;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Backend.Fx.Patterns.Authorization;
using Backend.Fx.Patterns.DependencyInjection;
using JetBrains.Annotations;

namespace Backend.Fx.EfCorePersistence
{
public class EfRepository<TAggregateRoot> : Repository<TAggregateRoot> where TAggregateRoot : AggregateRoot
{
private static readonly ILogger Logger = LogManager.Create<EfRepository<TAggregateRoot>>();
private readonly DbContext _dbContext;
private DbContext _dbContext;
private readonly IAggregateMapping<TAggregateRoot> _aggregateMapping;
private readonly IAggregateAuthorization<TAggregateRoot> _aggregateAuthorization;

public EfRepository(DbContext dbContext, IAggregateMapping<TAggregateRoot> aggregateMapping,
public EfRepository([CanBeNull] DbContext dbContext, IAggregateMapping<TAggregateRoot> aggregateMapping,
ICurrentTHolder<TenantId> currentTenantIdHolder, IAggregateAuthorization<TAggregateRoot> aggregateAuthorization)
: base(currentTenantIdHolder, aggregateAuthorization)
{
Expand All @@ -27,10 +29,25 @@ public class EfRepository<TAggregateRoot> : Repository<TAggregateRoot> where TAg
_aggregateAuthorization = aggregateAuthorization;

// somewhat a hack: using the internal EF Core services against advice
var localViewListener = dbContext.GetService<ILocalViewListener>();
localViewListener.RegisterView(AuthorizeChanges);
var localViewListener = dbContext?.GetService<ILocalViewListener>();
localViewListener?.RegisterView(AuthorizeChanges);
}

public DbContext DbContext
{
get => _dbContext ?? throw new InvalidOperationException("This EfRepository does not have a DbContext yet. You might either make sure a proper DbContext gets injected or the DbContext is initialized later using a derived class");
protected set
{
if (_dbContext != null)
{
throw new InvalidOperationException("This EfRepository has already a DbContext assigned. It is not allowed to change it later.");
}
_dbContext = value;
var localViewListener = _dbContext?.GetService<ILocalViewListener>();
localViewListener?.RegisterView(AuthorizeChanges);
}
}

/// <summary>
/// Due to the fact, that a real lifecycle hook API is not yet available (see issue https://github.com/aspnet/EntityFrameworkCore/issues/626)
/// we are using an internal service to achieve the same goal: When a state change occurs from unchanged to modified, and this repository is
Expand All @@ -57,26 +74,26 @@ private void AuthorizeChanges(InternalEntityEntry entry, EntityState previousSta
protected override void AddPersistent(TAggregateRoot aggregateRoot)
{
Logger.Debug($"Persistently adding new {AggregateTypeName}");
_dbContext.Set<TAggregateRoot>().Add(aggregateRoot);
DbContext.Set<TAggregateRoot>().Add(aggregateRoot);
}

protected override void AddRangePersistent(TAggregateRoot[] aggregateRoots)
{
Logger.Debug($"Persistently adding {aggregateRoots.Length} item(s) of type {AggregateTypeName}");
_dbContext.Set<TAggregateRoot>().AddRange(aggregateRoots);
DbContext.Set<TAggregateRoot>().AddRange(aggregateRoots);
}

protected override void DeletePersistent(TAggregateRoot aggregateRoot)
{
Logger.Debug($"Persistently removing {aggregateRoot.DebuggerDisplay}");
_dbContext.Set<TAggregateRoot>().Remove(aggregateRoot);
DbContext.Set<TAggregateRoot>().Remove(aggregateRoot);
}

protected override IQueryable<TAggregateRoot> RawAggregateQueryable
{
get
{
IQueryable<TAggregateRoot> queryable = _dbContext.Set<TAggregateRoot>();
IQueryable<TAggregateRoot> queryable = DbContext.Set<TAggregateRoot>();
if (_aggregateMapping.IncludeDefinitions != null)
{
foreach (var include in _aggregateMapping.IncludeDefinitions)
Expand Down

0 comments on commit 8e7eab0

Please sign in to comment.