Dynamically add a linked field to an ASP.NET MVC EF model - c#

I have the following database table:
ID ParentID LinksTo
---------------------
0 null "1,2"
1 0 "0,2"
2 0 "1"
It's a hierarchical design, where each records points to a parent record from the same table. I know the LinksTo field is a very bad design, but the table is given, I cannot change that. (Note that the LinksTo field creates a sort of many-to-many relationship.)
Using Code-First EF, I have the following model class:
public class Item
{
public int ID { get; set; }
public int ParentID { get; set; }
public string LinksTo { get; set; }
public virtual Item Parent { get; set; }
[InverseProperty("Parent")]
public virtual ICollection<Item> Children { get; set; }
}
Now, how can I add a dynamic property to the model so that I can access the Item collections Item.LinksTo and Item.LinksFrom (the reverse) in code?

I guess I'd do something like this:
private List<Item> _linkedItems;
private void UpdateLinksTo() {
this.LinksTo = string.Join<string>(_linkedItems.Select(i => i.ID.ToString()));
}
[NotMapped]
public ReadOnlyCollection<Item> LinkedItems {
get {
if(_linkedItems == null) {
_linkedItems = db.Items.Where(i => this.LinksTo.Split(',').Select(x => int.Parse(x)).Contains(i.ID)).ToList();
}
return _linkedItems.AsReadOnly();
}
}
[NotMapped]
public void AddLinkedItem(Item item) {
if(!_linkedItems.Select(i => i.ID).Contains(item.ID)) {
_linkedItems.Add(item);
UpdateLinksTo();
}
}
That will give you access to a read-only collection with methods to alter it (you can also make a DeleteLinkedItem method) which is about as good as you're going to do I think.
I haven't checked to even see if this compiles, btw.

Related

What's the correct way to reference tables using Code First with EF.Core for searching efficiently

Fairly new to EF.Core and I'm having some issues as my tables start getting more complex. Here's an example of what I have defined for my classes. Note ... there are many more columns and tables than what I have defined below. I've paired them down for brevity.
public class User
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public bool Active { get; set; }
}
Followed by
public class JournalEntry
{
public int Id { get; set; }
public int UserId { get; set; }
public string Details { get; set; }
public DateTime DateEntered { get; set; }
public virtual User User { get; set; }
}
I want to be able to issue the following query and INCLUDE the User Table so that I can then populate a ViewModel with columns from the User Table without having to do another lookup and also to sort the data while retrieving it:
public IQueryable<JournalEntry> GetByUser(int userId)
{
return _DbContext.JournalEntries.Where(j => j.UserId == userId)
.Include(u => u.User)
.OrderBy(u=> u.User.FirstName)
.ThenBy(j => j.DateEntered);
}
My controller would then have something similar to the following:
public IActionResult List(int userId)
{
var journalEntries = new _dbRepository.GetByUser(userId);
var myViewModel = new MyViewModel();
myViewModel.UserName = ($"{journalEntries.User.FirstName} {journalEntries.User.LastName}");
myViewModel.Entries = journalEntries;
etc ....
return View(myViewModel);
}
I'm loading the user's first and last name in the View Model and whatever other attributes from the various tables that are referenced. The problem that I'm having is that I'm getting errors on the Migration creation "Foreign key constraint may cause cycle or multiple cascade paths." And of course, if I remove the line reading public virtual User User { get; set; } from the JournalEntry class then the problem goes away (as one would expect).
I believe that the way I'm doing the models is incorrect. What would be the recommended way that I should code these models? I've heard of "lazy loading". Is that what I should be moving towards?
Thanks a bunch.
--- Val
Your query returns an IQueryable<JournalEntry> not a JournalEntry.
Change the code to get the user details from the first object:
var myViewModel.UserName = ($"{journalEntries.First().User.FirstName} {journalEntries.First().User.LastName}");
In the line above I'm calling First() on your journal entries collection and that would have a User. Then I can access FirstName and LastName.
Also, don't bother with LazyLoading since you are learning. It could cause select n+1 issues if used incorrectly

Copying data between models and saving children without entities duplicating themselves in Entity Framework

