Browse Source

#246 Implement concurrency control for mongodb too.

pull/643/head
Halil ibrahim Kalkan 8 years ago
parent
commit
88e7cbabce
  1. 3
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/AggregateRoot.cs
  2. 25
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs
  3. 108
      framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs
  4. 9
      framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Domain/ConcurrencyStamp_Tests.cs
  5. 25
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/ConcurrencyStamp_Tests.cs

3
framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/AggregateRoot.cs

@ -24,6 +24,7 @@ namespace Volo.Abp.Domain.Entities
protected AggregateRoot()
{
ExtraProperties = new Dictionary<string, object>();
ConcurrencyStamp = Guid.NewGuid().ToString("N");
}
protected virtual void AddLocalEvent(object eventData)
@ -75,12 +76,14 @@ namespace Volo.Abp.Domain.Entities
protected AggregateRoot()
{
ExtraProperties = new Dictionary<string, object>();
ConcurrencyStamp = Guid.NewGuid().ToString("N");
}
protected AggregateRoot(TKey id)
: base(id)
{
ExtraProperties = new Dictionary<string, object>();
ConcurrencyStamp = Guid.NewGuid().ToString("N");
}
protected virtual void AddLocalEvent(object eventData)

25
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs

@ -201,13 +201,14 @@ namespace Volo.Abp.EntityFrameworkCore
protected virtual void ApplyAbpConceptsForAddedEntity(EntityEntry entry, EntityChangeReport changeReport)
{
CheckAndSetId(entry);
SetConcurrencyStampIfNull(entry);
SetCreationAuditProperties(entry);
changeReport.ChangedEntities.Add(new EntityChangeEntry(entry.Entity, EntityChangeType.Created));
}
protected virtual void ApplyAbpConceptsForModifiedEntity(EntityEntry entry, EntityChangeReport changeReport)
{
HandleConcurrencyStamp(entry);
UpdateConcurrencyStamp(entry);
SetModificationAuditProperties(entry);
if (entry.Entity is ISoftDelete && entry.Entity.As<ISoftDelete>().IsDeleted)
@ -224,7 +225,7 @@ namespace Volo.Abp.EntityFrameworkCore
protected virtual void ApplyAbpConceptsForDeletedEntity(EntityEntry entry, EntityChangeReport changeReport)
{
CancelDeletionForSoftDelete(entry);
HandleConcurrencyStamp(entry);
UpdateConcurrencyStamp(entry);
SetDeletionAuditProperties(entry);
changeReport.ChangedEntities.Add(new EntityChangeEntry(entry.Entity, EntityChangeType.Deleted));
}
@ -252,7 +253,7 @@ namespace Volo.Abp.EntityFrameworkCore
}
}
protected virtual void HandleConcurrencyStamp(EntityEntry entry)
protected virtual void UpdateConcurrencyStamp(EntityEntry entry)
{
var entity = entry.Entity as IHasConcurrencyStamp;
if (entity == null)
@ -260,7 +261,23 @@ namespace Volo.Abp.EntityFrameworkCore
return;
}
entity.ConcurrencyStamp = Guid.NewGuid().ToString();
entity.ConcurrencyStamp = Guid.NewGuid().ToString("N");
}
protected virtual void SetConcurrencyStampIfNull(EntityEntry entry)
{
var entity = entry.Entity as IHasConcurrencyStamp;
if (entity == null)
{
return;
}
if (entity.ConcurrencyStamp != null)
{
return;
}
entity.ConcurrencyStamp = Guid.NewGuid().ToString("N");
}
protected virtual void CancelDeletionForSoftDelete(EntityEntry entry)

108
framework/src/Volo.Abp.MongoDB/Volo/Abp/Domain/Repositories/MongoDB/MongoDbRepository.cs

