Getting “attempt was made to lazy-load navigation property on detached entity” after trying to pass alambda expression into the Where clause - ef-core-2.1

I'm using Entity Framework Core 2.1.2 with lazy loading enabled and am performing a query. If I try to pass a lambda expression I always get the following exception:
System.InvalidOperationException: Error generated for warning
'Microsoft.EntityFrameworkCore.Infrastructure.DetachedLazyLoadingWarning:
An attempt was made to lazy-load navigation property 'Children' on
detached entity of type 'ParentProxy'. Lazy-loading is not supported
for detached entities or entities that are loaded with
'AsNoTracking()'.'
Here some code :
public class User
{
public virtual ICollection<UserRole> UserRoles { get; set; } = new HashSet<UserRole>();
}
public class UserRole
{
public int UserId { get; set; }
public int RoleId { get; set; }
public virtual User User { get; set; }
}
My db context:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddDbContext<Context>(options =>
{
options.UseLazyLoadingProxies();
options.UseSqlServer(Configuration.GetConnectionString("connection"));
});
}
That's how it works:
_userRepository.Table.Where(user => user.UserRoles.Any(userRole => userRole.RoleId == 10)).ToList()
And that's how it doesn't:
_userRepository.Table.Where(user => condition(user)).ToList()
Where the condition is:
Func<User, bool> condition
Example:
Func<User, bool> result = (user) => user.UserRoles.Any(role => role.RoleId == 10)
I'm so sorry for bad formatting. But I hope that you understand my problem.

Related

MySQL / EF 6.0 Core One-to-One relationship returns null

I am using EF Core 6.0 with MySQL (Pomelo.EntityFrameworkCore.MySql v6.0)
I am setting up my database via code first. Here are my 2 models (simplified):
public class Store : BaseEntity
{
public string Name { get; set; }
public virtual User? Owner { get; set; }
public int? OwnerId { get; set; }
}
public class User : BaseEntity
{
public string? Name { get; set; }
public virtual Store? Store { get; set; }
}
For both, I have
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
as the primary key (from BaseEntity).
I also have Lazy Loading enabled here:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseLazyLoadingProxies();
}
and in the Program.cs:
builder.Services.AddDbContext<AppDbContext>(opt => opt
.UseLazyLoadingProxies()
.ConfigureWarnings(warning => warning.Ignore(CoreEventId.DetachedLazyLoadingWarning))
.EnableSensitiveDataLogging()
.UseMySql(
Globals.DB_CONNECTION_STRING, new MySqlServerVersion(Globals.MYSQL_SERVER_VERSION),
o => o.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)
));
Now, to the problem - I am trying to get a store from the database, using the following code:
Store? store = await dbContext.Stores
.Include(x => x.Owner)
.FirstOrDefaultAsync(x => x.Owner.Id == ownerId && x.Id == storeId);
I am getting the store details, but the Owner object and OwnerId is null. I can see the data in the database (e.g. I see OwnerId is set up for this specific store), but in the code, it is null.
I read on SO that Pomelo has some issues with setting up navigation properties, so I set it manually in OnModelCreating as follows:
modelBuilder.Entity<User>()
.HasOne(x => x.Store)
.WithOne(x => x.Owner)
;
But that didn't do the trick.
The same configuration works perfectly with MSSQL.
Any thoughts?
Thanks
Call HasForeignKey after WithOne and point to OwnerId.

Cascade delete Entity Framework Identity User with relations

