SQLite-Net-Extensions GetWithChildren() Method only reaches first Child Layer - c#

currently I am working with the NuGet SQLite-Net-Extensions in Xamarin Forms and I have encountered a problem for which I can't find a solution.
The Problem: When calling GetWithChildren(primaryKey, recursive: true), the returned object only contains the first child layer. An example can be seen in the following.
The database is built up like this:
The equivalent code to this model is provided in the following:
User
namespace DatabaseTest
{
[Table("Users")]
public class User
{
[PrimaryKey,AutoIncrement]
public int Id { get; set; }
public string Name { get; set; }
[OneToMany]
public List<Contact> Contacts { get; set; }
}
}
Contact
namespace DatabaseTest
{
[Table("Contacts")]
public class Contact
{
[PrimaryKey,AutoIncrement]
public int Id { get; set; }
public string Name { get; set; }
[OneToMany]
public List<Entry> Entries { get; set; }
[ForeignKey(typeof(User))]
public int UserID { get; set; }
[ManyToOne]
public User User { get; set; }
}
}
Entry
namespace DatabaseTest
{
[Table("Entries")]
public class Entry
{
[PrimaryKey,AutoIncrement]
public int Id { get; set; }
public float Amount { get; set; }
[ForeignKey(typeof(Contact))]
public int ContactId { get; set; }
[ManyToOne]
public Contact Contact { get; set; }
}
}
In my App.cs I am creating the database and use the CreateTable() Method for all three classes. For the sake of this example, in MainPage.xaml.cs there is simply a button, which has a ButtonClicked Method.
In the real Application a process could look like this:
User logs in --> Adds Contact --> At some Point User creates Entry to one of his contacts
To accomplish this procedure in my example, the ButtonClicked Method looks like this:
void ButtonClicked(object sender, EventArgs e)
{
try
{
User user = new User()
{
Name = "Test user"
};
Contact contact = new Contact()
{
Name = "First contact"
};
Entry entry1 = new Entry()
{
Amount = 10F
};
Entry entry2 = new Entry()
{
Amount = 20F
};
App.Database.Insert(user);
if (user.Contacts==null)
{
user.Contacts = new List<Contact>();
}
App.Database.Insert(contact);
user.Contacts.Add(contact);
App.Database.UpdateWithChildren(user);
if (contact.Entries==null)
{
contact.Entries = new List<Entry>();
}
App.Database.Insert(entry1);
App.Database.Insert(entry2);
contact.Entries.Add(entry1);
contact.Entries.Add(entry2);
App.Database.UpdateWithChildren(contact);
App.Database.UpdateWithChildren(user);
var test = App.Database.GetWithChildren<User>(user.Id, recursive: true);
var test2 = App.Database.GetAllWithChildren<Contact>();
var test3 = App.Database.GetAllWithChildren<Entry>();
} catch (Exception ex)
{
Debug.Print(ex.Message);
}
}
I have set a breakpoint to the bracket that closes the try to inspect the result. In the End, my user looks like this:
Which is absolutely perfect.
However, when I try to get this user from my database, the result looks like this:
I don't know how to resolve this error and hope anyone can help me with this problem.
Since this post is very long already, I thank everyone who read this far in advance.

After many tries I finally solved my problem on my own.
Solution:
It could not have been any easier. In my User, Contact and Entry Class I provided my OneToMany and ManyToOne attributes with the CascadeOperation attribute. Example:
[OneToMany(CascadeOperations = CascadeOperation.All)]
public List<Entry> Entries { get; set; }
[ManyToOne(CascadeOperations = CascadeOperation.All)]
public User User { get; set; }
Even though I marked my GetWithChildren() Method as recursive: true, only by applying CascadeOperations it will work properly. More information about SQLite-Net-Extensions and CascadeOperations can be found here:
Source: TwinCoders SQLite-Net-Extensions

Related

Deleting missing descendant on Update