@ -1,12 +1,13 @@
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;
using MongoDB.Driver.Linq;
using Volo.Abp.Auditing;
using Volo.Abp.Data;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Domain.Entities.Events;
using Volo.Abp.EventBus;
@ -97,11 +98,18 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
AsyncHelper.RunSync(() => TriggerDomainEventsAsync(entity));
Collection.ReplaceOne(
CreateEntityFilter(entity),
var oldConcurrencyStamp = SetNewConcurrencyStamp(entity);
var result = Collection.ReplaceOne(
CreateEntityFilter(entity, true, oldConcurrencyStamp),
entity
);
if (result.MatchedCount <= 0)
{
ThrowOptimisticConcurrencyException();
}
return entity;
}
@ -124,32 +132,50 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
await TriggerDomainEventsAsync(entity);
await Collection.ReplaceOneAsync(
CreateEntityFilter(entity),
var oldConcurrencyStamp = SetNewConcurrencyStamp(entity);
var result = await Collection.ReplaceOneAsync(
CreateEntityFilter(entity, true, oldConcurrencyStamp),
entity,
cancellationToken: GetCancellationToken(cancellationToken)
);
if (result.MatchedCount <= 0)
{
ThrowOptimisticConcurrencyException();
}
return entity;
}
public override void Delete(TEntity entity, bool autoSave = false)
{
AsyncHelper.RunSync(() => ApplyAbpConceptsForDeletedEntityAsync(entity));
var oldConcurrencyStamp = SetNewConcurrencyStamp(entity);
if (entity is ISoftDelete softDeleteEntity)
{
softDeleteEntity.IsDeleted = true;
Collection.ReplaceOne(
CreateEntityFilter(entity),
var result = Collection.ReplaceOne(
CreateEntityFilter(entity, true, oldConcurrencyStamp),
entity
);
if (result.MatchedCount <= 0)
{
ThrowOptimisticConcurrencyException();
}
}
else
{
Collection.DeleteOne(
CreateEntityFilter(entity)
var result = Collection.DeleteOne(
CreateEntityFilter(entity, true, oldConcurrencyStamp)
);
if (result.DeletedCount <= 0)
{
ThrowOptimisticConcurrencyException();
}
}
}
@ -159,22 +185,33 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
CancellationToken cancellationToken = default)
{
await ApplyAbpConceptsForDeletedEntityAsync(entity);
var oldConcurrencyStamp = SetNewConcurrencyStamp(entity);
if (entity is ISoftDelete softDeleteEntity)
{
softDeleteEntity.IsDeleted = true;
await Collection.ReplaceOneAsync(
CreateEntityFilter(entity),
var result = await Collection.ReplaceOneAsync(
CreateEntityFilter(entity, true, oldConcurrencyStamp),
entity,
cancellationToken: GetCancellationToken(cancellationToken)
);
if (result.MatchedCount <= 0)
{
ThrowOptimisticConcurrencyException();
}
}
else
{
await Collection.DeleteOneAsync(
CreateEntityFilter(entity),
var result = await Collection.DeleteOneAsync(
CreateEntityFilter(entity, true, oldConcurrencyStamp),
GetCancellationToken(cancellationToken)
);
if (result.DeletedCount <= 0)
{
ThrowOptimisticConcurrencyException();
}
}
}
@ -239,7 +276,7 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
);
}
protected virtual FilterDefinition<TEntity> CreateEntityFilter(TEntity entity)
protected virtual FilterDefinition<TEntity> CreateEntityFilter(TEntity entity, bool withConcurrencyStamp = false, string concurrencyStamp = null)
{
throw new NotImplementedException(
$"{nameof(CreateEntityFilter)} is not implemented for MongoDB by default. It should be overrided and implemented by the deriving class!"
@ -332,6 +369,28 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
generatesDomainEventsEntity.ClearDistributedEvents();
}
}
/// <summary>
/// Sets a new <see cref="IHasConcurrencyStamp.ConcurrencyStamp"/> value
/// if given entity implements <see cref="IHasConcurrencyStamp"/> interface.
/// Returns the old <see cref="IHasConcurrencyStamp.ConcurrencyStamp"/> value.
/// </summary>
protected virtual string SetNewConcurrencyStamp(TEntity entity)
{
if (!(entity is IHasConcurrencyStamp concurrencyStampEntity))
{
return null;
}
var oldConcurrencyStamp = concurrencyStampEntity.ConcurrencyStamp;
concurrencyStampEntity.ConcurrencyStamp = Guid.NewGuid().ToString("N");
return oldConcurrencyStamp;
}
protected virtual void ThrowOptimisticConcurrencyException()
{
throw new AbpDbConcurrencyException("Database operation expected to affect 1 row but actually affected 0 row. Data may have been modified or deleted since entities were loaded. This exception has been thrown on optimistic concurrency check.");
}
}
public class MongoDbRepository<TMongoDbContext, TEntity, TKey>
@ -390,7 +449,7 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
public virtual void Delete(TKey id, bool autoSave = false)
{
Collection.DeleteOne(CreateEntityFilter(id));
Collection.DeleteOne(CreateEntityFilter(id)); //TODO: How to handle concurrency stamp!
}
public virtual Task DeleteAsync(
@ -404,9 +463,22 @@ namespace Volo.Abp.Domain.Repositories.MongoDB
);
}
protected override FilterDefinition<TEntity> CreateEntityFilter(TEntity entity)
protected override FilterDefinition<TEntity> CreateEntityFilter(TEntity entity, bool withConcurrencyStamp = false, string concurrencyStamp = null)
{
return Builders<TEntity>.Filter.Eq(e => e.Id, entity.Id);
if (!withConcurrencyStamp || !(entity is IHasConcurrencyStamp entityWithConcurrencyStamp))
{
return Builders<TEntity>.Filter.Eq(e => e.Id, entity.Id);
}
if (concurrencyStamp == null)
{
concurrencyStamp = entityWithConcurrencyStamp.ConcurrencyStamp;
}
return Builders<TEntity>.Filter.And(
Builders<TEntity>.Filter.Eq(e => e.Id, entity.Id),
Builders<TEntity>.Filter.Eq(e => ((IHasConcurrencyStamp)e).ConcurrencyStamp, concurrencyStamp)
);
}
protected virtual FilterDefinition<TEntity> CreateEntityFilter(TKey id, bool applyFilters = false)

