External login fails asp.net mvc core 2.2 - entity-framework

Did scaffold some identity pages (Login, Logout, Register) for translation purposes and added Google as an external provider. It works, but when I hit Register on the Identity/Account/ExternalLogin page I get the following error:
InvalidOperationException: The instance of entity type 'IdentityUserLogin' cannot be tracked because another instance with the same key value for {'LoginProvider', 'ProviderKey'} is already being tracked.
Did scaffold ExternalLogin to check what might be wrong: here is the (standard) code:
public async Task<IActionResult> OnPostConfirmationAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
// Get the information about the user from the external login provider
var info = await _signInManager.GetExternalLoginInfoAsync();
if (info == null)
{
ErrorMessage = "Error loading external login information during confirmation.";
return RedirectToPage("./Login", new { ReturnUrl = returnUrl });
}
if (ModelState.IsValid)
{
var user = new IdentityUser { UserName = Input.Email, Email = Input.Email };
var result = await _userManager.CreateAsync(user);
if (result.Succeeded)
{
result = await _userManager.AddLoginAsync(user, info);
if (result.Succeeded)
...
The await _userManager.AddLoginAsync(user, info) gives the exception mentioned.
InvalidOperationException: The instance of entity type 'IdentityUserLogin<string>' cannot be tracked because another instance with the same key value for {'LoginProvider', 'ProviderKey'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.
Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap<TKey>.ThrowIdentityConflict(InternalEntityEntry entry)
Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap<TKey>.Add(TKey key, InternalEntityEntry entry, bool updateDuplicate)
Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap<TKey>.Add(TKey key, InternalEntityEntry entry)
Microsoft.EntityFrameworkCore.ChangeTracking.Internal.NullableKeyIdentityMap<TKey>.Add(InternalEntityEntry entry)
Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntry entry)
Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState oldState, EntityState newState, bool acceptChanges)
Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState entityState, bool acceptChanges, Nullable<EntityState> forceStateWhenUnknownKey)
Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.PaintAction(EntityEntryGraphNode node, bool force)
Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph<TState>(EntityEntryGraphNode node, TState state, Func<EntityEntryGraphNode, TState, bool> handleNode)
Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.AttachGraph(InternalEntityEntry rootEntry, EntityState entityState, bool forceStateWhenUnknownKey)
Microsoft.EntityFrameworkCore.DbContext.SetEntityState(InternalEntityEntry entry, EntityState entityState)
Microsoft.EntityFrameworkCore.DbContext.SetEntityState<TEntity>(TEntity entity, EntityState entityState)
Microsoft.EntityFrameworkCore.DbContext.Add<TEntity>(TEntity entity)
Microsoft.EntityFrameworkCore.Internal.InternalDbSet<TEntity>.Add(TEntity entity)
Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserOnlyStore<TUser, TContext, TKey, TUserClaim, TUserLogin, TUserToken>.AddLoginAsync(TUser user, UserLoginInfo login, CancellationToken cancellationToken)
Microsoft.AspNetCore.Identity.UserManager<TUser>.AddLoginAsync(TUser user, UserLoginInfo login)
Project.Areas.Identity.Pages.Account.ExternalLoginModel.OnPostConfirmationAsync(string returnUrl) in ExternalLogin.cshtml.cs
+
result = await _userManager.AddLoginAsync(user, info);
How to solve this? Thanks in advance for any help!

Related

EF 7 - Context returns null although values are there

I have a CQRS setup and I am trying to use domain events.
After I receive a command for a new order, I am adding the newly created Order object to the dbcontext.
public async Task<Guid> Handle(CreateOrderCommand message, CancellationToken cancellationToken)
{
...
var order = new Order(...);
...
order.SubmitOrder();
_orderRepository.Add(order);
await _orderRepository.UnitOfWork
.SaveEntitiesAsync(cancellationToken);
return order.Id;
}
The order.SubmitOrder() method is as follows
public void SubmitOrder()
{
AddDomainEvent(new OrderPlacedDomainEvent(Guid.NewGuid(), Id));
}
and orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken); is on overload to the UnitOfWork.SaveEntitiesAsync() as follows:
public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default)
{
// Dispatch Domain Events collection.
// Choices:
// A) Right BEFORE committing data (EF SaveChanges) into the DB will make a single transaction including
// side effects from the domain event handlers which are using the same DbContext with "InstancePerLifetimeScope" or "scoped" lifetime
// B) Right AFTER committing data (EF SaveChanges) into the DB will make multiple transactions.
// You will need to handle eventual consistency and compensatory actions in case of failures in any of the Handlers.
if (_mediator != null)
{
await _mediator.DispatchDomainEventsAsync(this);
}
// After executing this line all the changes (from the Command Handler and Domain Event Handlers)
// performed through the DbContext will be committed
await base.SaveChangesAsync(cancellationToken);
return true;
}
Note that the changes are not saved before the events are dispatched and the handler is called.
Now in the event handler when I am trying to get the order object from the context:
await _context.Orders.Include(o => o.OrderItems).SingleOrDefaultAsync(o => o.Id == id, cancellationToken: cancellationToken);
it returns null although the data is available in the context under _context.ChangeTracker.DebugView.LongView
Is there any way to get the order data here?
Well, solution found:
If Find/FindAsyncis used instead of SingleOrDefaultAsync it will return the values available in the change tracker.
public async Task<Order?> FindAsync(Guid id, CancellationToken cancellationToken = default)
=> await _context.Orders.FindAsync(new object[] { id }, cancellationToken: cancellationToken);

