diff --git a/Source/EntityFramework.Extended/Batch/IBatchRunner.cs b/Source/EntityFramework.Extended/Batch/IBatchRunner.cs index bb53626..50da907 100644 --- a/Source/EntityFramework.Extended/Batch/IBatchRunner.cs +++ b/Source/EntityFramework.Extended/Batch/IBatchRunner.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Data.Entity.Core.Objects; using System.Linq.Expressions; +using System.Threading.Tasks; using EntityFramework.Mapping; namespace EntityFramework.Batch @@ -22,6 +23,17 @@ public interface IBatchRunner int Delete(ObjectContext objectContext, EntityMap entityMap, ObjectQuery query) where TEntity : class; + /// + /// Create and runs a batch delete statement asynchronously. + /// + /// The type of the entity. + /// The to get connection and metadata information from. + /// The for . + /// The query to create the where clause from. + /// The number of rows deleted. + Task DeleteAsync(ObjectContext objectContext, EntityMap entityMap, ObjectQuery query) + where TEntity : class; + /// /// Create and runs a batch update statement. /// @@ -33,5 +45,17 @@ int Delete(ObjectContext objectContext, EntityMap entityMap, ObjectQuer /// The number of rows updated. int Update(ObjectContext objectContext, EntityMap entityMap, ObjectQuery query, Expression> updateExpression) where TEntity : class; + + /// + /// Create and runs a batch update statement asynchronously. + /// + /// The type of the entity. + /// The to get connection and metadata information from. + /// The for . + /// The query to create the where clause from. + /// The update expression. + /// The number of rows updated. + Task UpdateAsync(ObjectContext objectContext, EntityMap entityMap, ObjectQuery query, Expression> updateExpression) + where TEntity : class; } } diff --git a/Source/EntityFramework.Extended/Batch/SqlServerBatchRunner.cs b/Source/EntityFramework.Extended/Batch/SqlServerBatchRunner.cs index a8c494a..e01baf8 100644 --- a/Source/EntityFramework.Extended/Batch/SqlServerBatchRunner.cs +++ b/Source/EntityFramework.Extended/Batch/SqlServerBatchRunner.cs @@ -8,6 +8,7 @@ using System.Linq.Expressions; using System.Text; using System.Text.RegularExpressions; +using System.Threading.Tasks; using EntityFramework.Extensions; using EntityFramework.Mapping; using EntityFramework.Reflection; @@ -29,7 +30,27 @@ public class SqlServerBatchRunner : IBatchRunner /// /// The number of rows deleted. /// - public int Delete(ObjectContext objectContext, EntityMap entityMap, ObjectQuery query) + public int Delete(ObjectContext objectContext, EntityMap entityMap, ObjectQuery query) where TEntity : class + { + return InternalDelete(objectContext, entityMap, query, false).Result; + } + + /// + /// Create and run a batch delete statement asynchronously. + /// + /// The type of the entity. + /// The to get connection and metadata information from. + /// The for . + /// The query to create the where clause from. + /// + /// The number of rows deleted. + /// + public Task DeleteAsync(ObjectContext objectContext, EntityMap entityMap, ObjectQuery query) where TEntity : class + { + return InternalDelete(objectContext, entityMap, query, true); + } + + private async Task InternalDelete(ObjectContext objectContext, EntityMap entityMap, ObjectQuery query, bool async = false) where TEntity : class { DbConnection deleteConnection = null; @@ -89,7 +110,9 @@ public int Delete(ObjectContext objectContext, EntityMap entityMap, Obj deleteCommand.CommandText = sqlBuilder.ToString(); - int result = deleteCommand.ExecuteNonQuery(); + int result = async + ? await deleteCommand.ExecuteNonQueryAsync() + : deleteCommand.ExecuteNonQuery(); // only commit if created transaction if (ownTransaction) @@ -121,7 +144,28 @@ public int Delete(ObjectContext objectContext, EntityMap entityMap, Obj /// /// The number of rows updated. /// - public int Update(ObjectContext objectContext, EntityMap entityMap, ObjectQuery query, Expression> updateExpression) + public int Update(ObjectContext objectContext, EntityMap entityMap, ObjectQuery query, Expression> updateExpression) where TEntity : class + { + return InternalUpdate(objectContext, entityMap, query, updateExpression, false).Result; + } + + /// + /// Create and run a batch update statement asynchronously. + /// + /// The type of the entity. + /// The to get connection and metadata information from. + /// The for . + /// The query to create the where clause from. + /// The update expression. + /// + /// The number of rows updated. + /// + public Task UpdateAsync(ObjectContext objectContext, EntityMap entityMap, ObjectQuery query, Expression> updateExpression) where TEntity : class + { + return InternalUpdate(objectContext, entityMap, query, updateExpression, true); + } + + private async Task InternalUpdate(ObjectContext objectContext, EntityMap entityMap, ObjectQuery query, Expression> updateExpression, bool async = false) where TEntity : class { DbConnection updateConnection = null; @@ -297,7 +341,9 @@ public int Update(ObjectContext objectContext, EntityMap entityMap, Obj updateCommand.CommandText = sqlBuilder.ToString(); - int result = updateCommand.ExecuteNonQuery(); + int result = async + ? await updateCommand.ExecuteNonQueryAsync() + : updateCommand.ExecuteNonQuery(); // only commit if created transaction if (ownTransaction) diff --git a/Source/EntityFramework.Extended/Caching/CacheManager.cs b/Source/EntityFramework.Extended/Caching/CacheManager.cs index e155ca9..99e1e55 100644 --- a/Source/EntityFramework.Extended/Caching/CacheManager.cs +++ b/Source/EntityFramework.Extended/Caching/CacheManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading.Tasks; namespace EntityFramework.Caching { @@ -285,6 +286,23 @@ public virtual object GetOrAdd(CacheKey cacheKey, Func valueFa return item; } + /// + /// Gets the cache value for the specified key that is already in the dictionary or the new value for the key as returned asynchronously by . + /// + /// A unique identifier for the cache entry. + /// The asynchronous function used to generate a value to insert into cache. + /// An object that contains eviction details for the cache entry. + /// + /// The value for the key. This will be either the existing value for the key if the key is already in the dictionary, + /// or the new value for the key as returned by if the key was not in the dictionary. + /// + public virtual Task GetOrAddAsync(CacheKey cacheKey, Func> valueFactory, CachePolicy cachePolicy) + { + var provider = ResolveProvider(); + var item = provider.GetOrAddAsync(cacheKey, valueFactory, cachePolicy); + + return item; + } /// /// Removes a cache entry from the cache. diff --git a/Source/EntityFramework.Extended/Caching/ICacheProvider.cs b/Source/EntityFramework.Extended/Caching/ICacheProvider.cs index e945c36..0077ecc 100644 --- a/Source/EntityFramework.Extended/Caching/ICacheProvider.cs +++ b/Source/EntityFramework.Extended/Caching/ICacheProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using System.Threading.Tasks; namespace EntityFramework.Caching { @@ -39,6 +40,18 @@ public interface ICacheProvider /// object GetOrAdd(CacheKey cacheKey, Func valueFactory, CachePolicy cachePolicy); + /// + /// Gets the cache value for the specified key that is already in the dictionary or the new value for the key as returned asynchronously by . + /// + /// A unique identifier for the cache entry. + /// The asynchronous function used to generate a value to insert into cache. + /// A that contains eviction details for the cache entry. + /// + /// The value for the key. This will be either the existing value for the key if the key is already in the cache, + /// or the new value for the key as returned by if the key was not in the cache. + /// + Task GetOrAddAsync(CacheKey cacheKey, Func> valueFactory, CachePolicy cachePolicy); + /// /// Removes a cache entry from the cache. /// diff --git a/Source/EntityFramework.Extended/Caching/MemoryCacheProvider.cs b/Source/EntityFramework.Extended/Caching/MemoryCacheProvider.cs index 8992548..89e58f6 100644 --- a/Source/EntityFramework.Extended/Caching/MemoryCacheProvider.cs +++ b/Source/EntityFramework.Extended/Caching/MemoryCacheProvider.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Linq; using System.Runtime.Caching; +using System.Threading.Tasks; namespace EntityFramework.Caching { @@ -76,6 +77,37 @@ public object GetOrAdd(CacheKey cacheKey, Func valueFactory, C return value; } + /// + /// Gets the cache value for the specified key that is already in the dictionary or the new value for the key as returned asynchronously by . + /// + /// A unique identifier for the cache entry. + /// The asynchronous function used to generate a value to insert into cache. + /// A that contains eviction details for the cache entry. + /// + /// The value for the key. This will be either the existing value for the key if the key is already in the cache, + /// or the new value for the key as returned by if the key was not in the cache. + /// + public async Task GetOrAddAsync(CacheKey cacheKey, Func> valueFactory, CachePolicy cachePolicy) + { + var key = GetKey(cacheKey); + var cachedResult = MemoryCache.Default.Get(key); + + if (cachedResult != null) + { + Debug.WriteLine("Cache Hit : " + key); + return cachedResult; + } + + Debug.WriteLine("Cache Miss: " + key); + + // get value and add to cache, not bothered + // if it succeeds or not just rerturn the value + var value = await valueFactory(cacheKey); + this.Add(cacheKey, value, cachePolicy); + + return value; + } + /// /// Removes a cache entry from the cache. /// diff --git a/Source/EntityFramework.Extended/Extensions/BatchExtensions.cs b/Source/EntityFramework.Extended/Extensions/BatchExtensions.cs index 29339cb..95e7b4a 100644 --- a/Source/EntityFramework.Extended/Extensions/BatchExtensions.cs +++ b/Source/EntityFramework.Extended/Extensions/BatchExtensions.cs @@ -3,6 +3,7 @@ using System.Data.Entity.Core.Objects; using System.Linq; using System.Linq.Expressions; +using System.Threading.Tasks; using EntityFramework.Batch; using EntityFramework.Mapping; @@ -66,6 +67,38 @@ public static int Delete( return source.Where(filterExpression).Delete(); } + /// + /// Executes a delete statement asynchronously using an expression to filter the rows to be deleted. + /// + /// The type of the entity. + /// The source used to determine the table to delete from. + /// The filter expression used to generate the where clause for the delete statement. + /// The number of row deleted. + /// Delete all users with email domain @test.com. + /// u.Email.EndsWith(emailDomain)); + /// ]]> + /// + /// + /// When executing this method, the statement is immediately executed on the database provider + /// and is not part of the change tracking system. Also, changes will not be reflected on + /// any entities that have already been materialized in the current context. + /// + public static Task DeleteAsync( + this IQueryable source, + Expression> filterExpression) + where TEntity : class + { + if (source == null) + throw new ArgumentNullException("source"); + if (filterExpression == null) + throw new ArgumentNullException("filterExpression"); + + return source.Where(filterExpression).DeleteAsync(); + } + /// /// Executes a delete statement using the query to filter the rows to be deleted. /// @@ -102,7 +135,44 @@ public static int Delete(this IQueryable source) var runner = ResolveRunner(); return runner.Delete(objectContext, entityMap, sourceQuery); } - + + /// + /// Executes a delete statement asynchronously using the query to filter the rows to be deleted. + /// + /// The type of the entity. + /// The used to generate the where clause for the delete statement. + /// The number of row deleted. + /// Delete all users with email domain @test.com. + /// u.Email.EndsWith(emailDomain)).Delete(); + /// ]]> + /// + /// + /// When executing this method, the statement is immediately executed on the database provider + /// and is not part of the change tracking system. Also, changes will not be reflected on + /// any entities that have already been materialized in the current context. + /// + public static Task DeleteAsync(this IQueryable source) + where TEntity : class + { + ObjectQuery sourceQuery = source.ToObjectQuery(); + if (sourceQuery == null) + throw new ArgumentException("The query must be of type ObjectQuery or DbQuery.", "source"); + + ObjectContext objectContext = sourceQuery.Context; + if (objectContext == null) + throw new ArgumentException("The ObjectContext for the source query can not be null.", "source"); + + EntityMap entityMap = sourceQuery.GetEntityMap(); + if (entityMap == null) + throw new ArgumentException("Could not load the entity mapping information for the query ObjectSet.", "source"); + + var runner = ResolveRunner(); + return runner.DeleteAsync(objectContext, entityMap, sourceQuery); + } + /// /// The API was refactored to no longer need this extension method. Use query.Where(expression).Update(updateExpression) syntax instead. /// @@ -186,6 +256,42 @@ public static int Update( return source.Where(filterExpression).Update(updateExpression); } + /// + /// Executes an update statement asynchronously using an expression to filter the rows that are updated. + /// + /// The type of the entity. + /// The source used to determine the table to update. + /// The filter expression used to generate the where clause. + /// The used to indicate what is updated. + /// The number of row updated. + /// Update all users in the test.com domain to be inactive. + /// u.Email.EndsWith(emailDomain), + /// u => new User { IsApproved = false, LastActivityDate = DateTime.Now }); + /// ]]> + /// + /// + /// When executing this method, the statement is immediately executed on the database provider + /// and is not part of the change tracking system. Also, changes will not be reflected on + /// any entities that have already been materialized in the current context. + /// + public static Task UpdateAsync( + this IQueryable source, + Expression> filterExpression, + Expression> updateExpression) + where TEntity : class + { + if (source == null) + throw new ArgumentNullException("source"); + if (filterExpression == null) + throw new ArgumentNullException("filterExpression"); + + return source.Where(filterExpression).UpdateAsync(updateExpression); + } + /// /// Executes an update statement using the query to filter the rows to be updated. /// @@ -233,6 +339,53 @@ public static int Update( return runner.Update(objectContext, entityMap, sourceQuery, updateExpression); } + /// + /// Executes an update statement asynchronously using the query to filter the rows to be updated. + /// + /// The type of the entity. + /// The query used to generate the where clause. + /// The used to indicate what is updated. + /// The number of row updated. + /// Update all users in the test.com domain to be inactive. + /// u.Email.EndsWith(emailDomain)) + /// .Update(u => new User { IsApproved = false, LastActivityDate = DateTime.Now }); + /// ]]> + /// + /// + /// When executing this method, the statement is immediately executed on the database provider + /// and is not part of the change tracking system. Also, changes will not be reflected on + /// any entities that have already been materialized in the current context. + /// + public static Task UpdateAsync( + this IQueryable source, + Expression> updateExpression) + where TEntity : class + { + if (source == null) + throw new ArgumentNullException("source"); + if (updateExpression == null) + throw new ArgumentNullException("updateExpression"); + + ObjectQuery sourceQuery = source.ToObjectQuery(); + if (sourceQuery == null) + throw new ArgumentException("The query must be of type ObjectQuery or DbQuery.", "source"); + + ObjectContext objectContext = sourceQuery.Context; + if (objectContext == null) + throw new ArgumentException("The ObjectContext for the query can not be null.", "source"); + + EntityMap entityMap = sourceQuery.GetEntityMap(); + if (entityMap == null) + throw new ArgumentException("Could not load the entity mapping information for the source.", "source"); + + var runner = ResolveRunner(); + return runner.UpdateAsync(objectContext, entityMap, sourceQuery, updateExpression); + } + private static IBatchRunner ResolveRunner() { var provider = Locator.Current.Resolve(); diff --git a/Source/EntityFramework.Extended/Extensions/CacheExtensions.cs b/Source/EntityFramework.Extended/Extensions/CacheExtensions.cs index 487deaf..75623cd 100644 --- a/Source/EntityFramework.Extended/Extensions/CacheExtensions.cs +++ b/Source/EntityFramework.Extended/Extensions/CacheExtensions.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Data.Entity; using System.Text; +using System.Threading.Tasks; using EntityFramework.Caching; namespace EntityFramework.Extensions @@ -23,7 +24,7 @@ public static class CacheExtensions /// /// The result of the query. /// - public static IEnumerable FromCache(this IQueryable query, CachePolicy cachePolicy = null, IEnumerable tags = null) + public static IList FromCache(this IQueryable query, CachePolicy cachePolicy = null, IEnumerable tags = null) where TEntity : class { string key = query.GetCacheKey(); @@ -37,7 +38,37 @@ public static IEnumerable FromCache(this IQueryable q cacheKey, k => query.AsNoTracking().ToList(), cachePolicy ?? CachePolicy.Default - ) as IEnumerable; + ) as IList; + + return result; + } + + /// + /// Returns the result of the ; if possible from the cache, + /// otherwise the query is materialized asynchronously and the result cached before being returned. + /// + /// The type of the data in the data source. + /// The query to be materialized. + /// The cache policy for the query. + /// The list of tags to use for cache expiration. + /// + /// The result of the query. + /// + public static async Task> FromCacheAsync(this IQueryable query, CachePolicy cachePolicy = null, IEnumerable tags = null) + where TEntity : class + { + string key = query.GetCacheKey(); + var cacheKey = new CacheKey(key, + tags ?? Enumerable.Empty()); + + // allow override of CacheManager + var manager = Locator.Current.Resolve(); + + var result = await manager.GetOrAddAsync( + cacheKey, + async k => await query.AsNoTracking().ToListAsync(), + cachePolicy ?? CachePolicy.Default + ) as IList; return result; } @@ -60,6 +91,24 @@ public static TEntity FromCacheFirstOrDefault(this IQueryable .FirstOrDefault(); } + /// + /// Returns the first element of the ; if possible from the cache, + /// otherwise the query is materialized asynchronously and the result cached before being returned. + /// + /// The type of the data in the data source. + /// The query to be materialized. + /// The cache policy for the query. + /// The list of tags to use for cache expiration. + /// default(T) if source is empty; otherwise, the first element in source. + public static async Task FromCacheFirstOrDefaultAsync(this IQueryable query, CachePolicy cachePolicy = null, IEnumerable tags = null) + where TEntity : class + { + return (await query + .Take(1) + .FromCacheAsync(cachePolicy, tags) + ).FirstOrDefault(); + } + /// /// Removes the cached query from cache. ///