I have using .net Core 3.1 scaffolded Identity and extended that with my own classes. But when I use the build in DeletePersonalData.OnPostAsync() it fails due to my relations from my other classes. I don't get how I make the delete to delete all the extended classes too.
Error message:
SqlException: The DELETE statement conflicted with the REFERENCE
constraint "FK_Workspaces_AspNetUsers_OwnerId". The conflict occurred
in database "myupload", table "dbo.Workspaces", column 'OwnerId'. The
statement has been terminated.
Microsoft.Data.SqlClient.SqlCommand+<>c.b__164_0(Task
result)
Extended Identity:
public class ApplicationDbContext : IdentityDbContext<IdentityUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<MyFile> MyFiles { get; set; }
public DbSet<Workspace> Workspaces { get; set; }
public DbSet<WorkspacePermission> WorkspacePermissions { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Customize the ASP.NET Identity model and override the defaults if needed.
// For example, you can rename the ASP.NET Identity table names and more.
// Add your customizations after calling base.OnModelCreating(builder);
builder.Entity<Workspace>()
.HasOne(p => p.Owner)
.WithMany()
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<MyFile>()
.HasOne(p => p.Workspace)
.WithMany(b => b.MyFile);
}
}
And the delete method:
public async Task<IActionResult> OnPostAsync()
{
var user = await _userManager.GetUserAsync(User);
if (user == null)
{
return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
}
RequirePassword = await _userManager.HasPasswordAsync(user);
if (RequirePassword)
{
if (!await _userManager.CheckPasswordAsync(user, Input.Password))
{
ModelState.AddModelError(string.Empty, "Incorrect password.");
return Page();
}
}
var result = await _userManager.DeleteAsync(user);
var userId = await _userManager.GetUserIdAsync(user);
if (!result.Succeeded)
{
throw new InvalidOperationException($"Unexpected error occurred deleting user with ID '{userId}'.");
}
await _signInManager.SignOutAsync();
_logger.LogInformation("User with ID '{UserId}' deleted themselves.", userId);
return Redirect("~/");
}
}
One of the Classes:
public class Workspace
{
// Base
public Guid WorkspaceID { get; set; }
public string Name { get; set; }
// Security
public virtual IdentityUser Owner { get; set; }
public string Password { get; set; }
// Files
public virtual ICollection<MyFile> MyFile { get; set; }
// Statistics
public Workspace()
{
}
}
Using Fluent API I managed to add the Cascade Delete to the IdentityUser with this Code in the OnModelCreating method.
builder.Entity<Workspace>()
.HasOne(p => p.Owner)
.WithMany()
.OnDelete(DeleteBehavior.Cascade);
builder.Entity<MyFile>()
.HasOne(p => p.Workspace)
.WithMany(b => b.MyFile);

Include fields from related tables

I'm using EF with DotNet Core 2.1 in my application. The application deals with data in multiple related and with FK interconnected tables.
I need audit logging data only changes to one table. However, my problem is, the table I need to audit log has quite some FK and for each of these I would like to log the FK itself and a field from the related table.
Let me try illustrate what I'm about - let's suppose this is my model:
public class Blog {
public int Id { get; set; }
public string Name { get; set; }
public string Url { get; set; }
[InverseProperty ("Blog")]
public ICollection<Post> Posts { get; set; }
public Blog() {
Posts = new Collection<Post> ();
}
}
...
[AuditInclude]
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
[Required]
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
As said, I'd like audit logging only changes to one entity - let's say it is a Post - here is an audit class:
public class Audit_Post : IAudit {
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public string Blog { get; set; } // <- I need populating this from Blog.Name
[StringLength (64)]
public string AuditUsername { get; set; }
public DateTime AuditDt { get; set; }
public string AuditAction { get; set; }
public Audit_Manufacturer () { }
}
And this is how I set up audit logging in my startup.cs -> ConfigureService():
...
Audit.Core.Configuration.Setup ()
.UseEntityFramework (ef => ef
.AuditTypeExplicitMapper (m => m
.Map<Post, Audit_Post> ((d, al) => {
al.Blog = d.Blog?.Name; // !! This doesn't work
})
.AuditEntityAction<IAudit> ((evt, entry, auditEntity) => {
Object val;
var gotVal = evt.CustomFields.TryGetValue ("AuditUsername", out val);
string username = null;
if (gotVal && val is string)
username = val as string;
else
username = "<anonymous>";
auditEntity.AuditDt = DateTime.UtcNow;
auditEntity.AuditUsername = username;
auditEntity.AuditAction = entry.Action;
})
)
);
question: Is it possible at all to get and audit log data from dependant table (one-to-many) relation?
Beside the mentioned issue, I'm also bumped in an off-topic one, which is - if I forget updating the DB with the migration for initialising the Audit_Posts table and I'm doing operations on Posts table, the data get stored to the later even if audit logs fail to get written (UnitOfWork save exception). Is there a flag to AuditDbContext that would make it run in the same transaction as the original query?
As #thepirat000 pointed out, it is enough to guarantee all related items being present in the DbContext memory. This means:
INSERT Just before doing context.Posts.Add(item) do a query to all related items such as context.Blogs.Find(item.BlogId).
UPDATE When retrieving the Post, do it with .Include(d => d.Blog) + other related items.
DELETE When retrieving the Post, do it with .Include(d => d.Blog) + other related items.
An additional important thing that was causing me troubles is the layout of my Audit table. The issue was I reused the same property name in Audit table with a different type - in the original table the property Blog was a relationship property, whiles in the audit table it was a string. This caused errors in conversion from one to the other model.
[AuditInclude]
public class Post
{
...
[Required]
public int BlogId { get; set; }
public Blog Blog { get; set; }
...
}
Just rename it to something else like:
public class Audit_Post
{
...
public int BlogId { get; set; }
public string BlogName { get; set; }
...
}
...
// and in startup.cs use ...
...
.Map<Post, Audit_Post> ((d, al) => {
al.BlogName = d.Blog?.Name;
})
...
Regarding the 2nd issue - running audit inside transactions. I decided not to use it for now. I'll get covered the described case with tests.
Maybe a suggestion for future development of the package - it would be nice to have mentioned cases covered easily - I mean, transitive properties.
Your code should work fine if you Include the Blog property when retrieving the Post entity, for example:
using (var context = new BlogsContext())
{
var post = context.Posts
.Include(p => p.Blog) // Important, otherwise post.Blog will be NULL
.First();
post.Content += " adding this";
context.SaveChanges();
}
If you can't include the Blog entity because any reason, you could do the query on the mapping, but you will need to use a lower level overload on the Map method, like this:
.Map<Post, PostAudit>((ev, entry, postAudit) =>
{
var entryEf = entry.GetEntry();
var post = entryEf.Entity as Post;
var dbContext = entryEf.Context as BlogsContext;
// Get the blog related
var blog = dbContext.Blogs.FirstOrDefault(b => b.Id == post.BlogId);
postAudit.Blog = blog?.Name;
})
Regarding the other question, currently there is no built-in mechanism to rollback your database changes when the audit saving fails, but maybe you try with the AuditDbContext overrides