Does EF Core Have a Query Que?

I am new to EF Core and am trying to figure out why I keep having to increase my SQl timeout setting to make a specific query work. In the process I developed a theory. What if EF has a que of pending requests and when there are too many it just stops working instead of removing older requests from the que. That might explain why increasing timeout time is the only way to keep my browser from showing "err connection closed" when I load pages using a specific query.
I tried posting the query on the Microsoft Q and A website with my problem a couple days ago (https://learn.microsoft.com/en-us/answers/questions/601858/aspnet-core-app-only-works-for-few-minutes-after-r.html), but as usual Microsoft has not responded with an explanation that specifically explains why their product does not work and how to fix it. The best responses I've gotten are vague suggestions that don't really explain what to do.
For instance, one guy suggest not using ToArray() but doesn't say how the same functionality can be obtained without it. Another guy suggests researching connection resiliency but doesn't say how doing that might solve my problem.
So far I have managed to keep the app running longer without this problem occurring by using LazyCache to cache results of the problem query, but after a couple days the app is having the same problem again. Why is it that Microsoft is quick to tell people not to do something, but never tells people what to do instead?
UPDATE: Since Microsoft requires people to sign into their account to access the above link and nobody here want to sign in here is part of what is posted there. It is an error message from running my app locally
An unhandled exception occurred while processing the request.
Win32Exception: The wait operation timed out.
Unknown location
SqlException: Execution Timeout Expired. The timeout period elapsed prior to completion of the operation or the server is not responding.
Microsoft.Data.SqlClient.SqlCommand+<>c.<ExecuteDbDataReaderAsync>b__169_0(Task<SqlDataReader> result)
Stack Query Cookies Headers Routing
Win32Exception: The wait operation timed out.
Show raw exception details
SqlException: Execution Timeout Expired. The timeout period elapsed prior to completion of the operation or the server is not responding.
Microsoft.Data.SqlClient.SqlCommand+<>c.<ExecuteDbDataReaderAsync>b_169_0(Task<SqlDataReader> result)
System.Threading.Tasks.ContinuationResultTaskFromResultTask<TAntecedentResult, TResult>.InnerInvoke()
System.Threading.Tasks.Task+<>c.<.cctor>b277_0(object obj)
System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, object state)
System.Threading.Tasks.Task.ExecuteWithThreadLocal(ref Task currentTaskSlot, Thread threadPoolThread)
Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
Microsoft.EntityFrameworkCore.Storage.RelationalCommand.ExecuteReaderAsync(RelationalCommandParameterObject parameterObject, CancellationToken cancellationToken)
Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable<T>+AsyncEnumerator.InitializeReaderAsync(DbContext , bool result, CancellationToken cancellationToken)
Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync<TState, TResult>(Func<DbContext, TState, CancellationToken, Task<TResult>> operation, Func<DbContext, TState, CancellationToken, Task<ExecutionResult<TResult>>> verifySucceeded, TState state, CancellationToken cancellationToken)
Microsoft.EntityFrameworkCore.Storage.ExecutionStrategy.ExecuteImplementationAsync<TState, TResult>(Func<DbContext, TState, CancellationToken, Task<TResult>> operation, Func<DbContext, TState, CancellationToken, Task<ExecutionResult<TResult>>> verifySucceeded, TState state, CancellationToken cancellationToken)
Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable<T>+AsyncEnumerator.MoveNextAsync()
Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync<TSource>(IQueryable<TSource> source, CancellationToken cancellationToken)
Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync<TSource>(IQueryable<TSource> source, CancellationToken cancellationToken)
Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToArrayAsync<TSource>(IQueryable<TSource> source, CancellationToken cancellationToken)
PostAlmostAnything.SiteServices.PostsService.GetPosts() in PostsService.cs
}
public async Task<Posts[]> GetPosts()
{
IAppCache cache = new CachingService(CachingService.DefaultCacheProvider) { DefaultCachePolicy = new CacheDefaults { DefaultCacheDurationSeconds = GlobalStatic.SITEPOSTSCACHEDURATION() } };
string cacheKey = $"GetPosts";
var cacheobject = await (from post in _context.Posts
where post.Active == (bool?)true && post.Adminban == (bool?)false && post.Site == (int?)GlobalStatic.SITENUMBER()
select post into pts
select new Posts
{
Postid = pts.Postid,
Title = pts.Title,
PostAlmostAnything.SiteServices.PostsService.GetPaginatedResult(int currentPage, int pageSize) in PostsService.cs
Sc2url = linkgenerator.subcategory2link($"{pts.CategoryNavigation.Categoryurl}", $"{pts.SubcategoryNavigation.Subcategoryurl}", $"{pts.Subcategory2Navigation.Subcategory2url}")
}
}).ToArrayAsync();
}
public async Task<List<Posts>> GetPaginatedResult(int currentPage, int pageSize)
{
return (await GetPosts()).OrderByDescending((Posts d) => d.Postid).Skip((currentPage - 1) * pageSize).Take(pageSize)
.ToList();
}
public async Task<List<Posts>> GetPaginatedResultByCategoryCity(string catslug, string waslug, string regionslug, string cityslug, int currentPage, int pageSize)
{
return (from d in await GetPosts()
PostAlmostAnything.Pages.IndexModel.OnGetAsync() in Index.cshtml.cs
{
PostService = postService;
}
public async Task<PageResult> OnGetAsync()
{
Posts = await PostService.GetPaginatedResult(CurrentPage, PageSize);
return Page();
}
}
}
Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory+GenericTaskHandlerMethod.Convert<T>(object taskAsObject)
Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory+GenericTaskHandlerMethod.Execute(object receiver, object[] arguments)
Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeHandlerMethodAsync()
Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeNextPageFilterAsync()
Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Rethrow(PageHandlerExecutedContext context)
Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeInnerFilterAsync()
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g_Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(ref State next, ref Scope scope, ref object state, ref bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>gAwaited|19_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, object state, bool isCompleted)
Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>gLogged|17_1(ResourceInvoker invoker)
Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g_AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.MigrationsEndPointMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.DatabaseErrorPageMiddleware.Invoke(HttpContext httpContext)
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore.DatabaseErrorPageMiddleware.Invoke(HttpContext httpContext)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
UPDATE: Here is where I am now as far as trying to paginate the query. I cannot find a way to use an IQueryable and access it with an async task without getting errors for lacking GetAwaiter
public IQueryable<Posts> GetPosts(bool active)
{
var query = _context.Posts.Where(post => post.Active == active && post.Adminban == (bool?)false && post.Site == GlobalStatic.SITENUMBER());
return query;
}
public IQueryable<Posts> GetPaginatedResult(bool active, int currentPage, int pageSize)
{
//IAppCache cache = new CachingService(CachingService.DefaultCacheProvider) { DefaultCachePolicy = new CacheDefaults { DefaultCacheDurationSeconds = GlobalStatic.SITEPOSTSCACHEDURATION() } };
//string cacheKey = $"GetPosts";
var cacheobject = GetPosts(active)
.Select(pts => new Posts
{
Postid = pts.Postid,
Title = pts.Title,
Description = pts.Description,
Dateposted = pts.Dateposted,
Datemodified = pts.Datemodified,
Video = pts.Video,
Videostream = pts.Videostream,
Location = pts.Location,
Tags = pts.Tags,
Cap = pts.Cap,
Titletag = pts.Titletag,
Metatag = pts.Metatag,
Link = pts.Link,
Linkurl = pts.Linkurl,
Category = pts.Category,
Subcategory = pts.Subcategory,
Subcategory2 = pts.Subcategory2,
Worldarea = pts.Worldarea,
Region = pts.Region,
City = pts.City,
Sendemail = pts.Sendemail,
Userid = pts.Userid,
Active = pts.Active,
Adminban = pts.Adminban,
Posturl = linkgenerator.postlink($"{pts.Postid}", $"{pts.Title}"),
Comments = (from cm in pts.Comments
where cm.Active == (bool?)true && cm.Adminblock == (bool?)false
select cm into cmts
select new Comments
{
Commentid = cmts.Commentid,
Comment = cmts.Comment,
Date = cmts.Date,
Active = cmts.Active,
Adminblock = cmts.Adminblock,
Userid = cmts.Userid,
Post = cmts.Post,
Ratings = cmts.Ratings.Select((Ratings rts) => new Ratings
{
Commentnumber = rts.Commentnumber,
Rating = rts.Rating
}).ToArray(),
Images = cmts.Images.Select((Images imgs) => new Images
{
Image = imgs.Image,
Imagename = imgs.Imagename
}).ToArray(),
Likes = cmts.Likes.Select((Likes lks) => new Likes
{
Likeid = lks.Likeid,
Commentid = lks.Commentid
}).ToArray()
}).ToArray(),
Commentcount = pts.Comments.Where((Comments cm) => cm.Active == (bool?)true && cm.Adminblock == (bool?)false).Count(),
Ratings = (from r in pts.Ratings
where r.CommentnumberNavigation.Active == (bool?)true && r.CommentnumberNavigation.Adminblock == (bool?)false
select r into rtgs
select new Ratings
{
Ratingid = rtgs.Ratingid,
Rating = rtgs.Rating,
Daterated = rtgs.Daterated,
CommentnumberNavigation = new Comments
{
Commentid = rtgs.CommentnumberNavigation.Commentid
}
}).ToArray(),
Ratingcount = pts.Ratings.Where((Ratings rc) => rc.CommentnumberNavigation.Active == (bool?)true && rc.CommentnumberNavigation.Adminblock == (bool?)false).Count(),
Ratingavg = ratingavg((from rac in pts.Ratings
where rac.CommentnumberNavigation.Active == (bool?)true && rac.CommentnumberNavigation.Adminblock == (bool?)false
select rac into ra
select ra.Rating).Average()),
Images = pts.Images.Select((Images imgs) => new Images
{
Imageid = imgs.Imageid,
Imagename = imgs.Imagename,
Image = imgs.Image,
Imagetype = imgs.Imagetype,
Postid = imgs.Postid,
Comment = imgs.Comment,
Userid = imgs.Userid
}).ToArray(),
Primaryimage = computations.primaryimage((from pic in pts.Images
where pic.Imagetype == (int?)1
select pic into pi
select new Images
{
Imageid = pi.Imageid,
Image = pi.Image,
Imagename = pi.Imagename
}).DefaultIfEmpty().First()),
Likes = pts.Likes.Select((Likes lks) => new Likes
{
Likeid = lks.Likeid,
Post = lks.Post,
Commentid = lks.Commentid
}).ToArray(),
Likecount = pts.Likes.Where((Likes lc) => lc.Commentid == null).Count(),
WorldareaNavigation = new Worldarea
{
Worldarea1 = pts.WorldareaNavigation.Worldarea1,
Worldareaacronym = pts.WorldareaNavigation.Worldareaacronym,
Wurl = linkgenerator.worldarealink($"{pts.WorldareaNavigation.Worldareaacronym}")
},
RegionNavigation = new Regions
{
Regionname = pts.RegionNavigation.Regionname,
Regionacronym = pts.RegionNavigation.Regionacronym,
Rurl = linkgenerator.regionlink($"{pts.WorldareaNavigation.Worldareaacronym}", $"{pts.RegionNavigation.Regionacronym}")
},
CityNavigation = new Cities
{
City = pts.CityNavigation.City,
Cityacronym = pts.CityNavigation.Cityacronym,
Cityurl = linkgenerator.citylink($"{pts.WorldareaNavigation.Worldareaacronym}", $"{pts.RegionNavigation.Regionacronym}", $"{pts.CityNavigation.Cityacronym}")
},
CategoryNavigation = new Categories
{
Categoryname = pts.CategoryNavigation.Categoryname,
Categorylongname = pts.CategoryNavigation.Categorylongname,
Categorytitle = pts.CategoryNavigation.Categorytitle,
Categorydescription = pts.CategoryNavigation.Categorydescription,
Categorykeywords = pts.CategoryNavigation.Categorykeywords,
Categorymeta = pts.CategoryNavigation.Categorymeta,
Categoryurl = pts.CategoryNavigation.Categoryurl,
Curl = linkgenerator.categorylink($"{pts.CategoryNavigation.Categoryurl}")
},
SubcategoryNavigation = new Subcategories
{
Subcategoryname = pts.SubcategoryNavigation.Subcategoryname,
Subcategorylongname = pts.SubcategoryNavigation.Subcategorylongname,
Subcategorytitle = pts.SubcategoryNavigation.Subcategorytitle,
Subcategorydescription = pts.SubcategoryNavigation.Subcategorydescription,
Subcategorymeta = pts.SubcategoryNavigation.Subcategorymeta,
Subcategorykeywords = pts.SubcategoryNavigation.Subcategorykeywords,
Subcategoryurl = pts.SubcategoryNavigation.Subcategoryurl,
Scurl = linkgenerator.subcategorylink($"{pts.CategoryNavigation.Categoryurl}", $"{pts.SubcategoryNavigation.Subcategoryurl}")
},
Subcategory2Navigation = new Subcategory2
{
Subcategory2name = pts.Subcategory2Navigation.Subcategory2name,
Subcategory2longname = pts.Subcategory2Navigation.Subcategory2longname,
Subcategory2title = pts.Subcategory2Navigation.Subcategory2title,
Subcategory2description = pts.Subcategory2Navigation.Subcategory2description,
Subcategory2meta = pts.Subcategory2Navigation.Subcategory2meta,
Subcategory2keywords = pts.Subcategory2Navigation.Subcategory2keywords,
Subcategory2url = pts.Subcategory2Navigation.Subcategory2url,
Sc2url = linkgenerator.subcategory2link($"{pts.CategoryNavigation.Categoryurl}", $"{pts.SubcategoryNavigation.Subcategoryurl}", $"{pts.Subcategory2Navigation.Subcategory2url}")
}
}).OrderByDescending((Posts d) => d.Postid).Skip((currentPage - 1) * pageSize).Take(pageSize);
//Func<List<Posts>> actionThatWeWantToCache = () => cacheobject;
//var CachedAdapter = cache.GetOrAdd(cacheKey, actionThatWeWantToCache);
return cacheobject;
}```
Ok, here's your problem with code like this:
public async Task<Posts[]> GetPosts()
{
IAppCache cache = new CachingService(CachingService.DefaultCacheProvider) { DefaultCachePolicy = new CacheDefaults { DefaultCacheDurationSeconds = GlobalStatic.SITEPOSTSCACHEDURATION() } };
string cacheKey = $"GetPosts";
var cacheobject = await (from post in _context.Posts
where post.Active == (bool?)true && post.Adminban == (bool?)false && post.Site == (int?)GlobalStatic.SITENUMBER()
select post into pts
select new Posts
{
Postid = pts.Postid,
Title = pts.Title,
Sc2url = linkgenerator.subcategory2link($"{pts.CategoryNavigation.Categoryurl}", $"{pts.SubcategoryNavigation.Subcategoryurl}", $"{pts.Subcategory2Navigation.Subcategory2url}")
}
}).ToArrayAsync();
}
public async Task<List<Posts>> GetPaginatedResult(int currentPage, int pageSize)
{
return (await GetPosts()).OrderByDescending((Posts d) => d.Postid).Skip((currentPage - 1) * pageSize).Take(pageSize)
.ToList();
}
GetPosts is doing a ToArray which will look to materialize your ENTIRE table into entities BEFORE doing pagination.... This is also calling C# code like this linkgenerator(...) method which will trigger client-side evaluation which is a huge performance no-no since that will also materialize the related entities for all rows. You are then selecting the Post Entities into another class called Posts which is either a Post entity instance or a separate DTO/ViewModel in order to capture this link.
This is a common sin when I see developers pursuing DNRY (Do Not Repeat Yourself) to keep common logic in one place, and then employ patterns that treat everything consistently for consistency's sake.
GetPosts() is a dangerous method if it returns an array of posts as it would always return everything where the code you are calling wants to optimize for paginated results. This is very common in Generic implementations which force a one-size-fits-all on operations. Some tables will be small enough that a "GetAll" type operation is reasonable safe, however that common method against another table can halt a system for no good reason.
To satisfy DNRY, centralize the common query into a private method that can be called as needed. A public GetAll() method for Posts is probably never needed for this entity so I would remove it:
private IQueryable<Post> getPosts()
{
var query = _context.Posts
.Where(post => post.Active
&& !post.Adminban
&& post.Site == GlobalStatic.SITENUMBER());
return query;
}
This method can be called by every method that wants to query across Posts and their related details. This centralizes your common rules around active state, and tenancy. (ownership)
Now to your GetPaginatedResult method. Here you're going to want to ensure you are selecting your desired data. I would recommend declaring a ViewModel for the post data given you want to have some computed values.
[Serializable]
public class PostViewModel
{
public int PostId { get; set; }
public string Title { get; set; }
public string CategoryUrl { get; set; }
public string SubcategoryUrl { get; set; }
public string Subcategory2Url { get; set; }
public string Sc2Url
{
get { return linkgenerator.subcategory2link(CategoryUrl, SubcategoryUrl, Subcategory2url); }
}
}
Within Linq expressions we want to select all of the raw data from the entity and associated other entities. Where we want to compose/calculate values, we can expose getters in the view model that do this calculation as needed using the raw data.
And for the GetPaginatedResult method:
public async Task<List<Posts>> GetPaginatedResult(int currentPage, int pageSize)
{
var posts = await getPosts()
.Select(x => new PostViewModel
{
PostId = x.PostId,
Title = x.Title,
CategoryUrl = x.CategoryNavigation.Categoryurl,
SubcategoryUrl = x.SubcategoryNavigation.Subcategoryurl,
SubcategoryUrl = x.Subcategory2Navigation.Subcategory2url
}).Skip((currentPage -1) * pageSize)
.Take(pageSize)
.ToListAsync();
}
Note that the Select only selects the raw values from the Post and associated entities. It doesn't attempt to calculate the full link. The view model will take care of that when the property is accessed, such as when it is referenced to compose a view or serialized. You can perform simple computed fields in the Linq expression, but only basic stuff like appending strings together with "+" and some simple functions that EF can translate. Even String.Format (including $"{value}") is off the cards inside a Linq expression fed to EF. It cannot translate that to SQL so it would fall on client-side evaluation and you want to avoid that at all costs. By default with EF Core 2, client-side evaluation was on by default, with EF Core 3.1 onward it is off by default but some teams turn it on. I highly recommend leaving it off as it's a bigger landmine than lazy loading.
That should eliminate your performance issues. This generates a compact SQL statement taking into account the desired pagination and serializes just those results into a view model to return to the view. The core business rules around ownership can be cetralized, and by exposing IQueryable it is left to the consuming code to project or further modify the results (paginate, etc.) to build an efficient query. Don't expose any method that you don't intend to get called. (Like a GetAll())
Edit: It sounds like you are trying to replace:
public async Task<Posts[]> GetPosts()
with
public async IQueryable<Post> GetPosts()
This is *not the objective. The objective is to centralize the core logic into a protected method which GetPosts as well as GetPaginatedResult can both call rather than having GetPaginatedResult calling GetPosts which is loading all data. You also need to address that you cannot call linkgenerator from within the Select expression without triggering client-side evaluation so the suggestion there is to either define view models for the results, or if you insist on returning the entity models as a view model, adding a non-mapped property for the generated URL.
public async Task<Posts[]> GetPosts()
{
return await _getPosts()
.Select(pts => new Posts
{
Postid = pts.Postid,
Title = pts.Title,
CategoryUrl = pts.CategoryNavigation.CategoryUrl,
SubcategoryUrl = pts.SubcategoryNavigation.SubcategoryUrl,
Subcategory2Url = pts.SubcategoryNavigation.Subcategory2Url
}).ToArrayAsync();
}
public async Task<List<Posts>> GetPaginatedResult(int currentPage, int pageSize)
{
return await _getPosts()
.Select(pts => new Posts
{
Postid = pts.Postid,
Title = pts.Title,
CategoryUrl = pts.CategoryNavigation.CategoryUrl,
SubcategoryUrl = pts.SubcategoryNavigation.SubcategoryUrl,
Subcategory2Url = pts.SubcategoryNavigation.Subcategory2Url
}).Skip((currentPage-1) * pageSize)
.Take(pageSize)
.ToListAsync();
}
Where _getPosts() is a new protected IQueryable method that centralizes the core rules around your isActive and Site. I put an Underscore before the method name to differentiate it from your public GetPosts() method which is probably called in a number of different places.
You initially want to avoid changing the signature of your public methods because this would break anywhere and everywhere else that called this method, where it is better to correct this issue a bit at a time where it is most needed.
Looking at your updated example you don't want to change your public methods to IQueryable, leave those as your original return types and just ensure that these methods that call the new IQueryable method do their respective await with ToArrayAsync or ToListAsync respectively instead of calling the public GetPosts() method. The change you will still need to make is to move ALL properties where you are calling "linkgenerator" or any similar C# objects/functions into unmapped properties and instead use Select to retrieve the raw values these calls will need.

EF Core System.ObjectDisposedException

I am getting this error mentioned in the title. The same code works for the first API call, but not for the 2nd call. It fails on the 2nd SaveChangesAsync() call
await dbContext.SaveChangesAsync().ConfigureAwait(false);
I am using
services.AddTransient<ProjectContext>();
in my startup.cs. I tried AddScope but it doesn't work.
Error:
System.ObjectDisposedException: 'Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.
Object name: 'ProjectContext'.'
Code:
private readonly Action<InjectedControllerArgs, ProjectDto, NavigationElements> _postAfterSave = async (injectedControllerArgs, dto, nav) => {
var memberClient = new MemberClient { MemberId = member.MemberId,
InvitationEmail = member.Email,
ClientId = dto.ClientId,
LastModifierId = member.MemberId };
await dbContext.MemberClient!.AddAsync(memberClient).ConfigureAwait(false);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
var role = await dbContext.Role.Where(r => "client-admin" == r.RoleCode)
.FirstOrDefaultAsync()
.ConfigureAwait(false);
await dbContext.MemberClientRole!.AddAsync(new MemberClientRole
{ LastModifierId = member.MemberId,
MemberClientId = memberClient.MemberClientId,
RoleId = role.RoleId })
.ConfigureAwait(false);
await dbContext.SaveChangesAsync().ConfigureAwait(false); }
public async Task<ActionResult<ProjectDto>> PostInsertAsync(int customerId, int clientId, ProjectDto dto)
{
if (dto == null) throw new ArgumentNullException(nameof(dto));
dto.CustomerId = customerId;
dto.ClientId = clientId;
return await _pmBaseController.PostAsync(new NavigationElements(customerId, clientId, null), Topic, dto,
_returnOne, _postBeforeSave, _postAfterSave).ConfigureAwait(false);
}
public async Task<IActionResult> PutAsync(NavigationElements nav,
TopicName topic,
int id,
TDto dto,
Func<InjectedControllerArgs, NavigationElements, int, Task<T>> returnOne,
Func<TDto, int, TDto>? putBeforeSave = null,
Action<InjectedControllerArgs, TDto, NavigationElements>? putAfterSave = null
) ...
Your _postAfterSave delegate is asynchronous but does not return a Task which effectively means it is async void so your _pmBaseController is unable to await it which means execution will proceed before this Action is done. That the first SaveChanges works is timing / coincidence.
Try changing the type from Action<InjectedControllerArgs, ProjectDto, NavigationElements> to Func<InjectedControllerArgs, ProjectDto, NavigationElements, Task> and make sure to await it everywhere.

ExecuteReader requires the command to have a transaction

We get a strange exception when passing a transaction to EF:
Exception thrown: 'System.InvalidOperationException' in
System.Data.dll
Additional information: ExecuteReader requires the command to have a
transaction when the connection assigned to the command is in a
pending local transaction. The Transaction property of the command
has not been initialized.
this.DbContext = this.DbContextFactory.CreateContext<TContext>(connection);
this.DbContext.Database.UseTransaction(transaction);
This exception is caught by EF because it is shown only when 'Break when thrown' is on. Is it expected behavior or are we doing something potentially wrong?
Here is how call stack looks like:
System.Data.dll!System.Data.SqlClient.SqlCommand.ValidateCommand(string method, bool async)
System.Data.dll!System.Data.SqlClient.SqlCommand.RunExecuteReader(System.Data.CommandBehavior cmdBehavior, System.Data.SqlClient.RunBehavior runBehavior, bool returnStream, string method, System.Threading.Tasks.TaskCompletionSource<object> completion, int timeout, out System.Threading.Tasks.Task task, bool asyncWrite)
System.Data.dll!System.Data.SqlClient.SqlCommand.RunExecuteReader(System.Data.CommandBehavior cmdBehavior, System.Data.SqlClient.RunBehavior runBehavior, bool returnStream, string method)
System.Data.dll!System.Data.SqlClient.SqlCommand.ExecuteReader(System.Data.CommandBehavior behavior, string method)
EntityFramework.dll!System.Data.Entity.Infrastructure.Interception.InternalDispatcher<System.Data.Entity.Infrastructure.Interception.IDbCommandInterceptor>.Dispatch<System.Data.Common.DbCommand, System.Data.Entity.Infrastructure.Interception.DbCommandInterceptionContext<System.Data.Common.DbDataReader>, System.Data.Common.DbDataReader>(System.Data.Common.DbCommand target, System.Func<System.Data.Common.DbCommand, System.Data.Entity.Infrastructure.Interception.DbCommandInterceptionContext<System.Data.Common.DbDataReader>, System.Data.Common.DbDataReader> operation, System.Data.Entity.Infrastructure.Interception.DbCommandInterceptionContext<System.Data.Common.DbDataReader> interceptionContext, System.Action<System.Data.Entity.Infrastructure.Interception.IDbCommandInterceptor, System.Data.Common.DbCommand, System.Data.Entity.Infrastructure.Interception.DbCommandInterceptionContext<System.Data.Common.DbDataReader>> executing, System.Action<System.Data.Entity.Infrastructure.Interception.IDbCommandInterceptor, System.Data.Common.DbCommand, System.Data.Entity.Infrastructure.Interception.DbCommandInterceptionContext<System.Data.Common.DbDataReader>> executed)
EntityFramework.dll!System.Data.Entity.Infrastructure.Interception.DbCommandDispatcher.Reader(System.Data.Common.DbCommand command, System.Data.Entity.Infrastructure.Interception.DbCommandInterceptionContext interceptionContext)
EntityFramework.SqlServer.dll!System.Data.Entity.SqlServer.SqlVersionUtils.GetServerType(System.Data.Common.DbConnection connection)
EntityFramework.SqlServer.dll!System.Data.Entity.SqlServer.SqlProviderServices.QueryForManifestToken(System.Data.Common.DbConnection conn)
EntityFramework.SqlServer.dll!System.Data.Entity.SqlServer.SqlProviderServices.GetDbProviderManifestToken.AnonymousMethod__9(System.Data.Common.DbConnection conn)
EntityFramework.SqlServer.dll!System.Data.Entity.SqlServer.SqlProviderServices.UsingConnection.AnonymousMethod__32()
EntityFramework.SqlServer.dll!System.Data.Entity.SqlServer.DefaultSqlExecutionStrategy.Execute.AnonymousMethod__0()
EntityFramework.SqlServer.dll!System.Data.Entity.SqlServer.DefaultSqlExecutionStrategy.Execute<object>(System.Func<object> operation)
EntityFramework.SqlServer.dll!System.Data.Entity.SqlServer.SqlProviderServices.GetDbProviderManifestToken(System.Data.Common.DbConnection connection)
EntityFramework.dll!System.Data.Entity.Core.Common.DbProviderServices.GetProviderManifestToken(System.Data.Common.DbConnection connection)
EntityFramework.dll!System.Data.Entity.Utilities.DbProviderServicesExtensions.GetProviderManifestTokenChecked(System.Data.Entity.Core.Common.DbProviderServices providerServices, System.Data.Common.DbConnection connection)
mscorlib.dll!System.Collections.Concurrent.ConcurrentDictionary<System.Tuple<System.Type, string, string>, string>.GetOrAdd(System.Tuple<System.Type, string, string> key, System.Func<System.Tuple<System.Type, string, string>, string> valueFactory)
EntityFramework.dll!System.Data.Entity.Utilities.DbConnectionExtensions.GetProviderInfo(System.Data.Common.DbConnection connection, out System.Data.Entity.Core.Common.DbProviderManifest providerManifest)
EntityFramework.dll!System.Data.Entity.DbModelBuilder.Build(System.Data.Common.DbConnection providerConnection)
EntityFramework.dll!System.Data.Entity.Internal.LazyInternalContext.CreateModel(System.Data.Entity.Internal.LazyInternalContext internalContext)
EntityFramework.dll!System.Data.Entity.Internal.RetryLazy<System.Data.Entity.Internal.LazyInternalContext, System.Data.Entity.Infrastructure.DbCompiledModel>.GetValue(System.Data.Entity.Internal.LazyInternalContext input)
EntityFramework.dll!System.Data.Entity.Internal.LazyInternalContext.InitializeContext()
EntityFramework.dll!System.Data.Entity.Internal.LazyInternalContext.GetObjectContextWithoutDatabaseInitialization()
EntityFramework.dll!System.Data.Entity.Database.UseTransaction(System.Data.Common.DbTransaction transaction)
Transaction is created within Open method of the session:
public virtual void Open(IsolationLevel isolation, string user)
{
ValidateState(false);
this.m_user = user;
this.m_connection = this.m_database.CreateConnection();
this.m_connection.Open();
if (IsolationLevel.Unspecified != isolation)
{
this.m_transaction = this.m_connection.BeginTransaction(isolation);
}
}
Then this method is overreiden in the class which supports EF:
public override void Open(System.Data.IsolationLevel isolation, string user)
{
if (isolation == System.Data.IsolationLevel.Unspecified)
{
throw new InvalidOperationException("Isolation level 'Unspecified' is not supported");
}
base.Open(isolation, user);
this.DbContext = this.DbContextFactory.CreateContext<TContext>(this.Connection);
this.DbContext.Database.UseTransaction(this.Transaction);
}
During its initialization, DdContext will determine the version of SQL Server. This process will attempt to connect to the underlying server and query select cast (serverproperty ('EngineEdition') as int) using the provided connection, or in the absence of that, creating a new one from the configured connectionstring.
This will happen only once, after its first initialization.
Therefore, if your first use of DbContext in your application life cycle is from within a transaction, this will cause DbContext to try to use this connection and result in the observed error.
If you assure a first non-transactional call to DbContext (DbContext construction and usage for querying), you will avoid this behavior.
private static object _syncRoot = new object();
private static bool _Initialized = false;
private MyDbContext(string connectionString) : base(connectionString)
{
Database.SetInitializer<MyDbContext>(null);
}
private MyDbContext(DbTransaction dbTransaction) : base(dbTransaction.Connection, false)
{
Database.SetInitializer<MyDbContext>(null);
Database.UseTransaction(dbTransaction);
}
public static MyDbContext Factory()
{
return new MyDbContext(Tools.GetDefaultConnectionString());
}
public static MyDbContext Factory(DbTransaction dbTransaction)
{
if(_Initialized==false)
{
lock(_syncRoot)
{
if(_Initialized==false)
{
using (MyDbContext tempDbContext = new MyDbContext(dbTransaction.Connection.ConnectionString))
using (System.Data.Common.DbTransaction temptransaction = tempDbContext.BeginTransaction())
{
var mySampleData = tempDbContext.OneRowTable.AsNoTracking().ToList();
temptransaction.Commit();
_Initialized = true;
}
}
}
}
MyDbContext finalContext = new MyDbContext(dbTransaction);
return finalContext;
}
This is a solution for a scenario where you can not or do not want to use a TransactionScope in your software.
The issue appears on first access to to the underling ObjectContext (attempting to set the transaction accesses the underlying context). Using a TransactionScope is fine; this issue is when using a connection that already has a local transaction associated with it.
While a bit ugly, and approach such as the follow does work around this issue. Specific implementation details such as determining notCreateYet are left as an exercise.
if (notCreatedYet)
{
// Create a second session/connection to not touch the incoming connection.
// Using a TransactionScope works and proper enlistment is done.
// Attempts to open a manual transaction prior to creation will fail.
using (new TransactionScope(TransactionScopeOption.RequiresNew))
using (var initConn = new SqlConnection(connection.ConnectionString))
{
initConn.Open();
var tempContext = this.DbContextFactory.CreateContext<TContext>(connection);
// Touch the object context.
if (tempContext is IObjectContextAdapter contextAdapter)
{
_ = contextAdapter.ObjectContext;
}
}
}
// Then later on this is fine.
this.DbContext = this.DbContextFactory.CreateContext<TContext>(connection);
this.DbContext.Database.UseTransaction(transaction);

Check if an insert or update was successful in Entity Framework

In ADO.NET, ExecuteNonQuery() "For UPDATE, INSERT, and DELETE statements, the return value is the number of rows affected by the command" (http://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlcommand.executenonquery.aspx)
In EF v1, context.SaveChanges() method returns "The number of objects in an Added, Modified, or Deleted state when SaveChanges was called." (http://msdn.microsoft.com/en-us/library/bb739065.aspx)
Please tell, when multiple entities (or single entity) are added or updated to context and context.SaveChanges() method is called, how to check if actual INSERT or UPDATE was successful.
Can we assume if there was NO exception that INSERT(s) or UPDATE(s) was successful ?
Thank You
Yes, if there is no exception you may assume that the statements executed successfully.
In EntityFramework, SaveChangesAsync() returns an int.
So you can check if it is > 0 or not.
If something happens with SaveChangesAsync() it will return the number of effected rows and this means if value > 0 then true. So simply, you can have below scenerio:
INSERT
public Task<bool> CreateEntity(Entity entity){
if(entity == null)
return false;
await _dataContext.Entities.AddAsync(entity);
var created = await _dataContext.SaveChangesAsync();
return created > 0;
}
UPDATE
public async Task<bool> UpdateEntity(Entity entityToUpdate)
{
if(entityToUpdate == null)
return false;
_dataContext.Posts.Update(entityToUpdate);
var updated = await _dataContext.SaveChangesAsync();
return updated > 0;
}
DELETE
public async Task<bool> DeleteEntity(int entityId)
{
var entity = await _dataContext.Entities.FindAsync(entityId);
if (entity == null)
return false;
_dataContext.Entities.Remove(entity);
var deleted = await _dataContext.SaveChangesAsync();
return deleted > 0;
}
And in your methods, now you can simply check if that change is success or not:
For a simple MVC scenerio:
public Task<IActionResult> CreateEntity(EntityModel model)
{
if(model == null)
return StatusCode(404);
var entity = new Entity
{
attribute1 = model.attribute1,
attribute2 = model.attribute3
};
var isCreated = await _entityService.CreateEntity(entity);
if(isCreated)
{
//do something and return a view.
}
else
{
//you can return a status code, or an error view.
}
}
You can do the same practice for Update & Delete
Maybe this is not direct answer to the question, but may help.
By default all commands are encapsulated in one DbTransaction when SaveChanges method is called (Julia Lerman, Programming Entity Framework). So, or all commands will be successfully executed, or neither. That's one way to know if insert, or update or delete was successful.
define variable SaveStatus
var SaveStatus=context.SaveChanges()
then you can know if the creation has been done by getting the value of SaveStatus=1
in the case of "SaveStatus=0" it means no record has been created or affected