I am working with .NET MVC that have a model implement recursion as below:
public class Comment
{
[Key]
[Required]
public int CommentId { get; set; }
[Required]
public string Content { get; set; }
public bool Anonymous { get; set; }
[Required]
public DateTime Created_date { get; set; }
[Required]
public DateTime Last_modified { get; set; }
public virtual Comment Reply { get; set; }
public virtual ICollection<Comment> Replys { get; set;}
public virtual Idea Idea { get; set; }
}
In explanation, each Idea contains different Comments inside it, and each comment also has several smaller Comments that reply to the previous one. However, I have no idea how to do recursion to get replies for a comment and smaller reply for each reply until the last one in controller and also how to display them in view. Feel free to ask me any question since my explanation is not clear sometimes.
public ActionResult Index(int? i)
{
List<Idea> Ideas = db.Ideas.ToList();
foreach(Idea idea in Ideas)
{
idea.Comments = db.Comments.Include(x => x.Reply).ToList();
idea.Comments=idea.Comments.Where(x => x.Idea == idea).ToList();
foreach (Comment comment in idea.Comments)
{
comment.Replys = GetComments(comment.CommentId); //This function use to get list of reply for comment
foreach (Comment reply in comment.Replys)
{
reply.Replys=GetComments(reply.CommentId);
foreach (Comment reply2 in reply.Replys)
{
reply2.Replys=GetComments(reply2.CommentId);
foreach(Comment reply3 in reply2.Replys)
{
reply3.Replys = GetComments(reply3.CommentId);
//When would this stop ?
}
}
}
}
}
return View(Ideas.ToPagedList(i ?? 1, 5));
}
Use this loop;
foreach(Idea idea in Ideas)
{
idea.Comments = db.Comments.Include(x => x.Reply).ToList();
idea.Comments= idea.Comments.Where(x => x.Idea == idea).ToList();
foreach (Comment comment in idea.Comments)
{
RecursionGetComments(comment);
}
}
Add this function;
public void RecursionGetComments(Comment comment){
comment.Replys = GetComments(comment.CommentId);
foreach(var reply in comment.Replys){
RecursionGetComments(reply);
}
}
If the RecursionGetComments above didn't fill properly, you might need to use the ref keyword which passes the current instance of the class (pass by reference);
public void RecursionGetComments(ref Comment comment){
comment.Replys = GetComments(comment.CommentId);
foreach(var reply in comment.Replys){
RecursionGetComments(reply);
}
}
I also noticed that in your model, you are using the keyword virtual.
Virtual signals entity framework that the navigation property will be loaded automatically / lazy loading, since you are loading your comments manually, you could remove that.
Related
I have encountered really weird situation. I am developing a net 5 api, and (among ohther entities) have three tables, Doctor, Specialization, and DoctorSpecialization. My entities:
public class Doctor
{
public int Id { get; set; }
public string Name { get; set; }
public string Resume { get; set; }
public ICollection<DoctorSpecialization1> DoctorSpecializations { get; set; }
}
public class DoctorSpecialization
{
public int Id { get; set; }
public int Doctor1Id { get; set; }
[ForeignKey("Doctor1Id")]
public Doctor1 Doctor { get; set; }
public int Specialization1Id { get; set; }
[ForeignKey("Specialization1Id")]
public Specialization1 Specialization { get; set; }
}
public class Specialization
{
public int Id { get; set; }
public string SpecializationName { get; set; }
}
I want to fetch all the specializations associated with the certain doctor, therefore I created a service:
public class DoctorService : IDoctorService
{
public async Task<List<Specialization>> GetAllSpecializationsForDoctor(int id)
{
var doctor = await _context.Doctors.Where(x => x.Id == id).FirstOrDefaultAsync();
var doctorSpecializations = await _context.DoctorSpecializations.
Where(x => x.DoctorId == doctor.Id)
.ToListAsync();
IEnumerable<int> ids = doctorSpecializations.Select(x => x.SpecializationId);
var specializations = await _context.Specializations.Where(x =>
ids.Contains(x.Id)).ToListAsync();
return specializations;
}
}
In the end, I am adding a method in my controller which is supposed to fetch specializations based on doctor's id:
[HttpGet("specializations/{id}")]
public async Task<ActionResult<List<Specialization1>>> GetSpecializationsForDoctor(int id)
{
var doctor = await _doctorService.FindDoctorById(id);
if (doctor == null) return NotFound();
var specialization = _doctorService.GetAllSpecializationsForDoctor(id);
return Ok(specialization);
}
When I run this in postman, I get the error stated in the title of this question. However, I encountered an article that explains I should install newtonsoft.json and make some changes in my Startup in order to overcome this issue. So therefore I installed Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.10" and made changes in my Starup as follows:
services.AddControllers().AddNewtonsoftJson(options =>
options.SerializerSettings.ReferenceLoopHandling =
Newtonsoft.Json.ReferenceLoopHandling.Ignore);
This time I get the expected result, but my postman shows tons of data before giving me the wanted result, and I wonder is it ok to return this much data to the client. Can somebody explain me what is going on, tnx in advance!
Apparently the solution was so simple, I missed the await in:
var specialization = await
_doctorService.GetAllSpecializationsForDoctor(id);
Once again I managed to find solution thanks to stackoverflow, because apparently someone else had this problem in: JsonException: A possible object cycle was detected which is not supported. This can either be due to a cycle or if the object depth is larger than
So please everyone, don't be so superficial as I was, make sure to use await and save your time. Thanks again to the community:).
Here are 2 (minimized classes):
[Index(nameof(Name), IsUnique = true)]
public partial class ParticipantList
{
public ParticipantList()
{
Emails = new HashSet<Email>();
}
[Key]
[Column("id")]
public long Id { get; set; }
//...
[InverseProperty(nameof(Email.ParticipantList))]
public virtual ICollection<Email> Emails { get ; set; }
}
public partial class Email
{
[Key]
[Column("id", TypeName = "integer")]
public long Id { get; set; }
[Column("participant_list_id", TypeName = "integer")]
public long? ParticipantListId { get; set; }
/...
[ForeignKey(nameof(ParticipantListId))]
[InverseProperty("Emails")]
public virtual ParticipantList ParticipantList { get; set; }
}
And the DbContext OnConfiguring method contains:
optionsBuilder
.UseLazyLoadingProxies()
.UseSqlite(...);
And the DBContext OnModelCreating method contains:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Email>(entity =>
{
entity.HasOne(d => d.ParticipantList)
.WithMany(p => p.Emails)
.HasForeignKey(d => d.ParticipantListId)
.OnDelete(DeleteBehavior.ClientSetNull);
});
This is what works:
var email = _db.Emails.Find(1);
// email.ParticipantList (this lazy loads just fine).
What doesn't work:
var plist = _db.ParticipantLists.Find(1);
// plist.Emails.Count() == 0 (this doesn't lazy load at all)
// plist.Emails.Count == 0 (again no lazy loading)
// plist.Emails.ToList() (empty list, no lazy loading)
I should insert a rant here about the stupidity of having both Count() and Count with different meanings here. I mean, seriously, who is going to catch that during a code review????
This is most likely due to:
public ParticipantList()
{
Emails = new HashSet<Email>();
}
Which was created by the ef-scaffold app. I mean it makes sense, the HashSet is empty, but it seems like the model should override the access to the HashSet and do the lazy loading.
The problem is that if you remove this:
public ParticipantList()
{
// Emails = new HashSet<Email>();
}
And then call this code:
// plist.Emails.Count() == 0 (Now you get a null reference exception
// because guess what: Emails is not initialized!)
// plist.Emails.ToList() (Also gives a null reference exception)
// plist.Emails.Count == 0 (Again, a null reference exception)
The documentation shows code like this (note that Posts is NOT initialized in the example at https://learn.microsoft.com/en-us/ef/core/querying/related-data/lazy ):
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public virtual Blog Blog { get; set; }
}
Which will purportedly lazy load the Posts from the Blog. But when I follow that pattern and remove the ctor that initializes Emails/Posts I get a null reference exception.
To add to the mystery/madness is this:
var email = _db.Emails.Find(1);
var plist = _db.ParticipantLists.Find(1);
// plist.Emails.Count() == 1 (It knows about the relation and added it to
// the Emails HashSet on the ParticipantList!)
So, how do I get Emails to LazyLoad. I know about the 1 + N problem, and I'm not worried about that aspect, but I'd like the models to load when they are requested.
This is a C# Question, using .NET framework built on Asp.NET Boilerplate.
Again, to re-emphasis the question being asked is "HOW...", so if an answer that was provided was a url link or a descriptive explanation on how something was supposed to be done, i would very much appreciate it. (Dont answer questions on how to tie shoelace by showing a picture of a tied shoe, nor do you answer "how to fish" by showing a recording of someone fishing...)
Since the question is pretty basic (i don't need to rephrase/repeat the header again), i'll give an example.
If i have a Forum service, and i create a class to load a Thread. Inside that thread class should be some sort of collection, array, list, or even a dbset of Post that is pulled on construct.
[Table("Thread", Schema = "dbo")]
public class ThreadModel
{
[Key]
public long Id { get; set; }
public string Title { get; set; }
//Idea 1
//Value should automatically be pulled and cached the moment class connects to database
public Post[] Posts { get; set; }
//Idea 2
//Post has a constructor to return all post that matches a thread id. While new tag keeps the return value constantly refreshed.
public Post[] Posts { get { return new Post(this.Id) } }
//Idea 3
//Not sure how collection is supposed to work. Does it automatically just pull or will i need to make a method to request?
public virtual ICollection<Post> Posts { get; set; }
//Example constructor
//When connected to database key-value pairs that match database labels will automatically get stored in class
protected ThreadModel()
{
//Idea 1-A
//Should be a value of null or empty if database yields no results
Posts = new Post();
}
public ThreadModel(int threadid) : this()
{
//Idea 1-A
Id = threadid;
//new Post => returns all posts in db
//Posts default value is all post in db
Posts = Posts.Select(post => post.threadid == this.id)
//Idea 3-A
Posts = Posts.Get(post => post.threadid == this.id)
//Idea 4
Posts = new Posts().GetThread(threadid);
}
}
Side questions
If all entities are created by inheriting Entity then at what point am i exposed to EntityFramework and DbContext?
I love this example here, submitted by a user as they attempt to connect ABP to their database. But their example doesn't show parent/child resources. I'm unable to find the guide they used to create that, and how it relates back to using ABP to fetch EntityFramework's DbContext example
How does this work? I'm unable to find instructions or explanation for this? (What am i to enter into google to get answers on these mechanics?)
[Table("AbpItems")]
public class Item : Entity
{
[ForeignKey("PostId")]
public Post Post { get; set; }
public int PostId { get; set; }
}
How does this integrate into/with abp's EntityFramework?
Where am i supposed to be creating my Database Table/Class? The project follows the Core.csproj, Application.csproj, and EntityFramework.csproj assembly layout. But it seems like every example is creating the classes at different stages or locations of the solution.
use GetAllIncluding. See https://github.com/aspnetboilerplate/aspnetboilerplate/issues/2617
Here's a complete solution ;
namespace EbicogluSoftware.Forum.Threads
{
[Table("Threads")]
public class Thread : FullAuditedEntity
{
[Required]
[StringLength(500)]
public virtual string Title { get; set; }
[Required]
[StringLength(2000)]
public virtual string Text { get; set; }
public virtual List<Post> Posts { get; set; }
public Thread()
{
Posts = new List<Post>();
}
}
[Table("Posts")]
public class Post : FullAuditedEntity
{
[Required]
[StringLength(2000)]
public virtual string Text { get; set; }
}
public class ThreadDto
{
public string Title { get; set; }
public string Text { get; set; }
public List<PostDto> Posts { get; set; }
public ThreadDto()
{
Posts = new List<PostDto>();
}
}
public class PostDto
{
public string Text { get; set; }
}
public class ThreadAppService : IApplicationService
{
private readonly IRepository<Thread> _threadRepository;
public ThreadAppService(IRepository<Thread> threadRepository)
{
_threadRepository = threadRepository;
}
public async Task<List<TenantListDto>> GetThreads()
{
var threads = await _threadRepository.GetAllIncluding(x => x.Posts).ToListAsync();
return threads.MapTo<List<TenantListDto>>();
}
}
}
Where am i supposed to be creating my Database Table/Class?
You can create them in YourProject.Core.proj
I am creating a simple blogging application to get .NET MVC 4 down and I am having a problem. Everything works except for when I try to tag a blog using an array of strings for each blog like so:
public class BlogEntry
{
public List<Comment> BlogComments { get; set; }
public virtual List<String> RawTags { get; set; }
public virtual List<Tag> BlogTags { get; set; }
public virtual User Author { get; set; }
public int AuthorId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public DateTime DatePosted { get; set; }
[Key]
public int Id { get; set; }
public bool IsAcceptingComments { get; set; }
public bool IsVisible { get; set; }
public DateTime LastEdited { get; set; }
}
public class Tag
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public int RefCount { get; set; }
}
Upon creating a blog and tagging it, I save tags into the BlogEntry model using this:
[HttpPost]
public int Create(string data)
{
if (data != null)
{
BlogEntry newBlog = JsonConvert.DeserializeObject<BlogEntry>(data);
newBlog.Author = Session["user"] as User;
newBlog.AuthorId = newBlog.Author.Id;
newBlog.IsVisible = true;
newBlog.IsAcceptingComments = true;
newBlog.LastEdited = DateTime.Now;
newBlog.DatePosted = DateTime.Now;
newBlog.BlogTags = new List<Tag>();
foreach (String s in newBlog.RawTags)
{
// First check to see if the tag already exists
Tag check = Db.Tags.Where(m => m.Name == s).FirstOrDefault();
if (check != null)
{
check.RefCount++;
newBlog.BlogTags.Add(check);
Db.Tags.Attach(check);
Db.Entry(check).State = System.Data.Entity.EntityState.Modified;
Db.SaveChanges();
}
else
{
// Create a new tag
Tag newTag = new Tag();
newTag.Name = s;
newTag.RefCount = 1;
newBlog.BlogTags.Add(newTag);
Db.Tags.Add(newTag);
}
}
Db.BlogEntries.Add(newBlog);
Db.SaveChanges();
return newBlog.Id;
}
return -1;
}
First I do a check to see if a tag already exists.. If it does, I try to add the same tag, check to the newBlog object. I would have thought that this would just save a reference to this Tag object in the DbSet, however, if I create multiple blogs posts with the tag "html" and then run a query to see what blogs have the html tag, only the most recently tagged blog retains this value.... What can I do so that I can have multiple BlogEntry objects with the same Tag object in the database?
I don't have my dev machine in front of me right now, so this is just a guess, but I figure it's better than making you wait until tomorrow...
I don't think you need the last 3 lines in your if(check!=null) and in fact, I wonder if they aren't messing you up:
Db.Tags.Attach(check);
Db.Entry(check).State = System.Data.Entity.EntityState.Modified;
Db.SaveChanges();
You shouldn't need to attach because you got it from the Db object already, so it should already be being tracked. This means you don't need to change the state and as for the SaveChanges, you are going to do that below.
And now another disclaimer: I've done some work with Entity Framework (version 6, if you want to know), but not with MVC, so it may be different, but my understanding is that it is better to create a new DbContext for each set of instructions, rather than having a class variable that just tracks running changes. I'm not sure if that is what you are doing or not, but it sort of looks that way from this code sample. Assuming that is relevant in MVC, you may consider creating a new DbContext (Db) at the top of your create method.
Let me know how it goes--if this doesn't help, I'll delete this answer.
First you would have to update the Tag class so that it can track its registered blog entries itself. Here the BlogEntry and Tag classes have a many-to-many relationship. So the Tag class would look like below:
public class Tag
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public int RefCount { get; set; }
public virtual List<BlogEntry> BlogEntries { get; set; } // MODIFICATION
}
Now you have to add the blog entry to all of its tags for back referencing to meet your query in an easy way. Look for the modifications I have made in the for-loop below:
foreach (String s in newBlog.RawTags)
{
// First check to see if the tag already exists
Tag check = Db.Tags.Where(m => m.Name == s).FirstOrDefault();
if (check != null)
{
check.RefCount++;
check.BlogEntries.Add(newBlog); // MODIFICATION
newBlog.BlogTags.Add(check);
Db.Tags.Attach(check);
Db.Entry(check).State = System.Data.Entity.EntityState.Modified;
Db.SaveChanges();
}
else
{
// Create a new tag
Tag newTag = new Tag();
newTag.Name = s;
newTag.RefCount = 1;
newTag.BlogEntries = new List<BlogEntry>(); // MODIFICATION
newTag.BlogEntries.Add(newBlog); // MODIFICATION
newBlog.BlogTags.Add(newTag);
Db.Tags.Add(newTag);
}
}
To see what blogs have the html tag, you just have to query on the Tag class and search through the BlogEntries to get the desired blogs. Good luck!
I'm at a loss as to how to title this, so if someone can rename this more appropriately, that would be appreciated.
I'm stuck as to how I can go about populating custom properties of a partial entities class. Some examples
// partial class from EF base class - base class contains
// properties such as commentID, comment, userID etc
// Comments.cs
public partial class Comment {
public int UpVotes { get; set; }
public int DownVotes { get; set; }
public int CurrentVotes()
{
return UpVotes - DownVotes;
}
}
_
//CommentRepository.cs
//snip
public Comment GetItem(int id)
{
Comment c = db.Comments.SingleOrDefault(x => x.CommentID == id);
c.UpVotes = 0 //get UpVotes
c.DownVotes = 0 // get DownVotes
return c;
}
public IQueryable<Comment> GetAllCommentsByPage(int pageid)
{
}
public IQueryable<Comment> GetAllCommentsByPage(string slug)
{
}
public IQueryable<Comment> GetCommentSelection(int PageID, int Skip, int Take)
{
}
public int CountCommentByPage(int PageID)
{
}
In the old .Net 1.x days, I would have had the GetAllx methods select a list of CommentIDs, then populated a new List by using List.Add(GetItem(ID)).
In EF4, I want to do similar, but not lose out on the delayed execution of IQueryables.
In a couple of instances I have found it useful to stack these GetAllx methods in quick succession before doing a final .ToList() or similar to get the actual data.
Is there a sensible way for me to do what I hope is fairly simple and eluding me? I'd hate to have to change each method to return a static List of items that can be generated through the one GetItem method.
Thanks in advance.
----- edit -----
Okay, here's the solution I have currently:
public List<Comment> IQueryable2ToList(IQueryable<Comment> c)
{
List<Comment> comments = new List<Comment>();
List<int> ids = c.Select(x => x.CommentID).ToList();
foreach (int id in ids) {
comments.Add(GetComment(id));
}
return comments;
}
Which I call by doing:
List<Comment> comments = (new CommentRepository()).IQueryable2ToList(db.FindAllCommentsByPage(slug));
Which just seems a little dirty somehow...
Well, you can eliminate the n+1 selects:
public List<Comment> IQueryable2ToList(IQueryable<Comment> comments)
{
List<Comment> comments = comments.ToList()
foreach (Comment c in comments) {
c.UpVotes = 0 //get UpVotes
c.DownVotes = 0 // get DownVotes
}
return comments;
}
However, that's not what I'd do. Instead I'd make a presentation model and project:
public class CommentPresentation {
public int CommentID { get; set; }
public string WittyInsight { get; set; }
public int UpVotes { get; set; }
public int DownVotes { get; set; }
public int CurrentVotes()
{
return UpVotes - DownVotes;
}
}
public IQueryable<CommentPresentation> ToPresentation(IQueryable<Comment> comments)
{
return from c in comments
select new CommentPresentation
{
CommentId = c.CommentId,
WittyInsight = c.WittyInsight,
UpVotes = 0,
DownVotes = 0
};
}
If you want to assign something other than a constant, you may have to go through AsEnumerable(), so you'd want to do paging first. But this should get you started.