EntityFramework Include and possibly join?

I have the following table structure as shown in the picture. (see: Table structure). Both tables ("Batches" and "Methods") reference to a "Project" table.
When I now create a new Project I would like to get all childs created as well.
Doing so I did the follwoing:
_dbContext.Projects.Where(x => x.Id == prjId)
.Include(x => x.Batches)
.Include(x => x.Batches.Select(y => y.Measurements))
.Include(x => x.Methods).AsNoTracking().FirstOrDefault();
Now the problem is the following:
New Batch and Method instances are created - thus they get a new ID(PK). The referenced Project_Id (FK) is set correct. But in my new Measurement instance only the Batch_Id(FK) is set correct and the Method_Id remains unchanged (has the old value) (see: result).
What I need is that the Measurements.Mehtod_Id is set from the Methods table. Is there any suitable solution for that?
My entities look like the following
public class Project
{
[Key]
public long Id { get; set; }
public string Name { get; set; }
public bool IsActive { get; set; }
public virtual List<Batch> Batches { get; set; }
public virtual List<Method> Methods { get; set; }
}
public class Batch : BaseObject
{
public Batch()
{
BatchFiles = new List<FileAttachment>();
Measurements = new List<Measurement>();
}
public long Id { get; protected set; }
public long Project_Id { get; set; }
public virtual Project Project { get; set; }
public virtual List<Measurement> Measurements { get; set; }
}
public class Method : BaseObject
{
public Method()
{
Parameters = new List<Parameter>();
}
public long Id { get; protected set; }
public long Project_Id { get; set; }
public virtual Project Project { get; set; }
public virtual List<Measurement> Measurements { get; set; }
}
public class Measurement
{
public int Id { get; protected set; }
[ForeignKey("Batch")]
public long? Batch_Id { get; set; }
[Required]
public virtual Batch Batch { get; set; }
[ForeignKey("Method")]
public long? Method_Id { get; set; }
public virtual Method Method { get; set; }
}
// creation code (just a copy with new IDs for all childs)
Project newProjectVersion = _dbContext.Projects.Where(x => x.Id == prjId)
.Include(x => x.Batches)
.Include(x => x.Batches.Select(y => y.Measurements))
.Include(x => x.Methods)
.AsNoTracking().FirstOrDefault();
_dbContext.Projects.Add(newProjectVersion);
_dbContext.SaveChanges();
Thanks for any help!
The first problem is that your Select statement doesn't connect Measurements to Methods because of the AsNoTracking() addition. Only Projects and Methods are connected because they are explicitly Included off of the Project entity. The Measurements have a Method_id but this is value is not accompanied by a Method in their Method property. You could check that in the debugger if you walk through the object graph (with lazy loading disabled though!). Because of this, when all entities will be Add-ed to the context, EF won't notice that measurements receive new methods.
You could get tempted to fix that by Include-ing Measurement.Method as well:
...
.Include(x => x.Batches.Select(y => y.Measurements.Select(m => m.Method)))
...
Now you'll see that Measurement.Method will be populated everywhere in the object graph.
However, there's a gotcha here. When using AsNoTracking, EF6 doesn't keep track of entities it materialized (duh). This means that for each Measurement it creates a new Method instance, even if an identical Method (by id) was materialized before for another Measurement. (And in this case it will always materialize duplicates, because you already include Project.Methods.)
That's why you can't do this in the quick way with AsNoTracking and Add using one context instance. You'll get an error that EF tries to attach duplicate entities.
You must build the object graph using one context, with tracking, so EF will not materialize duplicates. Then you must Add this object graph to a new context. Which will look like this:
Project project;
using(var db = new MyContext())
{
db.Configuration.ProxyCreationEnabled = false;
project = db.Projects.Where(x => x.Id == prjId)
.Include(x => x.Batches)
.Include(x => x.Batches.Select(y => y.Measurements))
.Include(x => x.Methods).FirstOrDefault();
}
using(var db = new MyContext())
{
db.Projects.Add(project);
db.SaveChages();
}
Three remarks:
Proxy creation is disabled, because you can't attach a proxy to another context without explicitly detaching it first.
No, I didn't forget to include Measurement.Method. All methods are loaded by including them in the Project and now (because of tracking, and assuming that measurement will only have methods of the project they belong to), EF connects them with the Measurements by relationship fixup.
EF-core is smarter here: when adding AsNoTracking it won't track materialized entities, but still, it won't create duplicates either. It seems to have some temporary tracking during the construction of an object graph.
thanks for your answer so far. This works quite fine right now. Unfortunately I noticed that the Measurements entity has another required relationship to a table named 'MeasurementTypes':
[Required]
public virtual MeasurementType MeasurementType { get; set; }
[ForeignKey("MeasurementType")]
public long MeasurementType_Id { get; set; }
In contrast to Batches and Methods these entries must not be copied and the entries already exist in the MeasrementTypes table.
What would be a good way to put the required reference to the Measurements?