I have the following method in my controller:
[HttpPost]
public async Task<ActionResult<ChartsCategoryDto>> PostChartsCategory(ChartsCategoryDto chartsCategory)
{
try
{
var category = _mapper.Map<ChartsCategoryDto, ChartsCategory>(chartsCategory);
if (category.Id == 0)
{
category = _context.ChartsCategories.Add(category).Entity;
await _context.SaveChangesAsync();
return CreatedAtAction("GetChartsCategory", new { id = category.Id }, _mapper.Map<ChartsCategory, ChartsCategoryDto>(category));
}
else
{
_context.ChartsCategories.Update(category);
await _context.SaveChangesAsync();
return NoContent();
}
}
catch (Exception)
{
return StatusCode(StatusCodes.Status500InternalServerError, "Error updating data");
}
}
Which is supposed to handle both create and update of the entity named ChartsCategory, which has a collection of Charts:
public partial class ChartsCategory
{
public ChartsCategory()
{
Charts = new HashSet<Chart>();
InverseParent = new HashSet<ChartsCategory>();
}
public int Id { get; set; }
public string Name { get; set; }
public string Label { get; set; }
public string Icon { get; set; }
public int? ParentId { get; set; }
public virtual ChartsCategory Parent { get; set; }
public virtual ICollection<Chart> Charts { get; set; }
public virtual ICollection<ChartsCategory> InverseParent { get; set; }
}
When I create a new entity - everything works as expected.
When I update an existing entity - everything works as expected.
Updating or creating a singular Chart within a ChartCategory - as expected.
Deleting a Chart within the ChartsCategory doesn't work.
I would expect the Update() functionality to remove missing items.
I found this answer however it seems a little too explicit. I want to globally state that a descendant item must be sent, otherwise it should be removed.
Thanks
I think that in the first case the retrieved user is not tracked by the context.
_context.ChartsCategories.AsNoTrcking.Remove(category);

Nested one-to-many tables SQLite-Net Extensions [duplicate]