I am having trouble saving children entities via Entity Framework / ASP Identity. It seems to be adding duplicates of everything that is added.
I have tried using a detached graph of the DrivingLicenceModel by TeamMember.DrivingLicence = null in the TeamMemberModel and then working with a detached graph by looking if there is new or old DrivingLicenceCategories but because DrivingLicence links back to TeamMember it causes TeamMember.DrivingLicenceId to be null as it cannot link back to TeamMember.
I have tried Manually adding the EntityState to the DrivingLicence and DrivingLicenceCategories but when I do that it complains that it cannot save two entities with the same primary key.
I assume this is because they way I am copying the entities but I after a lot of looking I am drawing a blank.
If there anyway to copy from TeamMemberRequestModel to TeamMemberModel and then save without the children trying to create clone copies of themselves?
Models
public class TeamMemberModel : IdentityUser
{
public virtual DrivingLicenceModel DrivingLicence { get; set; }
public void ShallowCopy(TeamMemberRequestModel src)
{
this.DateOfBirth = src.DateOfBirth;
if (src.DrivingLicence != null)
{
if (this.DrivingLicence == null)
{
this.DrivingLicence = new DrivingLicenceModel(src.DrivingLicence);
}
else
{
this.DrivingLicence.ShallowCopy(src.DrivingLicence);
}
}
}
public TeamMemberModel() { }
}
public class DrivingLicenceModel
{
[Key]
public int Id { get; set; }
[ForeignKey("TeamMember")]
public string TeamMemberId { get; set; }
[JsonIgnore]
public TeamMemberModel TeamMember { get; set; }
public virtual List<DrivingLicenceCategoryModel> DrivingLicenceCategories { get; set; }
public DrivingLicenceModel() { }
public DrivingLicenceModel(DrivingLicenceModel src)
{
this.ShallowCopy(src);
}
public void ShallowCopy(DrivingLicenceModel src)
{
this.Id = src.Id;
this.IsFullLicence = src.IsFullLicence;
this.IssueDate = src.IssueDate;
this.ExpiryDate = src.ExpiryDate;
this.IssuingAuthority = src.IssuingAuthority;
this.LicenceNumber = src.LicenceNumber;
this.DrivingLicenceCategories = src.DrivingLicenceCategories;
this.DrivingLicencePoints = src.DrivingLicencePoints;
}
}
public class DrivingLicenceCategoryModel
{
[Key]
public int Id { get; set; }
[ForeignKey("DrivingLicence")]
public int DrivingLicenceId { get; set; }
[JsonIgnore]
public DrivingLicenceModel DrivingLicence { get; set; }
}
public class TeamMemberRequestModel
{
public string Id { get; set; }
public virtual DrivingLicenceModel DrivingLicence { get; set; }
}
Context
public class TIERDBContext : IdentityDbContext<TeamMemberModel, RoleModel, string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim>
{
public TIERDBContext() : base("SARDBConnection") { }
public DbSet<DrivingLicenceModel> DrivingLicences { get; set; }
public DbSet<DrivingLicenceCategoryModel> DrivingLicenceCategories { get; set; }
}
Controller
public async Task<IHttpActionResult> Put(string id, TeamMemberRequestModel teamMember)
{
TeamMemberModel CurrentTeamMember = await this.TIERUserManager.FindByIdAsync(id);
CurrentTeamMember.ShallowCopy(teamMember);
await this.TIERUserManager.UpdateAsync(CurrentTeamMember);
}
you have to create clone property into context class
.
In the context clases you could to use clone method that retiran the entity you send by parameters this duplicarse any entity you pass. Sorry for my english
hope you help
After far to many hours working over this. I have come to an answer. The best way to deal with this is to simply deal with it is to add or attach all entities down the tree.
The controller now attaches all children unless they have an ID of 0, therefore new and uses add instead. Then I use this very useful extension I found here http://yassershaikh.com/c-exceptby-extension-method/ to compare lists to see added and deleted entities in the list. While I don't need the added part as the entity will already be marked to an add state as I use add() it does not harm and I want to use it later with add and delete state changing.
Controller
public async Task<IHttpActionResult> Put(string id, TeamMemberRequestModel teamMember)
{
TIERDBContext IdentityContext = (TIERDBContext)this.TIERUserManager.UserStore().Context;
foreach (DrivingLicenceCategoryModel DrivingLicenceCategory in teamMember.DrivingLicence.DrivingLicenceCategories)
{
if (DrivingLicenceCategory.Id == 0)
{
IdentityContext.DrivingLicenceCategories.Add(DrivingLicenceCategory);
}
else
{
IdentityContext.DrivingLicenceCategories.Attach(DrivingLicenceCategory);
}
}
foreach (DrivingLicencePointModel DrivingLicencePoint in teamMember.DrivingLicence.DrivingLicencePoints)
{
if (DrivingLicencePoint.Id == 0)
{
IdentityContext.DrivingLicencePoints.Add(DrivingLicencePoint);
}
else
{
IdentityContext.DrivingLicencePoints.Attach(DrivingLicencePoint);
}
}
this.DetectAddedOrRemoveAndSetEntityState(CurrentTeamMember.DrivingLicence.DrivingLicenceCategories.AsQueryable(),teamMember.DrivingLicence.DrivingLicenceCategories, IdentityContext);
this.DetectAddedOrRemoveAndSetEntityState(CurrentTeamMember.DrivingLicence.DrivingLicencePoints.AsQueryable(),teamMember.DrivingLicence.DrivingLicencePoints, IdentityContext);
CurrentTeamMember.ShallowCopy(teamMember);
await this.TIERUserManager.UpdateAsync(CurrentTeamMember);
}
I then use a generic that uses ExceptBy to work out what is added and delete from the old team member model to the new team member model.
protected void DetectAddedOrRemoveAndSetEntityState<T>(IQueryable<T> old, List<T> current, TIERDBContext context) where T : class, IHasIntID
{
List<T> OldList = old.ToList();
List<T> Added = current.ExceptBy(OldList, x => x.Id).ToList();
List<T> Deleted = OldList.ExceptBy(current, x => x.Id).ToList();
Added.ForEach(x => context.Entry(x).State = EntityState.Added);
Deleted.ForEach(x => context.Entry(x).State = EntityState.Deleted);
}
It works but it is far from great. It takes two DB queries, getting the original and updating. I just cannot think of any better way to do this.

