Skip to content

Commit

Permalink
Improving the support for idempotency behavior, providing repositorie…
Browse files Browse the repository at this point in the history
…s that track changes of the returned entities
  • Loading branch information
tsutomi committed Nov 4, 2023
1 parent 9199554 commit a8ce716
Show file tree
Hide file tree
Showing 20 changed files with 565 additions and 62 deletions.
9 changes: 8 additions & 1 deletion Deveel.Repository.sln
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Deveel.Repository.Manager.E
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Deveel.Repository.Finbuckle.MultiTenant", "src\Deveel.Repository.Finbuckle.MultiTenant\Deveel.Repository.Finbuckle.MultiTenant.csproj", "{69E01D54-C430-4A1E-989E-5A61AB4C5B54}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deveel.Repository.Management.Tests", "test\Deveel.Repository.Management.Tests\Deveel.Repository.Management.Tests.csproj", "{7DC0EA52-DAAB-49D4-8443-3409C001E27E}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Deveel.Repository.Management.Tests", "test\Deveel.Repository.Management.Tests\Deveel.Repository.Management.Tests.csproj", "{7DC0EA52-DAAB-49D4-8443-3409C001E27E}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "management", "management", "{85200FA4-CC68-4758-B6E5-287E8AFA79FE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deveel.Repository.Finbuckle.MultiTenant.XUnit", "test\Deveel.Repository.Finbuckle.MultiTenant.XUnit\Deveel.Repository.Finbuckle.MultiTenant.XUnit.csproj", "{BE852D51-A08A-4151-A8C1-396096F0AB52}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -140,6 +142,10 @@ Global
{7DC0EA52-DAAB-49D4-8443-3409C001E27E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7DC0EA52-DAAB-49D4-8443-3409C001E27E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7DC0EA52-DAAB-49D4-8443-3409C001E27E}.Release|Any CPU.Build.0 = Release|Any CPU
{BE852D51-A08A-4151-A8C1-396096F0AB52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BE852D51-A08A-4151-A8C1-396096F0AB52}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BE852D51-A08A-4151-A8C1-396096F0AB52}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BE852D51-A08A-4151-A8C1-396096F0AB52}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -165,6 +171,7 @@ Global
{69E01D54-C430-4A1E-989E-5A61AB4C5B54} = {2860FD4D-510F-43C8-870E-5559B90D0CAD}
{7DC0EA52-DAAB-49D4-8443-3409C001E27E} = {85200FA4-CC68-4758-B6E5-287E8AFA79FE}
{85200FA4-CC68-4758-B6E5-287E8AFA79FE} = {50434E05-0F21-4871-AFB3-A483CEE4A300}
{BE852D51-A08A-4151-A8C1-396096F0AB52} = {50434E05-0F21-4871-AFB3-A483CEE4A300}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {01FD9B16-84B3-4D99-80C1-11B2F3D65B56}
Expand Down
12 changes: 12 additions & 0 deletions src/Deveel.Repository.Core/Data/ITrackingRepository_T.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace Deveel.Data {
/// <summary>
/// Defines a repository that is able to track changes
/// on entities returned by queries.
/// </summary>
/// <typeparam name="TEntity">
/// The type of entity managed by the repository.
/// </typeparam>
public interface ITrackingRepository<TEntity> : ITrackingRepository<TEntity, object>
where TEntity : class {
}
}
42 changes: 42 additions & 0 deletions src/Deveel.Repository.Core/Data/ITrackingRepository_T2.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace Deveel.Data {
/// <summary>
/// Defines a repository that is able to track changes
/// on entities returned by queries.
/// </summary>
/// <typeparam name="TEntity">
/// The type of entity managed by the repository.
/// </typeparam>
/// <typeparam name="TKey">
/// The type of the key used to identify the entity.
/// </typeparam>
public interface ITrackingRepository<TEntity, TKey> : IRepository<TEntity, TKey>
where TEntity : class {
/// <summary>
/// Gets a value indicating if the repository is tracking
/// changes on entities.
/// </summary>
/// <remarks>
/// A repository implementation of this contract might
/// be configured not to track changes on entities, in
/// this case the value of this property is <c>false</c>.
/// </remarks>
bool IsTrackingChanges { get; }

/// <summary>
/// Finds the original entity that is being tracked by the repository,
/// as it was loaded from the data source.
/// </summary>
/// <param name="key">
/// The key that identifies the entity.
/// </param>
/// <param name="cancellationToken">
/// A cancellation token that can be used to cancel the operation.
/// </param>
/// <returns>
/// Returns the original version of entity that is being tracked by the repository,
/// as it was loaded from the data source, or <c>null</c> if the entity is not
/// found or it's not being tracked.
/// </returns>
Task<TEntity?> FindOriginalAsync(TKey key, CancellationToken cancellationToken = default);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ public class EntityRepository<TEntity> : EntityRepository<TEntity, object>,
IRepository<TEntity>,
IFilterableRepository<TEntity>,
IQueryableRepository<TEntity>,
IPageableRepository<TEntity>
IPageableRepository<TEntity>,
ITrackingRepository<TEntity>
where TEntity : class {

/// <summary>
Expand Down
23 changes: 21 additions & 2 deletions src/Deveel.Repository.EntityFramework/Data/EntityRepository_T2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public class EntityRepository<TEntity, TKey> :
IFilterableRepository<TEntity, TKey>,
IQueryableRepository<TEntity, TKey>,
IPageableRepository<TEntity, TKey>,
ITrackingRepository<TEntity, TKey>,
IDisposable
where TEntity : class {
private bool disposedValue;
Expand Down Expand Up @@ -130,6 +131,8 @@ internal EntityRepository(DbContext context, ITenantInfo? tenantInfo, ILogger? l
protected bool IsTrackingChanges => Entities.Local != null ||
Context.ChangeTracker.QueryTrackingBehavior != QueryTrackingBehavior.NoTracking;

bool ITrackingRepository<TEntity, TKey>.IsTrackingChanges => IsTrackingChanges;

/// <summary>
/// Gets the information about the tenant that the repository is using to access the data.
/// </summary>
Expand Down Expand Up @@ -448,8 +451,7 @@ public virtual async Task<long> CountAsync(IQueryFilter filter, CancellationToke
if (result == null)
return result;

if (result != null)
result = await OnEntityFoundByKeyAsync(key, result, cancellationToken);
result = await OnEntityFoundByKeyAsync(key, result, cancellationToken);

return result;
} catch (Exception ex) {
Expand All @@ -458,6 +460,23 @@ public virtual async Task<long> CountAsync(IQueryFilter filter, CancellationToke
}
}

/// <inheritdoc/>
public virtual async Task<TEntity?> FindOriginalAsync(TKey key, CancellationToken cancellationToken = default) {
ThrowIfDisposed();

try {
var result = await Entities.FindAsync(new object?[] { ConvertEntityKey(key) }, cancellationToken);
if (result == null)
return result;

var entry = Context.Entry(result);
return (TEntity) entry.OriginalValues.ToObject();
} catch (Exception ex) {
Logger.LogUnknownError(ex, typeof(TEntity));
throw new RepositoryException("Unable to find an entity int he repository because of an error", ex);
}
}

/// <inheritdoc/>
public virtual async Task<IList<TEntity>> FindAllAsync(IQuery query, CancellationToken cancellationToken = default) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Finbuckle.MultiTenant;

namespace Deveel.Data {
Expand Down
75 changes: 54 additions & 21 deletions src/Deveel.Repository.InMemory/Data/InMemoryRepository_T2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
// limitations under the License.

using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.Serialization;
Expand All @@ -34,11 +33,12 @@ public class InMemoryRepository<TEntity, TKey> :
IQueryableRepository<TEntity, TKey>,
IPageableRepository<TEntity, TKey>,
IFilterableRepository<TEntity, TKey>,
ITrackingRepository<TEntity, TKey>,
IMultiTenantRepository<TEntity, TKey>,
IDisposable
where TEntity : class
where TKey : notnull {
private SortedList<TKey, TEntity> entities;
private SortedList<TKey, Entry> entities;
private bool disposedValue;
private MemberInfo? idMember;
private readonly IFieldMapper<TEntity>? fieldMapper;
Expand Down Expand Up @@ -91,10 +91,13 @@ protected InMemoryRepository(string tenantId,

IQueryable<TEntity> IQueryableRepository<TEntity, TKey>.AsQueryable() => Entities.AsQueryable();

bool ITrackingRepository<TEntity, TKey>.IsTrackingChanges => true;

/// <summary>
/// Gets the read-only list of entities in the repository.
/// </summary>
public virtual IReadOnlyList<TEntity> Entities => entities.Values.ToList().AsReadOnly();
public virtual IReadOnlyList<TEntity> Entities => entities.Values.Select(x => x.Entity)
.ToList().AsReadOnly();

string? IMultiTenantRepository<TEntity, TKey>.TenantId => TenantId;

Expand All @@ -104,25 +107,29 @@ protected InMemoryRepository(string tenantId,
/// </summary>
protected virtual string? TenantId { get; }

private SortedList<TKey, TEntity> CopyList(IEnumerable<TEntity> source) {
var result = new SortedList<TKey, TEntity>();
private SortedList<TKey, Entry> CopyList(IEnumerable<TEntity> source) {
var result = new SortedList<TKey, Entry>();
foreach (var item in source) {
var id = GetEntityId(item);
if (id == null)
throw new RepositoryException("The entity does not have an ID");

result.Add(id, Clone(item));
result.Add(id, new Entry(item));
}

return result;
}

private static TEntity Clone(TEntity entity) {
var serializer = new DataContractSerializer(typeof(TEntity));
using var stream = new MemoryStream();
serializer.WriteObject(stream, entity);
stream.Position = 0;
return (TEntity)serializer.ReadObject(stream)!;
var cloneMethod = typeof(TEntity)
.GetMethod("MemberwiseClone", BindingFlags.Instance | BindingFlags.NonPublic);

return (TEntity)cloneMethod!.Invoke(entity, new object[0])!;
//var serializer = new DataContractSerializer(typeof(TEntity));
//using var stream = new MemoryStream();
//serializer.WriteObject(stream, entity);
//stream.Position = 0;
//return (TEntity)serializer.ReadObject(stream)!;
}

/// <inheritdoc/>
Expand Down Expand Up @@ -192,7 +199,7 @@ public Task<long> CountAsync(IQueryFilter filter, CancellationToken cancellation
cancellationToken.ThrowIfCancellationRequested();

try {
var result = entities.Values.AsQueryable().LongCount(filter);
var result = Entities.AsQueryable().LongCount(filter);
return Task.FromResult(result);
} catch (Exception ex) {
throw new RepositoryException("Could not count the entities", ex);
Expand All @@ -207,7 +214,7 @@ public Task AddAsync(TEntity entity, CancellationToken cancellationToken = defau
var key = GenerateNewKey();
SetEntityId(entity, key);

entities.Add(key, Clone(entity));
entities.Add(key, new Entry(entity));

return Task.CompletedTask;
} catch (RepositoryException) {
Expand All @@ -227,7 +234,7 @@ public Task AddRangeAsync(IEnumerable<TEntity> entities, CancellationToken cance
var key = GenerateNewKey();
SetEntityId(item, key);

this.entities.Add(key, Clone(item));
this.entities.Add(key, new Entry(item));
}

return Task.CompletedTask;
Expand Down Expand Up @@ -302,7 +309,7 @@ public Task<bool> ExistsAsync(IQueryFilter filter, CancellationToken cancellatio
cancellationToken.ThrowIfCancellationRequested();

try {
var result = entities.Values.AsQueryable().Any(filter);
var result = Entities.AsQueryable().Any(filter);
return Task.FromResult(result);
} catch (Exception ex) {
throw new RepositoryException("Could not check if any entities exist in the repository", ex);
Expand All @@ -315,7 +322,7 @@ public Task<IList<TEntity>> FindAllAsync(IQuery query, CancellationToken cancell
cancellationToken.ThrowIfCancellationRequested();

try {
var result = query.Apply(entities.Values.AsQueryable()).ToList();
var result = query.Apply(Entities.AsQueryable()).ToList();

return Task.FromResult<IList<TEntity>>(result);
} catch (Exception ex) {
Expand All @@ -329,13 +336,28 @@ public Task<IList<TEntity>> FindAllAsync(IQuery query, CancellationToken cancell
cancellationToken.ThrowIfCancellationRequested();

try {
var result = query.Apply(entities.Values.AsQueryable()).FirstOrDefault();
var result = query.Apply(Entities.AsQueryable()).FirstOrDefault();
return Task.FromResult(result);
} catch (Exception ex) {
throw new RepositoryException("Error while searching for any entities in the repository matching the filter", ex);
}
}

/// <inheritdoc/>
public Task<TEntity?> FindOriginalAsync(TKey key, CancellationToken cancellationToken = default) {
ArgumentNullException.ThrowIfNull(key, nameof(key));
cancellationToken.ThrowIfCancellationRequested();

try {
if (!entities.TryGetValue(key, out var entry))
return Task.FromResult<TEntity?>(null);

return Task.FromResult<TEntity?>(entry.Original);
} catch (Exception ex) {
throw new RepositoryException("Error while searching any entities with the given ID", ex);
}
}

/// <inheritdoc/>
public Task<TEntity?> FindAsync(TKey key, CancellationToken cancellationToken = default) {
ArgumentNullException.ThrowIfNull(key, nameof(key));
Expand All @@ -346,7 +368,7 @@ public Task<IList<TEntity>> FindAllAsync(IQuery query, CancellationToken cancell
if (!entities.TryGetValue(key, out var entity))
return Task.FromResult<TEntity?>(null);

return Task.FromResult<TEntity?>(entity);
return Task.FromResult<TEntity?>(entity.Entity);
} catch (Exception ex) {
throw new RepositoryException("Error while searching any entities with the given ID", ex);
}
Expand Down Expand Up @@ -377,7 +399,7 @@ public Task<PageResult<TEntity>> GetPageAsync(PageQuery<TEntity> request, Cancel
cancellationToken.ThrowIfCancellationRequested();

try {
var entitySet = request.ApplyQuery(entities.Values.AsQueryable());
var entitySet = request.ApplyQuery(Entities.AsQueryable());

var itemCount = entitySet.Count();
var items = entitySet
Expand All @@ -403,10 +425,10 @@ public Task<bool> UpdateAsync(TEntity entity, CancellationToken cancellationToke
if (entityId == null)
return Task.FromResult(false);

if (!entities.TryGetValue(entityId, out var existing))
if (!entities.TryGetValue(entityId, out var entry))
return Task.FromResult(false);

entities[entityId] = entity;
entry.Entity = entity;
return Task.FromResult(true);
} catch (Exception ex) {
throw new RepositoryException("Unable to update the entity", ex);
Expand Down Expand Up @@ -438,5 +460,16 @@ public void Dispose() {
Dispose(disposing: true);
GC.SuppressFinalize(this);
}

class Entry {
public Entry(TEntity entity) {
Entity = entity;
Original = Clone(entity);
}

public TEntity Original { get; }

public TEntity Entity { get; set; }
}
}
}
21 changes: 21 additions & 0 deletions src/Deveel.Repository.Manager/Data/EntityManager_T.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,27 @@ public EntityManager(
ILoggerFactory? loggerFactory = null) : base(repository, WrapValidator(validator), cache, systemTime, errorFactory, services, loggerFactory) {
}

/// <inheritdoc/>
public override bool IsTrackingChanges {
get {
ThrowIfDisposed();

return (Repository is ITrackingRepository<TEntity> trackingRepository);
}
}

/// <inheritdoc/>
protected override ITrackingRepository<TEntity, object> TrackingRepository {
get {
ThrowIfDisposed();

if (!(Repository is ITrackingRepository<TEntity> trackingRepository))
throw new InvalidOperationException("The repository is not tracking changes.");

return trackingRepository;
}
}

private static IEntityValidator<TEntity, object>? WrapValidator(IEntityValidator<TEntity>? validator) =>
validator == null ? null : new EntityValidatorWrapper(validator);

Expand Down
Loading

0 comments on commit a8ce716

Please sign in to comment.