I am trying to use SQLite-Net Extensions to create a Relational Database. I'm running into an issue when trying to pull the Term object from the database. It successfully pulls over its associated courses, but not the courses associated assessments and notes. I'm not sure if the problem lies in how I insert the objects into the database, how I pull the objects from the database, or how I have the objects attributes listed.
I feel like the SQLite-Net Extensions documentation is extremely limited, so I'm not even sure what's going on. I've tried it many different ways, including adding CascadeOperations, but non of those seemed to help.
Here is the (simplified) code for my objects:
[Table("Terms")]
public class Term
{
[PrimaryKey, AutoIncrement]
public int ID { get; set; }
public string Name { get; set; }
[OneToMany]
public List<Course> Courses { get; set; }
public Term() { }
public Term(string name, List<Course> courses)
{
Name = name;
Courses = courses;
}
Courses
[Table("Courses")]
public class Course
{
[PrimaryKey, AutoIncrement]
public int ID { get; set; }
[ForeignKey(typeof(Term))]
public int TermID { get; set; }
public string Name { get; set; }
[OneToMany]
public List<Assessment> Assessments { get; set; }
[OneToMany]
public List<Note> Notes { get; set; }
public Course() { }
public Course(string name, List<Assessment> assessments, List<Note> notes)
{
Name = name;
Assessments = assessments;
Notes = notes;
}
}
Assessments
[Table("Assessments")]
public class Assessment
{
[PrimaryKey, AutoIncrement]
public int ID { get; set; }
[ForeignKey(typeof(Course))]
public int CourseID { get; set; }
public string Name { get; set; }
public Assessment() { }
public Assessment(string name)
{
Name = name;
}
}
Notes
[Table("Notes")]
public class Note
{
[PrimaryKey, AutoIncrement]
public int ID { get; set; }
[ForeignKey(typeof(Course))]
public int CourseID { get; set; }
public string Name { get; set; }
public string Text { get; set; }
public Note() { }
public Note(string name, string note)
{
Name = name;
Text = note;
}
}
And here is the code for inserting and getting objects:
Inserting
public bool SaveTermAsync(Term term)
{
if (term.ID != 0)
{
_database.UpdateWithChildrenAsync(term);
return true;
}
else
{
foreach (var course in term.Courses)
{
foreach (var assessment in course.Assessments)
{
_database.InsertAsync(assessment);
}
foreach (var note in course.Notes)
{
_database.InsertAsync(note);
}
_database.InsertAsync(course);
}
_database.InsertAsync(term);
_database.UpdateWithChildrenAsync(term);
return false;
}
}
Getting
public Task<List<Term>> GetTermsAsync()
{
return _database.GetAllWithChildrenAsync<Term>();
}
I know it's a bit of a code dump, but I have no idea where or what could be going wrong. If anyone could give any information about what is potentially going wrong, that would be awesome. Perhaps I'm simply expecting something to happen that isn't actually how it works. I don't know.
Also, if anyone has any links to some better documentation than https://bitbucket.org/twincoders/sqlite-net-extensions/src/master/ that would be awesome
EDIT
I tried using Cascading Options as well, CascadeRead, CascadeInsert, and CascadeAll. Using CascadeInsert or CascadeAll with _database.InsertWithChildrenAsync(term, true) resulted in a crash. The crash does not provide any error messages, and even wrapping the InsertWithChildren with a try catch block didn't work. Removing the recursive bool caused the program not to crash, and actually get the closest to what I'm looking for. Assessments and Notes are no longer null, but are still empty. Here's my updated code:
Saving and Getting:
public async Task<List<Term>> GetTermsAsync()
{
return await _database.GetAllWithChildrenAsync<Term>(recursive: true);
}
public async void SaveTermAsync(Term term)
{
if (term.ID != 0)
{
await _database.UpdateWithChildrenAsync(term);
}
else
{
//Trying this with recursion results in crash
await _database.InsertWithChildrenAsync(term);
}
}
One-To-Many Relationships:
//In Term
[OneToMany(CascadeOperations = CascadeOperation.All)]
public List<Course> Courses { get; set; }
//In Courses
[OneToMany(CascadeOperations = CascadeOperation.All)]
public List<Assessment> Assessments { get; set; }
[OneToMany(CascadeOperations = CascadeOperation.All)]
public List<Note> Notes { get; set; }
Also, I forgot to include last time how I'm populating the tables in the first place.
public bool CreateTables()
{
_database.CreateTableAsync<Term>().Wait();
_database.CreateTableAsync<Course>().Wait();
_database.CreateTableAsync<Assessment>().Wait();
_database.CreateTableAsync<Note>().Wait();
return true;
}
public Task<int> ClearTablesTest()
{
_database.DropTableAsync<Term>();
_database.DropTableAsync<Course>();
_database.DropTableAsync<Assessment>();
return _database.DropTableAsync<Note>();
}
async public Task<int> PopulateTestData()
{
await ClearTablesTest();
CreateTables();
Term term = new Term("Test Term", true, DateTime.Now, DateTime.Now.AddDays(10),
new List<Course>
{
new Course("Test Course", CourseStatus.Completed, "Guys Name", "(999)-999-9999", "email#gmail.com", 6, DateTime.Now, DateTime.Now.AddDays(10),
new List<Assessment>
{
new Assessment("Test Assessment", AssessmentType.Objective, false, DateTime.Now, DateTime.Now.AddDays(10))
},
new List<Note>
{
new Note("Test Note", "This is a test note.")
})
});
App.Database.SaveTermAsync(term);
return 0;
}
I finally figured out what was causing the crash as well as causing general confusion within SQLite-Net Extensions.
In my Assessment class, the property
public string BackgroundColor
{
get { return IsComplete ? "#558f45" : "Gray"; }
set { BackgroundColor = value; }
}
was causing the crash when recursion was used. I've been scouring the web for over two weeks looking for solutions to this issue, but haven't found anything similar to this. I submitted a bug report on the SQLite-Net Extensions bitbucket.
If anyone knows why this specific line would cause issues, I'd love to hear your input. Until then I'm going to mark this question as answered and continue work on my app.
Thanks #redent84 for your help thus far on this issue.

Adding a class to an ICollection List

I have a C# project in which users can log in and create boards. The user class has an ICollection for listing all the boards made by a specific user and I am trying to figure out how to add that board to the list.
These are the two classes, the User class:
public class User
{
public int ID { get; set; }
public string Name { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string UserType { get; set; }
public DateTime LastLoginDate { get; set; }
public virtual ICollection<Board> Boards { get; set; }
public virtual ICollection<Post> Posts { get; set; }
And this is the Board class:
public class Board
{
public int ID { get; set; }
public string Name { get; set; }
public DateTime DateCreated { get; set; }
public virtual ICollection<Post> Posts { get; set; }
The aim is that each User would have a list of all the boards that have been added they have created so it is necessary for the board to be added to that list when it is created. This the webform where a board can be added:
public partial class AddBoard : System.Web.UI.Page
{
Board boardToAdd = new Board();
User user = new User();
Utility utility = new Utility();
static User loggedInUser;
protected void CreateButton_Click(object sender, EventArgs e)
{
string name = NameTextBox.Text;
loggedInUser = (User)Session["loggedInUser"];
string checkName = utility.CheckBoardName(name);
if (checkName == "OK")
{
boardToAdd.Name = name;
boardToAdd.DateCreated = DateTime.Now;
user.AddBoard(boardToAdd, loggedInUser);
boardToAdd.AddBoard(boardToAdd);
}
else
{
CreateLabel.Text = checkName;
}
}
And this is the method in the user class I've been trying to use to add the board to the list:
public User AddBoard(Board board, User user)
{
BulletinContext _context = new BulletinContext();
user.Boards.Add(board);
return null;
}
}
For added reference this is the method in the board class I am calling in order to add the board:
public bool AddBoard(Board board)
{
BulletinContext _context = new BulletinContext();
try
{
_context.Boards.Add(board);
_context.SaveChanges();
return true;
}
catch
{
return false;
}
}
}
In your AddBoard, you are adding to a local variable.
After function exit, this is destroyed.
Promote your _context to a class member
As Pieter21 said, the variable _context in Board.AddBoard() is recreated each time you call Board.AddBoard(). As far as I can see, _context should be static and declared outside of the AddBoard() method (since you probably need to access it later).
I've managed to sort out the problem. I just added in code to my method to call the user that the board was being added to and the board to be added.It now looks like this:
public User AddBoard(Board board, User user)
{
BulletinContext _context = new BulletinContext();
User _user = _context.Users.Find(user.ID);
Board _board = _context.Boards.Find(board.ID);
_user.Boards.Add(_board);
_context.SaveChanges();
return null;
}
It's necessary to call this method from the other method that adds the board to the Board class so that it occurs afterwards.

Iteration error in Entity Framework

I'm creating a simple Messaging application with C# Winforms. I'm connecting to a SQLEXPRESS server running on my computer and storing everything there.
Here is my schema:
public class UserContext : DbContext {
public UserContext() : base("name=BuddyDatabase") {
}
public DbSet<User> Users { get; set; }
public DbSet<Message> Messages { get; set; }
}
public class User {
[Key]
public string username { get; set; }
public string password { get; set; }
public static implicit operator User(bool v) {
throw new NotImplementedException();
}
public virtual List<User> friends { get; set; }
}
public class Message {
[Key]
public int ID { get; set; }
public virtual User sender { get; set; }
public virtual User recipient { get; set; }
public string content { get; set; }
public virtual List<User> group { get; set; }
}
Pretty simple
Sending messages to a single recipient works but group messaging doesn't, whenever I open the "Messages Screen" I get this error:
Unable to create a constant value of type 'WindowsFormsApp1.User'. Only primitive types or enumeration types are supported in this context.
And this is the method I have run when the Message Screen loads up:
var x = db.Messages.Where(b => b.recipient.username == currentuser.username);
foreach (var y in x) {
MainMessagesBox.Text += y.content;
}
x = null;
var z = db.Messages.Where(b => b.group.Contains(currentuser));
foreach (var y in z) {
MainMessagesBox.Text += y.content;
}
(Visual Studio highlights the 'in' in this line as the cause of the error.)
foreach (var y in z) {
Thought this might be a problem with me using the Where method and involving non-primitives (as the error message suggests) so I tried changing my schema so that group is a list of strings that contain the usernames of the intended recipients and modified the methods accordingly but that didn't work either. Will provide code and errors for that trial on request.
Here is the actual "Send Message" code:
if (!textBox1.Text.Contains(',')) {
db.Messages.Add(new Message { sender = currentuser, recipient = db.Users.Find(textBox1.Text), content = currentuser.username + ": " + ContentBox.Text + "\n" });
db.SaveChanges();
} else {
List<User> recips = new List<User>();
string[] poop = textBox1.Text.Split(',');
foreach (var x in poop) {
recips.Add(db.Users.Find(x));
}
db.Messages.Add(new Message { sender = currentuser, group = recips, content = ContentBox.Text });
db.SaveChanges();
}
poop is an array of the intended recipients separated by comma from a textbox, this is temporary.
Sorry if anything is misformatted or I'm unclear, this is my first question.
Thank you in advance.
The problem is the Select query on message table where you check for the current user to be in the group list. This won't work, because the query has to be translated to SQL to be send to the database.
SQL does not understand what a ´User´ type is and can't compare references. And actually, neither does C# compare this properly. Two objects of type ´User´ with the same username would not be equal in your case. You need to compare the username of those objects.
Regardless, as Bitmask explained in a comment, you have to properly define the foreign key for the messages.
It's quite easy for the sender of the messages. You have a public virtual ICollection<Message> Messages { get; set; } in your User class.
But for the recipients, it's a many-to-many relation. So both, the User class and the Message class have a collection to Message and User respectively.
Something like this should work:
public class User {
[Key]
public string username { get; set; }
public string password { get; set; }
public static implicit operator User(bool v) {
throw new NotImplementedException();
}
public virtual ICollection<Message> SentMessages { get; set; }
public virtual ICollection<Message> ReceivedMessages { get; set; }
public virtual List<User> friends { get; set; }
}
public class Message {
[Key]
public int ID { get; set; }
public virtual User Sender { get; set; }
public virtual ICollection<User> Recipients { get; set; }
public string content { get; set; }
}
Check this link for an example on how to define a many-to-many relation either using data annotations or fluent API. This assumes you're using EF6 though. If you're using an older version of EF, you might have to define the joining table yourself to get the many-to-many relation.
And as for the query. You can use the following in your message screen OnLoad method:
MainMessagesBox.Text = string.Join(System.Environment.NewLine, currentuser.ReceivedMessages.Select(m => m.content))
This concatenates all messages, separated with a new line.

Error inserting record with entity framework

I am sorry if it has already been answered but I can't find any solution. Here is my (little) problem. Also all my apologies if the terms I use are approximate, I am far from being a skilled C# developer
Note that I think my problem is similar to this one Entity Framework validation error for missing field, but it's not missing?
I have a table "Tweets" with a tweet_id field (bigint) which is my primary key.
I use the following class to load the table :
class TwitterDbContext : DbContext
{
public TwitterDbContext() : base("Twitter")
{
}
public DbSet<Stream> Streams { get; set; }
public DbSet<StreamParameter> StreamParameters { get; set; }
public DbSet<Tweet> Tweets { get; set; }
}
public class Tweet
{
public Tweet()
{
}
[Key]
public long tweet_id { get; set; }
public string tweet { get; set; }
public long creator { get; set; }
public double latitude { get; set; }
public double longitude { get; set; }
public string language { get; set; }
public DateTime created_at { get; set; }
public DateTime registered_at { get; set; }
public long? in_reply_to { get; set; }
public bool retweeted { get; set; }
}
I have an other class to store within the code execution all the fields used by the Tweet table. For the need here, let's imagine I manually create it that way
private void Test_Button_Click(object sender, RoutedEventArgs e)
{
Twts twtReceived = new Twts();
twtReceived.tweet_id = 1;
twtReceived.tweet = "test";
twtReceived.creator = 1;
twtReceived.latitude = -1;
twtReceived.longitude = -1;
twtReceived.language = "a";
twtReceived.created_at = DateTime.Now;
twtReceived.registered_at = DateTime.Now;
twtReceived.in_reply_to = 1;
twtReceived.retweeted = true;
AddTweet(twtReceived);
}
Now here is the AddTweet method
static public void AddTweet(Twts twtReceived)
{
try
{
// update the tweet data in the database
using (var TwitterDb = new TwitterDbContext())
{
Tweet twt = new Tweet()
{
tweet_id = twtReceived.tweet_id,
tweet = twtReceived.tweet,
creator = twtReceived.creator,
longitude = twtReceived.longitude,
latitude = twtReceived.latitude,
language = twtReceived.language,
created_at = twtReceived.created_at,
registered_at = twtReceived.registered_at,
in_reply_to = twtReceived.in_reply_to,
retweeted = twtReceived.retweeted
};
TwitterDb.Tweets.Add(twt);
TwitterDb.SaveChanges();
}
}
catch(Exception ex)
{
MessageBox.Show(ex.InnerException.ToString());
}
}
I constantly have the same error message:
Cannot insert the value NULL into column 'tweet_id', table
'Twitter.dbo.Tweets'; column does not allow nulls. INSERT fails.
The thing is that when I spy on "TwitterDb.Tweets.Local" after TwitterDb.Tweets.Add(twt); I correctly have tweet_id set to 1.
Any idea where is the issue?
Try marking your tweet_id field with following (instead of just [Key]), if this is a primary key column where you want to provide values yourself
[Required, Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
If it is an auto-increment, then remove explicit assignments to this field and mark it as 'Identity' instead:
[Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]

Categories

Resources