How To Save Entity Relations With Audit.Net? - entity-framework

I am using Audit.Net (Audit.EntityFramework) and I want to know how can I save an entity's relation?
Here's my configuration
Audit.Core.Configuration.Setup()
.UseEntityFramework(x => x
.AuditTypeMapper(typeName => typeof(AuditLog))
.AuditEntityAction<AuditLog>((ev, ent, auditEntity) =>
{
auditEntity.Table = ent.Table;
auditEntity.AuditDate = DateTime.UtcNow;
auditEntity.Action = ent.Action;
auditEntity._Changes = ent.Changes;
auditEntity._Entries = ev.GetEntityFrameworkEvent().Entries;
auditEntity.Success = ev.GetEntityFrameworkEvent().Success;
auditEntity._ColumnValues = ent.ColumnValues;
auditEntity._PrimaryKey = ent.PrimaryKey;
}));
Consider the following relationship
public class Blog
{
public int Id { set; get; }
public string Title { set; get; }
public string AuthorName { set; get; }
public IList<Post> Posts { set; get; }
}
public class Post
{
public int Id { set; get; }
public string Title { set; get; }
public string Content { set; get; }
public virtual Blog Blog { set; get; }
}
I want to know what is the Blog's data when I remove a Post object.

The Entity Framework Data Provider gives you options to create your audit tables. So you must create an Audit table base on your plan and save related and extra data that you need.

If the Blog is included on the Post instance being deleted, you should get that information on the audit event.
For example if you delete like this:
var post = dbContext.Posts
.Include(p => p.Blog)
.First(p => p.Id == 1);
dbContext.Posts.Remove(post);
dbContext.SaveChanges();
And you are including the entity objects on the Audit.EF config:
Audit.EntityFramework.Configuration.Setup()
.ForAnyContext(_ => _
.IncludeEntityObjects()
);
You should be able to get the blog information on the AuditEntityAction / CustomAction:
Audit.Core.Configuration.Setup()
.UseEntityFramework(x => x
.AuditTypeMapper(typeName => typeof(AuditLog))
.AuditEntityAction<AuditLog>((ev, ent, auditEntity) =>
{
if (ent.Entity is Post post)
{
var blog = post.Blog;
}
// OR, if you don't IncludeEntityObjects:
if (ent.GetEntry().Entity is Post post)
{
}
//...
}));

Related

How to get Entity from DB including navigation properties and child list total amount

I have next entity
public class Objective
{
public virtual UserInfo AssignedUser { get; set; }
public int? AssignedUserID { get; set; }
public string ObjectiveText { get; set; }
public virtual ICollection<ObjectiveTask> Tasks { get; set; }
public virtual UserInfo User { get; set; }
public int UserID { get; set; }
}
One objective could has one Assigned User and one User but many Tasks.
After getting Entity from DB I map it to DTO class which looks like this
public class ObjectiveListViewModel
{
public string AssignedString { get; set; }
public string ObjectiveText { get; set; }
public int TasksCount { get; set; }
public string UserContactName { get; set; }
}
Mapping settings doesn't meter
When I do this with query like this
(from objective in context.Set<Objective>() select objective)
.Include(o => o.User)
.Include(o => o.AssignedUser)
.ToListAsync();
Everything works cool - User and Assigned User properties are loaded and no need do extra query to DB to get their data.
But I need return objectives with tasks amount.
To do this I've created a generic class
public class EntitySubCount<TEntity>
{
public TEntity Entity { get; set; }
public int GroupCount { get; set; }
}
And use it in this way
(from objective in context.Set<Objective>() select objective)
.Include(o => o.User)
.Include(o => o.AssignedUser)
.Select(o=> new EntitySubCount<Objective> {
Entity = o,
GroupCount = o.Tasks.Count })
.ToListAsync();
But User and Assigned User properties are not loaded and it require additional query to DB to get their data.
I understand that it because lazy loading.
The question is - how I can get from DB my Entity with loaded nav. properties and with count of Tasks at once?
Thank you for any help
You are close. No need for the includes if you are projecting. In this case I project to an anonymous type, but you could create a ViewModel class to project to if desired.
var objectiveList = context.Objectives
.Select(o => new
{
Entity = o,
// or you could just pick the properties:
ObjectiveText = o.ObjectiveText,
User = o.User,
AssignedUser = o.AssignedUser,
GroupCount = o.Tasks.Count
}).ToList();
EDIT: I see you already have a ViewModel(DTO). You might be looking for something like this:
var objectiveList = context.Objectives
.Select(o => new ObjectiveListViewModel
{
AssignedString = o.AssignedUser.Name,
ObjectiveText = o.ObjectiveText,
TasksCount = o.Tasks.Count
UserContactName = o.User.Name
}).ToList();