SQLite, Linq, Foreach: how to optimize performances?

I've developed an UWP app where I use a SQLite database to store datas that are synced.
Among these data, there a lot of tables that contain translated data. For example, for various cases, we have:
a "businness" table, which contains the id that is really used in the database
a "translation" table, which contains transaltion for the business table
The models of the "business" tables are defined like this:
public class Failure_Type : BasePoco
{
[PrimaryKey, NotNull]
public int failure_id { get; set; }
public int? function_type_id { get; set; }
public int? component_category_id { get; set; }
[MaxLength(200), NotNull]
public string description { get; set; }
public DateTime? disable_date { get; set; }
[Ignore]
public string _descriptionTr { get; set; }
}
The field "description" stores the english/default description, and the "_descriptionTr" field will store the translated description.
The models of the "translation" tables are defined like this:
public class Failure_Type_Translation : BasePoco
{
[PrimaryKey, NotNull]
public int failure_type_translation_id { get; set; }
public int? failure_type_id { get; set; }
[MaxLength(2)]
public string language { get; set; }
[MaxLength(200), NotNull]
public string description { get; set; }
}
The field "failure_type_id" is related to the business table, the other fields store the language code and the related translation.
So, after syncing datas in the SQLite database, I refresh the "translated" datas in the app and this can take a long moment. The load of the the 2 tables from the SQLite is very quickly, but the update of the "_descriptionTr" field can be very slow:
var failureType = ServiceLocator.Current.GetInstance<IRepository>().GetAll<Failure_Type>();
var failureTypeTranslations = ServiceLocator.Current.GetInstance<IRepository>().GetAll<Failure_Type_Translation>();
FailureType = new ObservableCollection<Failure_Type>();
foreach (var ft in failureType)
{
var ftt = failureTypeTranslations.FirstOrDefault(i => i.failure_type_id == ft.failure_id && i.language.ToLower().Equals(lang));
if (ftt != null)
ft._descriptionTr = ftt.description;
else
ft._descriptionTr = ft.description;
FailureType.Add(ft);
}
Is there a better way for doing this?
How could I optimize it?
Edit :
the "business" table contains 550 rows
the "translation" table contains 3500 rows
the duration of the loop is nearly 1 minute
A couple of suggestions:
Create the observable collection at once ...
FailureType = new ObservableCollection<Failure_Type>(failureType);
... so the individual additions don't fire notifications. Now use FailureType in the loop.
Instead of fetching all translations, filter them by lang:
var failureTypeTranslations = ServiceLocator.Current.GetInstance<IRepository>()
.GetAll<Failure_Type_Translation>()
.Where(l => i.language == lang);
Create a dictionary for lookup of known translations:
var dict = failureTypeTranslations.ToDictionary(ftt => ftt.failure_id);
foreach (var ft in FailureType)
{
Failure_Type_Translation ftt;
if (dict.TryGetValue(ft.failure_id, out ftt)
ft._descriptionTr = ftt.description;
else
ft._descriptionTr = ft.description;
}
I think that esp. the part failureTypeTranslations.FirstOrDefault kills performance. The query is executed for each iteration of the loop.

Entity Framework - Mapped property will not populate, despite being retrieved by SQL

I'm trying to add a new sub-entity, product component ProductRevComp to an existing entity ProductRev. However when I retrieve an instance of the ProductRev class, the Comps collection is never populated (even when explicitly Including() it). I BELIEVE I have mapped everything correctly, but it has taken more fiddling than I want and this is the most likely place for a mistake to be hiding. However profiling the SQL statements show the relevent columns are being populated with the correct data.
Checking db.ProductRevComps (i.e. the DbSet of all my comps) shows the records can be loaded, and that mapping is working as expected.
Mappings:
public class ProductRevConfiguration : EntityTypeConfiguration<ProductRev>
{
public ProductRevConfiguration()
{
HasKey(p => p.ProductRevId);
HasMany(p => p.Comps).WithRequired().HasForeignKey(p => p.ParentProductRevId);
Ignore(p => p.ProgrammedParts);
}
}
public class ProductRevCompConfiguration : EntityTypeConfiguration<ProductRevComp>
{
public ProductRevCompConfiguration()
{
HasKey(p => new { p.ParentProductRevId, p.CompProductRevId });
HasRequired(p => p.ParentProductRev).WithMany().HasForeignKey(p => p.ParentProductRevId);
HasRequired(p => p.CompProductRev).WithMany().HasForeignKey(p => p.CompProductRevId);
}
}
Product entity (amazingly simplified):
public class ProductRev
{
public string ProductRevId { get; set; }
public virtual List<ProductRevComp> Comps { get; set; }
public virtual List<ProductRevComp> ProgrammedParts { get { return Comps; } }//Will be filtered once I get this working
public ProductRev() { }
}
Comp entity:
public class ProductRevComp
{
public string ParentProductRevId { get; set; }
public virtual ProductRev ParentProductRev { get; set; }
public string CompProductRevId { get; set; }
public virtual ProductRev CompProductRev { get; set; }
public int CompTypeValue { get; set; }
public ProductRevCompType CompType
{
get { return (ProductRevCompType)CompTypeValue; }
set { CompTypeValue = (int)value; }
}
public enum ProductRevCompType { ProgrammedPart = 1 };
public ProductRevComp() { }
public override string ToString()
{
return base.ToString();
}
}
Removing the extra prog parts collection doesn't change anything.
How can I get the ProductRev entity to populate the Comps property without resorting to a manual DB hit?
(Must run as the office is closing and I don't have a key - I hope I have included all details, please comment if anything is missing.)

EF DbContext update entity collection

This is how my domain looks:
public abstract class Field
{
//PK
public int FieldId { get; set; }
//FK
[ForeignKey("FieldConfig")]
public int FieldConfigId { get; set; }
//Relations
public virtual FieldConfig FieldConfig { get; set; }
}
public class FieldConfig
{
//PK
public int FieldConfigId { get; set; }
//Columns
public string Name { get; set; }
//FK
[ForeignKey("LabelType")]
public int? LabelTypeId { get; set; }
//Relations
public virtual LabelType LabelType { get; set; }
public virtual ICollection<Field> Fields { get; set; }
}
What I want to do is the following:
Updata a FieldConfig its Fields, this is How I try to do it:
FieldConfig fc = labelTypeRepo.FindById(fieldConfig.LabelTypeId).FieldConfigs.SingleOrDefault(f => f.FieldConfigId == fieldConfig.FieldConfigId);
fc.Name = fieldConfig.Name;
fc.Fields = NewFields;
labelTypeRepo.SaveChanges();
when I do this it adds in my database 3 new fields instead of the normal ids so it looks like this:
1 - oldfield
2 - oldfield
3 - oldfield
4 - newfield
5 - newfield
6 - newfield
then I tried first deleting the old elements like this:
foreach (Field f in fc.Fields.ToList())
((IObjectContextAdapter)context).ObjectContext.DeleteObject(f);
that worked but when I added the new Fields they got an other Id obviously...
So my question is how can I update this collection without adding new fields while maintaining the Id's? (not that i'm using DbContext as context)
I already checked that I'm always using the same context.
Thx for the help..
Based on what you provided here are my toughts:
fc.Fields = NewFields;
I can't see these NewFields collection anywhere but I suspect they are not a modified version of the current fc.Fields. If the FieldId is auto generated, when you provide new values with the same id's as the old ones EF will not modify them but will insert new values instead.
Here's what I think you want to do:
FieldConfig fc = ...;
fc.Name = fieldConfig.Name;
foreach(var field in fc.Fields)
{
/*Modify each field properties with the corresponding properties of Newfields, e.g.:
var newField = NewFields.First(nf => nf.FieldId == field.FieldId);
field.FieldConfig = newField.FieldConfig;
*/
}
labelTypeRepo.SaveChanges();
Hope it helps.

Categories