diff --git a/src/YesSql.Abstractions/IQuery.cs b/src/YesSql.Abstractions/IQuery.cs index 5a5edf26..6b8cac95 100644 --- a/src/YesSql.Abstractions/IQuery.cs +++ b/src/YesSql.Abstractions/IQuery.cs @@ -63,13 +63,13 @@ public interface IQuery where T : class /// /// The index to filter on. IQuery With() where TIndex : class, IIndex; - + /// /// Filters the documents with a constraint on the specified index. /// /// The index to filter on. IQuery With(Expression> predicate) where TIndex : class, IIndex; - + /// /// Skips the specified number of document. /// @@ -128,7 +128,7 @@ public interface IQueryIndex where T : IIndex /// Joins the document table with an index, and filter it with a predicate. /// IQueryIndex With(Expression> predicate) where TIndex : class, IIndex; - + /// /// Adds a custom Where clause to the query. /// @@ -163,7 +163,7 @@ public interface IQueryIndex where T : IIndex /// Adds an OrderBy clause using a custom lambda expression. /// IQueryIndex ThenBy(Expression> keySelector); - + /// /// Adds a descending OrderBy clause using a custom lambda expression. /// @@ -221,7 +221,7 @@ public interface IQuery : IQuery /// Adds a custom Where clause to the query using a specific dialect. /// IQuery Where(Func sql); - + /// /// Adds a named parameter to the query. /// @@ -236,19 +236,19 @@ public interface IQuery : IQuery /// Sets an OrderBy clause using a custom lambda expression. /// IQuery OrderBy(Expression> keySelector); - + /// /// Sets an OrderBy clause using a custom SQL statement. /// IQuery OrderBy(string sql); - + IQuery OrderByDescending(Expression> keySelector); - + /// /// Sets a descending OrderBy clause using a custom SQL statement. /// IQuery OrderByDescending(string sql); - + /// /// Sets a random OrderBy clause. /// diff --git a/src/YesSql.Abstractions/ISession.cs b/src/YesSql.Abstractions/ISession.cs index 3ce0369d..e5080919 100644 --- a/src/YesSql.Abstractions/ISession.cs +++ b/src/YesSql.Abstractions/ISession.cs @@ -61,6 +61,16 @@ public interface ISession : IDisposable, IAsyncDisposable /// void Detach(object item, string collection = null); + /// + /// Removes multiple items from the identity map. + /// + /// + /// This method can be used to remove multiple items that should not be served again from the cache. + /// For instance when its state as changed and any subsequent query should not return the + /// modified instance but a fresh one. + /// + void Detach(IEnumerable entries, string collection = null); + /// /// Loads objects by id. /// diff --git a/src/YesSql.Abstractions/IStore.cs b/src/YesSql.Abstractions/IStore.cs index 15d73d9c..403d6e0f 100644 --- a/src/YesSql.Abstractions/IStore.cs +++ b/src/YesSql.Abstractions/IStore.cs @@ -10,7 +10,7 @@ public interface IStore : IDisposable /// /// Creates a new to communicate with the . /// - ISession CreateSession(); + ISession CreateSession(bool withTracking = true); /// /// Registers index providers. diff --git a/src/YesSql.Abstractions/SessionExtensions.cs b/src/YesSql.Abstractions/SessionExtensions.cs index bedc5665..f4e06a33 100644 --- a/src/YesSql.Abstractions/SessionExtensions.cs +++ b/src/YesSql.Abstractions/SessionExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Data; using System.Linq; @@ -13,16 +13,17 @@ public static class SessionExtensions /// Loads an object by its id. /// /// The object or null. - public async static Task GetAsync(this ISession session, long id, string collection = null) where T : class - { - return (await session.GetAsync(new[] { id }, collection)).FirstOrDefault(); - } + public async static Task GetAsync(this ISession session, long id, string collection = null) + where T : class + => (await session.GetAsync([id], collection)).FirstOrDefault(); /// /// Loads objects by id. /// /// A collection of objects in the same order they were defined. - public static Task> GetAsync(this ISession session, int[] ids, string collection = null) where T : class => session.GetAsync(ids.Select(x => (long)x).ToArray(), collection); + public static Task> GetAsync(this ISession session, int[] ids, string collection = null) + where T : class + => session.GetAsync(ids.Select(x => (long)x).ToArray(), collection); /// /// Imports an object in the local identity map. @@ -37,9 +38,7 @@ public async static Task GetAsync(this ISession session, long id, string c /// true if the object was imported, false otherwise. /// public static bool Import(this ISession session, object item, string collection = null) - { - return session.Import(item, 0, 0, collection); - } + => session.Import(item, 0, 0, collection); /// /// Registers index providers that are used only during the lifetime of this session. @@ -48,9 +47,7 @@ public static bool Import(this ISession session, object item, string collection /// The index providers to register. /// The instance. public static ISession RegisterIndexes(this ISession session, params IIndexProvider[] indexProviders) - { - return session.RegisterIndexes(indexProviders, null); - } + => session.RegisterIndexes(indexProviders, null); /// /// Registers index providers that are used only during the lifetime of this session. @@ -60,9 +57,7 @@ public static ISession RegisterIndexes(this ISession session, params IIndexProvi /// The name of the collection. /// The instance. public static ISession RegisterIndexes(this ISession session, IIndexProvider indexProvider, string collection = null) - { - return session.RegisterIndexes(new[] { indexProvider }, collection); - } + => session.RegisterIndexes([indexProvider], collection); /// /// Saves a new or existing object to the store, and updates diff --git a/src/YesSql.Core/Services/DefaultQuery.cs b/src/YesSql.Core/Services/DefaultQuery.cs index f47aec88..66de7368 100644 --- a/src/YesSql.Core/Services/DefaultQuery.cs +++ b/src/YesSql.Core/Services/DefaultQuery.cs @@ -36,7 +36,7 @@ public QueryState(ISqlBuilder sqlBuilder, IStore store, string collection) public List> _parameterBindings; public string _collection; public IStore _store; - internal CompositeNode _predicate; // the defaut root predicate is an AND expression + internal CompositeNode _predicate; // the default root predicate is an AND expression internal CompositeNode _currentPredicate; // the current predicate when Any() or All() is called public bool _processed = false; public bool _deduplicate = true; @@ -131,8 +131,7 @@ public class DefaultQuery : IQuery private readonly object _compiledQuery = null; private readonly string _collection; - public static Dictionary> MethodMappings = - new(); + public static Dictionary> MethodMappings = []; static DefaultQuery() { @@ -564,7 +563,7 @@ private ConstantExpression Evaluate(Expression expression) obj = null; } - _queryState._parameterBindings = _queryState._parameterBindings ?? new List>(); + _queryState._parameterBindings ??= new List>(); // Create a delegate that will be invoked every time a compiled query is reused, // which will re-evaluate the current node, for the current parameter. @@ -624,7 +623,7 @@ private ConstantExpression Evaluate(Expression expression) return Expression.Constant(Expression.Lambda(expression).Compile().DynamicInvoke()); } - private string GetBinaryOperator(Expression expression) + private static string GetBinaryOperator(Expression expression) { switch (expression.NodeType) { diff --git a/src/YesSql.Core/Session.cs b/src/YesSql.Core/Session.cs index 44059c4f..49b0faa5 100644 --- a/src/YesSql.Core/Session.cs +++ b/src/YesSql.Core/Session.cs @@ -33,18 +33,19 @@ public class Session : ISession protected string _tablePrefix; private readonly ISqlDialect _dialect; private readonly ILogger _logger; + private readonly bool _withTracking; - public Session(Store store) + public Session(Store store, bool withTracking = true) { _store = store; _tablePrefix = _store.Configuration.TablePrefix; _dialect = store.Dialect; _logger = store.Configuration.Logger; - + _withTracking = withTracking; _defaultState = new SessionState(); _collectionStates = new Dictionary() { - [""] = _defaultState + [string.Empty] = _defaultState }; } @@ -58,7 +59,7 @@ public ISession RegisterIndexes(IIndexProvider[] indexProviders, string collecti } } - _indexes ??= new List(); + _indexes ??= []; _indexes.AddRange(indexProviders); @@ -81,7 +82,6 @@ private SessionState GetState(string collection) return state; } - [Obsolete] public void Save(object entity, bool checkConcurrency = false, string collection = null) => SaveAsync(entity, checkConcurrency, collection).GetAwaiter().GetResult(); @@ -140,10 +140,7 @@ public async Task SaveAsync(object entity, bool checkConcurrency = false, string state.IdentityMap.AddEntity(id, entity); // Then assign a new identifier if it has one - if (accessor != null) - { - accessor.Set(entity, id); - } + accessor?.Set(entity, id); state.Saved.Add(entity); } @@ -222,6 +219,23 @@ public void Detach(object entity, string collection) var state = GetState(collection); + DetachInternal(entity, state); + } + + public void Detach(IEnumerable entries, string collection) + { + CheckDisposed(); + + var state = GetState(collection); + + foreach (var entry in entries) + { + DetachInternal(entry, state); + } + } + + private static void DetachInternal(object entity, SessionState state) + { state.Saved.Remove(entity); state.Updated.Remove(entity); state.Tracked.Remove(entity); @@ -277,14 +291,11 @@ private async Task SaveEntityAsync(object entity, string collection) doc.Version = 1; } - if (versionAccessor != null) - { - versionAccessor.Set(entity, doc.Version); - } + versionAccessor?.Set(entity, doc.Version); doc.Content = Store.Configuration.ContentSerializer.Serialize(entity); - _commands ??= new List(); + _commands ??= []; _commands.Add(new CreateDocumentCommand(doc, Store, collection)); @@ -300,14 +311,12 @@ private async Task UpdateEntityAsync(object entity, bool tracked, string collect throw new ArgumentNullException(nameof(entity)); } - var index = entity as IIndex; - if (entity is Document) { throw new ArgumentException("A document should not be saved explicitly"); } - if (index != null) + if (entity is IIndex index) { throw new ArgumentException("An index should not be saved explicitly"); } @@ -379,7 +388,7 @@ private async Task UpdateEntityAsync(object entity, bool tracked, string collect oldDoc.Content = newContent; - _commands ??= new List(); + _commands ??= []; _commands.Add(new UpdateDocumentCommand(oldDoc, Store, version, collection)); } @@ -463,7 +472,7 @@ private async Task DeleteEntityAsync(object obj, string collection) // Update impacted indexes await MapDeleted(doc, obj, collection); - _commands ??= new List(); + _commands ??= []; // The command needs to come after any index deletion because of the database constraints _commands.Add(new DeleteDocumentCommand(doc, Store, collection)); @@ -535,7 +544,7 @@ public IEnumerable Get(IList documents, string collection) where // Are all the objects already in cache? foreach (var d in documents) { - if (state.IdentityMap.TryGetEntityById(d.Id, out var entity)) + if (_withTracking && state.IdentityMap.TryGetEntityById(d.Id, out var entity)) { result.Add((T)entity); } @@ -568,9 +577,12 @@ public IEnumerable Get(IList documents, string collection) where accessor?.Set(item, d.Id); - // track the loaded object - state.IdentityMap.AddEntity(d.Id, item); - state.IdentityMap.AddDocument(d); + if (_withTracking) + { + // track the loaded object. + state.IdentityMap.AddEntity(d.Id, item); + state.IdentityMap.AddDocument(d); + } result.Add(item); } @@ -657,7 +669,7 @@ public async Task FlushAsync() } // prevent recursive calls in FlushAsync, - // when autoflush is triggered from an IndexProvider + // when auto-flush is triggered from an IndexProvider // for instance. if (_flushing) diff --git a/src/YesSql.Core/Store.cs b/src/YesSql.Core/Store.cs index b54376b5..bd5de1e4 100644 --- a/src/YesSql.Core/Store.cs +++ b/src/YesSql.Core/Store.cs @@ -57,8 +57,8 @@ static Store() private Store() { - Indexes = new List(); - ScopedIndexes = new List(); + Indexes = []; + ScopedIndexes = []; } /// @@ -204,8 +204,8 @@ private void ValidateConfiguration() } } - public ISession CreateSession() - => new Session(this); + public ISession CreateSession(bool withTracking = true) + => new Session(this, withTracking); public void Dispose() { diff --git a/test/YesSql.Tests/CoreTests.cs b/test/YesSql.Tests/CoreTests.cs index e754ed54..4f171e08 100644 --- a/test/YesSql.Tests/CoreTests.cs +++ b/test/YesSql.Tests/CoreTests.cs @@ -935,6 +935,24 @@ public async Task ShouldKeepIdentityMapOnCommitAsync() Assert.Equal(bill, newBill); } + [Fact] + public async Task ShouldNotKeepIdentityMapOnCommitAsync() + { + await using var session = _store.CreateSession(false); + var bill = new Person + { + Firstname = "Bill", + Lastname = "Gates" + }; + + await session.SaveAsync(bill); + await session.SaveChangesAsync(); + + var newBill = await session.GetAsync(bill.Id); + + Assert.NotEqual(bill, newBill); + } + [Fact] public async Task ShouldUpdateAutoFlushedIndex() { @@ -5476,6 +5494,46 @@ public async Task ShouldDetachEntity() await session.SaveChangesAsync(); } + [Fact] + public async Task ShouldMultipleDetachEntity() + { + await using var session = _store.CreateSession(); + var bill = new Person + { + Firstname = "Bill", + Lastname = "Gates" + }; + + var john = new Person + { + Firstname = "John", + Lastname = "Smith" + }; + + await session.SaveAsync(bill); + await session.SaveAsync(john); + + var newBill = await session.GetAsync(bill.Id); + + Assert.Equal(bill, newBill); + + var newJohn = await session.GetAsync(john.Id); + + Assert.Equal(john, newJohn); + + session.Detach([bill, john]); + + newBill = await session.GetAsync(bill.Id); + + Assert.NotEqual(bill, newBill); + + newJohn = await session.GetAsync(john.Id); + + Assert.NotEqual(john, newJohn); + + await session.SaveChangesAsync(); + } + [Fact] public async Task ShouldStoreBinaryInIndex() {