9
framework/test/Volo.Abp.MongoDB.Tests/Volo/Abp/MongoDB/Domain/ConcurrencyStamp_Tests.cs

@ -0,0 +1,9 @@
using Volo.Abp.TestApp.Testing;
namespace Volo.Abp.MongoDB.Domain
{
public class ConcurrencyStamp_Tests : ConcurrencyStamp_Tests<AbpMongoDbTestModule>
{
}
}

25
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Testing/ConcurrencyStamp_Tests.cs

@ -6,12 +6,12 @@ using Xunit;
namespace Volo.Abp.TestApp.Testing
{
public class ConcurrencyStamp_Tests<TStartupModule> : TestAppTestBase<TStartupModule>
public abstract class ConcurrencyStamp_Tests<TStartupModule> : TestAppTestBase<TStartupModule>
where TStartupModule : IAbpModule
{
protected readonly ICityRepository CityRepository;
public ConcurrencyStamp_Tests()
protected ConcurrencyStamp_Tests()
{
CityRepository = GetRequiredService<ICityRepository>();
}
@ -19,17 +19,38 @@ namespace Volo.Abp.TestApp.Testing
[Fact]
public async Task Should_Not_Allow_To_Update_If_The_Entity_Has_Changed()
{
//Got an entity from database, changed its value, but not updated in the database yet
var london1 = await CityRepository.FindByNameAsync("London");
london1.Name = "London-1";
//Another user has changed it just before I update
var london2 = await CityRepository.FindByNameAsync("London");
london2.Name = "London-2";
await CityRepository.UpdateAsync(london2);
//And updating my old entity throws exception!
await Assert.ThrowsAsync<AbpDbConcurrencyException>(async () =>
{
await CityRepository.UpdateAsync(london1);
});
}
[Fact]
public async Task Should_Not_Allow_To_Delete_If_The_Entity_Has_Changed()
{
//Got an entity from database, but not deleted in the database yet
var london1 = await CityRepository.FindByNameAsync("London");
//Another user has changed it just before I delete
var london2 = await CityRepository.FindByNameAsync("London");
london2.Name = "London-updated";
await CityRepository.UpdateAsync(london2);
//And deleting my old entity throws exception!
await Assert.ThrowsAsync<AbpDbConcurrencyException>(async () =>
{
await CityRepository.DeleteAsync(london1);
});
}
}
}

Loading…
Cancel
Save