Skip to content

Commit

Permalink
(#751) Moved over to AsNoTracking instead of disconnecting manually. …
Browse files Browse the repository at this point in the history
…Also updated tests to increase code coverage. (#767)
  • Loading branch information
adrianhall committed Sep 25, 2023
1 parent 8774a4c commit 202dc69
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 40 deletions.
Expand Up @@ -58,7 +58,7 @@ public EntityTableRepository(DbContext context)
/// paging requests.
/// </summary>
/// <returns>An <see cref="IQueryable{T}"/> for the entities in the data store.</returns>
public IQueryable<TEntity> AsQueryable() => DataSet.AsQueryable();
public IQueryable<TEntity> AsQueryable() => DataSet.AsNoTracking();

/// <summary>
/// Returns an unexecuted <see cref="IQueryable{T}"/> that represents the data store as a whole.
Expand All @@ -85,15 +85,11 @@ public async Task CreateAsync(TEntity entity, CancellationToken token = default)

try
{
var storeEntity = entity.Id == null ? null : await LookupAsync(entity.Id, token).ConfigureAwait(false);
if (storeEntity != null)
if (entity.Id != null && DataSet.Any(x => x.Id == entity.Id))
{
throw new ConflictException(Disconnect(storeEntity));
}
if (entity.Id == null)
{
entity.Id = Guid.NewGuid().ToString("N");
throw new ConflictException(await LookupUntrackedAsync(entity.Id, token).ConfigureAwait(false));
}
entity.Id ??= Guid.NewGuid().ToString("N");
entity.UpdatedAt = DateTimeOffset.UtcNow;
DataSet.Add(entity);
await Context.SaveChangesAsync(token).ConfigureAwait(false);
Expand Down Expand Up @@ -123,17 +119,11 @@ public async Task DeleteAsync(string id, byte[] version = null, CancellationToke

try
{
var storeEntity = await LookupAsync(id, token).ConfigureAwait(false);
if (storeEntity == null)
{
throw new NotFoundException();
}

if (version != null && storeEntity.Version?.SequenceEqual(version) != true)
var storeEntity = await LookupAsync(id, token).ConfigureAwait(false) ?? throw new NotFoundException();
if (PreconditionFailed(version, storeEntity.Version))
{
throw new PreconditionFailedException(Disconnect(storeEntity));
throw new PreconditionFailedException(await LookupUntrackedAsync(id, token).ConfigureAwait(false));
}

DataSet.Remove(storeEntity);
await Context.SaveChangesAsync(token).ConfigureAwait(false);
}
Expand All @@ -156,14 +146,13 @@ public async Task DeleteAsync(string id, byte[] version = null, CancellationToke
/// <param name="token">A cancellation token</param>
/// <returns>The entity, or null if the entity does not exist.</returns>
/// <exception cref="RepositoryException">if an error occurs in the data store.</exception>
public async Task<TEntity> ReadAsync(string id, CancellationToken token = default)
public Task<TEntity> ReadAsync(string id, CancellationToken token = default)
{
if (string.IsNullOrEmpty(id))
{
throw new BadRequestException();
}

return Disconnect(await LookupAsync(id, token).ConfigureAwait(false));
return LookupUntrackedAsync(id, token);
}

/// <summary>
Expand Down Expand Up @@ -191,17 +180,11 @@ public async Task ReplaceAsync(TEntity entity, byte[] version = null, Cancellati

try
{
var storeEntity = await LookupAsync(entity.Id, token).ConfigureAwait(false);
if (storeEntity == null)
var storeEntity = await LookupAsync(entity.Id, token).ConfigureAwait(false) ?? throw new NotFoundException();
if (PreconditionFailed(version, storeEntity.Version))
{
throw new NotFoundException();
throw new PreconditionFailedException(await LookupUntrackedAsync(entity.Id, token).ConfigureAwait(false));
}

if (version != null && storeEntity.Version?.SequenceEqual(version) != true)
{
throw new PreconditionFailedException(Disconnect(storeEntity));
}

entity.UpdatedAt = DateTimeOffset.UtcNow;
Context.Entry(storeEntity).CurrentValues.SetValues(entity);
await Context.SaveChangesAsync(token).ConfigureAwait(false);
Expand All @@ -226,11 +209,21 @@ internal ValueTask<TEntity> LookupAsync(string id, CancellationToken token = def
=> DataSet.FindAsync(new[] { id }, token);

/// <summary>
/// Gets a disconnected (as much as we can) copy of the entity provided.
/// Obtains a disconnected entity from the data set.
/// </summary>
/// <param name="id">The ID of the entity</param>
/// <param name="token">A <see cref="CancellationToken"/></param>
/// <returns>The entity, or null if no entity exists.</returns>
internal Task<TEntity> LookupUntrackedAsync(string id, CancellationToken token = default)
=> DataSet.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, token);

/// <summary>
/// Checks that the version provided matches the version in the database.
/// </summary>
/// <param name="entity">The entity to disconnect</param>
/// <returns>A non-tracked version of the entity.</returns>
private TEntity Disconnect(TEntity entity)
=> entity == null ? null : Context.Entry(entity).CurrentValues.Clone().ToObject() as TEntity;
/// <param name="requiredVersion">The requ</param>
/// <param name="currentVersion"></param>
/// <returns>True if we need to throw a <see cref="PreconditionFailedException"/>.</returns>
internal static bool PreconditionFailed(byte[] expectedVersion, byte[] currentVersion)
=> expectedVersion != null && currentVersion?.SequenceEqual(expectedVersion) != true;
}
}
Expand Up @@ -184,7 +184,7 @@ internal void DeleteEntity(string id, byte[] version = null)
throw new NotFoundException();
}

if (version != null && existingEntity.Version?.SequenceEqual(version) != true)
if (PreconditionFailed(version, existingEntity.Version))
{
throw new PreconditionFailedException(existingEntity);
}
Expand Down Expand Up @@ -220,7 +220,7 @@ internal void ReplaceEntity(TEntity entity, byte[] version = null)
throw new NotFoundException();
}

if (version != null && existingEntity.Version?.SequenceEqual(version) != true)
if (PreconditionFailed(version, existingEntity.Version))
{
throw new PreconditionFailedException(existingEntity);
}
Expand All @@ -229,5 +229,14 @@ internal void ReplaceEntity(TEntity entity, byte[] version = null)
}
}
#endregion

/// <summary>
/// Checks that the version provided matches the version in the database.
/// </summary>
/// <param name="requiredVersion">The requ</param>
/// <param name="currentVersion"></param>
/// <returns>True if we need to throw a <see cref="PreconditionFailedException"/>.</returns>
internal static bool PreconditionFailed(byte[] expectedVersion, byte[] currentVersion)
=> expectedVersion != null && currentVersion?.SequenceEqual(expectedVersion) != true;
}
}
@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved.
// Licensed under the MIT License.

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Datasync.LiteDb.Test")]
Expand Up @@ -84,4 +84,44 @@ public void Version_IsSet_WhenEntityTagUpdated()
// Assert
Assert.Equal(new byte[] { 0x61, 0x62, 0x63, 0x64 }, source.Version);
}