EF Code First, create new object in collection with a proxy

How can I create+persist+have-a-proxy-to a new instance of a code-first poco using only navigation property collections? In the bellow code, I show how you might want to do this, if using member functions to create POCOs that then create POCOs. You don't have a DbContext, but if you create an object and persist it using DbSet.Add, the returned object isn't a proxy, so you can't in turn use its DbSet.Add to add a different sub-object.
In this code, if you call MailingList.AddIncomingMessage("my message"), you get an exception at the "OOPS" comment, because the created msg isn't a proxy and thus its Message.doodads property is null.
class Doodad {
public int ID { get; set; }
public string doodad { get; set; };
}
class Message {
public int ID { get; set; }
public virtual MailingList mailingList { get; set; }
public virtual ICollection<Doodad> doodads { get; set; }
public string text { get; set; }
public void GetDoodadCreateIfNeeded(string doodad) {
try {
// won't be found since we just created this Message
return this.doodads.First(d => d.doodad == doodad);
} catch (Exception e) {
Doodad newDoodad = new Doodad() { doodad=doodad };
// OOPS! this.doodads == null, because its not a proxy object
this.doodads.Add(newDoodad);
return newDoodad;
}
}
}
class MailingList {
public int ID { get; set; }
public virtual ICollection<Message> messages { get; set; }
public void AddIncomingMessage(string message) {
var msg = new Message() { text=message };
// we have no Context, because we're in a POCO's member function
this.messages.Add(msg);
var doodad = msg.GetDoodadCreateIfNeeded("bongo drums");
}
}
EDIT: sorry guys, I forgot to put the property accessors and ID in for this simplified case, but I am using them in the actual code.
It has nothing to do with proxies. It is the same as any other code - if you want to use object / collection you must first initialize it! Your fist command:
return this.doodads.First(d => d.doodad == doodad);
doesn't throw exception because it didn't find doodad but because the doodads is null.
What do you need to do? You need to initialize collections before you first use them. You can do it:
Directly in their definition
In entity's constructor
In property getter (lazy initialization) once they are first needed - that would require to change your fields to properties which is btw. correct way to write classes in .NET
In your custom methods you can check if they are null and initialize them
Complementary to the navigation property, you need to have a property that is the Id of the foreign key.
So your MailingList will need to have this property:
[Key] // this attribute is important
public int Id { get; set; }
and you'll have to change the Message classe to have these properties:
public virtual int mailingListId { get; set;
public virtual MailingList mailingList { get; set; }
The { get; set; } property is important, so that it is a property, not just a public attribute.