Entity Framework Core - issue with loading related entities that also contain related entities

I am using Entity Framework Core following Chris Sakell's blog here.
He uses generics to manage his repositories and also a base repository that he uses for all the other repositories.
Part of the base repository has the the following code for the retrieval of a single entity that also downloads related entities using the includeProperties option. Here is the generic code for a retrieving a single item.
public T GetSingle(Expression<Func<T, bool>> predicate, params Expression<Func<T, object>>[] includeProperties)
{
IQueryable<T> query = _context.Set<T>();
foreach (var includeProperty in includeProperties)
{
query = query.Include(includeProperty);
}
return query.Where(predicate).FirstOrDefault();
}
I am using it on a client table that has many jobs attached to it.
This is how I structured my code.
public ClientDetailsViewModel GetClientDetails(int id)
{
Client _client = _clientRepository
.GetSingle(c => c.Id == id, c => c.Creator, c => c.Jobs, c => c.State);
if(_client != null)
{
ClientDetailsViewModel _clientDetailsVM = mapClientDetailsToVM(_client);
return _clientDetailsVM;
}
else
{
return null;
}
}
The line:
.GetSingle(c => c.Id == id, c => c.Creator, c => c.Jobs, c => c.State);
successfully retrieves values for creator state and job.
However, nothing is retrieved for those related entities associated with the "jobs".
In particuar, JobVisits is a collection of visits to jobs.
For completeness I am adding the "job" and "jobvisit" entities below
public class Job : IEntityBase
{
public int Id { get; set; }
public int? ClientId { get; set; }
public Client Client { get; set; }
public int? JobVisitId { get; set; }
public ICollection<JobVisit> JobVisits { get; set; }
public int? JobTypeId { get; set; }
public JobType JobType { get; set; }
public int? WarrantyStatusId { get; set; }
public WarrantyStatus WarrantyStatus { get; set; }
public int? StatusId { get; set; }
public Status Status { get; set; }
public int? BrandId { get; set; }
public Brand Brand { get; set; }
public int CreatorId { get; set; }
public User Creator { get; set; }
....
}
public class JobVisit : IEntityBase
{
...
public int? JobId { get; set; }
public Job Job { get; set; }
public int? JobVisitTypeId { get; set; }
public JobVisitType VisitType { get; set; }
}
My question is, how do I modify the repository code above and my GetSingle use so that I can also load the related enitities JobVisit collection and the other related single entities Brand and JobType?
It is intended that navigation properties are not necessary retrieved for associated with the "jobs". That is why some properties are null. By default the .Include(property); goes only 1-level deep and that is a good thing. It prevents your query from fetching all the data of your database.
If you want to include multiple levels, you should use .ThenInclude(property) after .Include(property). From the documentation:
using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Include(blog => blog.Posts)
.ThenInclude(post => post.Author)
.ToList();
}
My advice is that your method public T GetSingle(...) is nice and I would not change it in order to include deeper levels. Instead of that, you can simply use explicit loading. From the documentation:
using (var context = new BloggingContext())
{
var blog = context.Blogs
.Single(b => b.BlogId == 1);
context.Entry(blog)
.Collection(b => b.Posts)
.Load();
context.Entry(blog)
.Reference(b => b.Owner)
.Load();
}

The INSERT statement conflicted with the FOREIGN KEY constraint "FK_PostTag_Tag_TagId"