[Fact]
public void Version_Empty_WhenEntityTagEmpty()
{
// Arrange
var source = new Entity { Id = "test", EntityTag = null, UpdatedAt = DateTimeOffset.Now };

// Assert
Assert.Empty(source.Version);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void Deleted_CanBeRead(bool deleted)
{
// Arrange
var source = new Entity { Id = "test", EntityTag = null, UpdatedAt = DateTimeOffset.Now, Deleted = deleted };

// Assert
Assert.Equal(deleted, source.Deleted);
}

[Theory, CombinatorialData]
public void Equals_Works([CombinatorialRange(0, 5)] int offset)
{
DateTimeOffset dto = DateTimeOffset.Parse("2021-01-01T01:00:00Z");
List<Entity> testEntities = new()
{
null,
new Entity { Id = "nottest", EntityTag = "abcd", UpdatedAt = dto, Deleted = false },
new Entity { Id = "test", EntityTag = "efgh", UpdatedAt = dto, Deleted = false },
new Entity { Id = "test", EntityTag = "abcd", UpdatedAt = DateTimeOffset.UtcNow, Deleted = false },
new Entity { Id = "test", EntityTag = "abcd", UpdatedAt = dto, Deleted = true }
};
var test = testEntities[offset];
var source = new Entity { Id = "test", EntityTag = "abcd", UpdatedAt = DateTimeOffset.Parse("2021-01-01T01:00:00Z"), Deleted = false };

Assert.False(source.Equals(test));
}
}
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved.
// Licensed under the MIT License.

using Microsoft.AspNetCore.Datasync.LiteDb;

namespace Microsoft.AspNetCore.Datasync.EFCore.Test;

[ExcludeFromCodeCoverage]
Expand Down Expand Up @@ -413,4 +415,16 @@ public async Task ReplaceAsync_Throws_OnDbError()

Assert.NotNull(ex.InnerException);
}

[Theory]
[InlineData(true, true, false)]
[InlineData(true, false, false)]
[InlineData(false, true, true)]
public void PreconditionFailed_Works(bool v1IsNull, bool v2IsNull, bool expected)
{
byte[] v1 = v1IsNull ? null : new byte[] { 0x0A, 0x0B, 0x0C };
byte[] v2 = v2IsNull ? null : new byte[] { 0x0A, 0x0B, 0x0C };

Assert.Equal(expected, EntityTableRepository<EFMovie>.PreconditionFailed(v1, v2));
}
}
Expand Up @@ -83,13 +83,11 @@ public void AsQueryable_CanRetrieveFilteredLists()
Assert.Equal(95, ratedMovies.Count);
}

#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
[Fact]
public async Task CreateAsync_Throws_Null()
{
await Assert.ThrowsAsync<ArgumentNullException>(() => repository.CreateAsync(null));
}
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.

[Fact]
public async Task CreateAsync_CreatesNewEntity_WithSpecifiedId()
Expand Down Expand Up @@ -284,13 +282,11 @@ public async Task ReadAsync_ReturnsNull_IfMissing(string id)
Assert.Null(actual);
}

#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type.
[Fact]
public async Task ReplaceAsync_Throws_OnNull()
{
await Assert.ThrowsAsync<ArgumentNullException>(() => repository.ReplaceAsync(null));
}
#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type.

[Theory]
[InlineData(null)]
Expand Down Expand Up @@ -376,4 +372,16 @@ public async Task ReplaceAsync_Throws_WhenEntityVersionNull()
Assert.NotSame(original, ex.Payload);
Assert.Equal(original, ex.Payload as IMovie);
}

[Theory]
[InlineData(true, true, false)]
[InlineData(true, false, false)]
[InlineData(false, true, true)]
public void PreconditionFailed_Works(bool v1IsNull, bool v2IsNull, bool expected)
{
byte[] v1 = v1IsNull ? null : new byte[] { 0x0A, 0x0B, 0x0C };
byte[] v2 = v2IsNull ? null : new byte[] { 0x0A, 0x0B, 0x0C };

Assert.Equal(expected, LiteDbRepository<LiteDbMovie>.PreconditionFailed(v1, v2));
}
}

0 comments on commit 202dc69

Please sign in to comment.