I am using Entity Framework 7 RC1 and I have the entities:
public class Post {
public Int32 Id { get; set; }
public String Title { get; set; }
public virtual IList<PostTag> PostsTags { get; set; }
}
public class Tag {
public Int32 Id { get; set; }
public String Name { get; set; }
public virtual IList<PostTag> PostsTags { get; set; }
}
public class PostTag {
public Int32 PostId { get; set; }
public Int32 TagId { get; set; }
public virtual Post Post { get; set; }
public virtual Tag Tag { get; set; }
}
The model configuration for these entities is the following:
protected override void OnModelCreating(ModelBuilder builder) {
base.OnModelCreating(builder);
builder.Entity<Post>(b => {
b.ToTable("Posts");
b.HasKey(x => x.Id);
b.Property(x => x.Id).UseSqlServerIdentityColumn();
b.Property(x => x.Title).IsRequired().HasMaxLength(100);
});
builder.Entity<Tag>(b => {
b.ToTable("Tags");
b.HasKey(x => x.Id);
b.Property(x => x.Id).UseSqlServerIdentityColumn();
b.Property(x => x.Name).IsRequired().HasMaxLength(100);
});
builder.Entity<PostTag>(b => {
b.ToTable("PostsTags");
b.HasKey(x => new { x.PostId, x.TagId });
b.HasOne(x => x.Post).WithMany(x => x.PostsTags).HasForeignKey(x => x.PostId);
b.HasOne(x => x.Tag).WithMany(x => x.PostsTags).HasForeignKey(x => x.TagId);
});
}
I created the migration and the database. Then I tried to create a post:
Context context = new Context();
Post post = new Post {
PostsTags = new List<PostTag> {
new PostTag {
Tag = new Tag { Name = "Tag name" }
}
},
Title = "Post title"
};
context.Posts.Add(post);
await _context.SaveChangesAsync();
And on save I get the following error:
An error occurred while updating the entries.
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_PostTag_Tag_TagId".
The conflict occurred in database "TestDb", table "dbo.Tags", column 'Id'.
The statement has been terminated.
Does anyone knows the reason for this error?
I would say that you don't need to explicitly declare your foreign keys in EF CodeFirst the framework will handle it for you. So remove these properties from the PostTag class
public Int32 PostId { get; set; }
public Int32 TagId { get; set; }
And then remove these two lines from your configuration then try the save again. You will probably need to update your DB Model before saving.
b.HasKey(x => new { x.PostId, x.TagId });
b.HasOne(x => x.Post).WithMany(x => x.PostsTags).HasForeignKey(x => x.PostId);
b.HasOne(x => x.Tag).WithMany(x => x.PostsTags).HasForeignKey(x => x.TagId);
I had the same problems. Here's the solution I came up with. This SO question helped me a lot.
First of all, add a public DbSet<Tag> Tags {get; set;} to yout Context class if it's missing.
Then modify the post creation as follows
Context context = new Context();
var tmpTag = new Tag { Name = "Tag name" } //add the tag to the context
context.Tags.Add(tmpTag);
Post post = new Post {
PostsTags = new List<PostTag>(), // initialize the PostTag list
Title = "Post title"
};
context.Posts.Add(post);
var postTag = new PostTag() {Post = post, Tag = tag}; // explicitly initialize the PostTag AFTER addig both Post and Tag to context
post.PostTags.Add(postTag); // add PostTag to Post
await _context.SaveChangesAsync();
Explictly adding both post and tag to context.Posts and context.Tags before attempting to create the PostTag object allows EF to correctly manage the IDs while writing to the underlying DB.
For the sake of completeness, after solving this part of the many-to-many relationship management, I'm currently struggling with CascadeDelete Entity Framework Core (EF7), but that's a different story.

I have a little complex model, about EF cascade delete

Code first,and model look like this Figure:
public class Post
{
public int Id { get; set; }
public string title { get; set; }
public int fid { get; set; }
public int uid { get; set; }
public virtual lnk lnk { get; set; }
public virtual user user { get; set; }
public virtual ICollection<img> imgs { get; set; }
}
public class lnk
{
public int Id { get; set; }
public string lnkman { get; set; }
}
public class model1:Post
{
public string content { get; set; }
}
public class user
{
public int uid { get;set; }
public string Name {get;set;}
public virtual ICollection<Post> posts { get; set; }
}
public class img
{
public int id { get; set; }
public string imgUrl { get; set; }
public int pid { get; set; }
public virtual Post post { get; set; }
}
public class PostContext : DbContext
{
public DbSet<Post> posts { get; set; }
public DbSet<user> users { get; set; }
public DbSet<img> imgs { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>().HasKey(i=>i.Id);
modelBuilder.Entity<lnk>().HasKey(i => i.Id);
modelBuilder.Entity<Post>().HasRequired(i => i.lnk).WithRequiredPrincipal();
modelBuilder.Entity<Post>().ToTable("Post");
modelBuilder.Entity<lnk>().ToTable("Post");
modelBuilder.Entity<model1>().ToTable("model1");
modelBuilder.Entity<user>().HasKey(i => i.uid).ToTable("user");
modelBuilder.Entity<user>().HasMany(i => i.posts)
.WithRequired(i => i.user)
.HasForeignKey(i =>i.uid).WillCascadeOnDelete(false);
modelBuilder.Entity<img>().HasKey(i => i.id).ToTable("img");
modelBuilder.Entity<Post>().HasMany(i => i.imgs)
.WithRequired(i => i.post)
.HasForeignKey(i => i.pid)
.WillCascadeOnDelete(false);
}
}
the add and list method work well. but delele like this :
using (var db = new PostContext())
{
//model1 ToDel = new model1{ Id=id };
//b.Entry(ToDel).State = System.Data.EntityState.Deleted;
var ToDel = db.users.Single(i => i.uid == id);
db.users.Remove(ToDel);
db.SaveChanges();
return id + "DE: well done!" ;
}
System.Data.SqlClient.SqlException: DELETE statement with REFERENCE constraint "FK_dbo.Post_dbo.user_uid" conflict。The conflict occurred in database "jefunfl",table "dbo.Post", column 'uid'
I want delete users with user's posts, and posts with post's imgs.
How can I do this? Can some one give any tips?
btw,my Chinese English may be funny. forgive me.
this is my first post,but I am not new guy. thanks
bypass all one day, i found it. and thank you for answer to me.
1:
i delete WillCascadeOnDelete() method,so let ef do it by Conventions.
2:
i found that ,when i delete the posts,i can't directly delete with this statement like ToDel = db.posts.Single(i => i.Id == id)
but this:
ToDel = db.posts.Include(i=>i.lnk).Single(i => i.Id == id)
post and lnk has a relatialship that is called table splitting. both mapping to the same post table. when delete posts, EF can auto delete post.imgs by conventions. what will happen when delete user ? and so i got it. my code blow:
var ToDel = db.users.Include(i => i.posts)
.Single(i => i.uid == id);
var PostToDel = db.posts.Include(i => i.lnk)
.Single(i => i.Id == ToDel.uid);
db.posts.Remove(PostToDel);
db.users.Remove(ToDel);
db.SaveChanges();
this will work well. even not perfectly. but it can do what i want without a exception.
at last,the biggest problem is post class and lnk class has a "table splitting".
when delete post we should explicitly include.
i dont know if you can understand me, my english is very ugly. i am foolish with courage.
thank u. i am happy

Entity Framework Loading

I am using Entity Framework 4.1 (Code First) as O/R mapping framework:
I have three Entities: Post, PostForUser and User, whereas a PostForUser is associated with exactly one Post and one User.
Now I want to fetch all Posts for a given user and additionally load the user which created the post:
var posts = _dbContext.PostsForUser
.Include("Post.ByUser")
.Where(x => x.WasRead == false && x.User.Id == CurrentUser.Id)
.Select(x => x.Post)
.ToArray();
Using the query above, the "ByUser" property is never loaded. I am not using lazy loading at all, but I still can't get the loading working properly.
There aren't any custom mapping configs or special conventions registered. Just the following entity definitions:
public class Post
{
public int Id { get; set; }
public string Content { get; set; }
public DateTime Date { get; set; }
public User ByUser { get; set; }
}
public class PostForUser
{
public int Id { get; set; }
public User User { get; set; }
public Post Post { get; set; }
public bool WasRead { get; set; }
}
Your include .Include("Post.ByUser") applies to PostForUser but your are selecting Post so the the include should go to the and of the select:
var posts = _dbContext.PostsForUser
.Where(x => x.WasRead == false && x.User.Id == CurrentUser.Id)
.Select(x => x.Post).Include("ByUser")
.ToArray();