I have a pair of ViewModels that references data from a number of tables. One for displaying and one for editing.
When I return data from the display ViewModel I can map all the relevant fields using ValueInjecter InjectFrom functionality.
What do I do next to get the database to update?
If I send the models to my Update method in the repository I can see the changes in the model but the context doesn't pick them up. Am I missing a step or is there a better way of doing this?
If I try to modify one table at a time I can get the context to pick up the changes but then get an error as follows:
Store update, insert, or delete statement affected an unexpected
number of rows (0).
---EDIT---
I've updated the code and moved the mapping into the repository but I'm still getting the same error even though the debugger shows the entities with the new values.
ViewModels
public partial class HouseholdEditViewModel //for displaying in browser
{
public int entityID { get; set; }
public int familyID { get; set; }
public string UPRN { get; set; }
public string address { get; set; }
public HousingTypeDropDownViewModel housingTypeID { get; set; }
public KeyworkerDropDownViewModel keyworkerID { get; set; }
public string startDate { get; set; }
public bool loneParent { get; set; }
public string familyPhoneCode { get; set; }
public string familyPhone { get; set; }
}
public partial class HouseholdAddViewModel //for mapping to database
{
public int familyID { get; set; }
public string UPRN { get; set; }
public string address { get; set; }
public int entityTypeID { get; set; }
public int housingTypeID { get; set; }
public int keyworkerID { get; set; }
public DateTime startDate { get; set; }
public bool loneParent { get; set; }
public string familyPhoneCode { get; set; }
public string familyPhone { get; set; }
}
Repository (Current version - I've attempted a few different things without success)
public interface IHouseholdRepository : IDisposable
{
//other methods here...
void Update(HouseholdAddViewModel model, int id);
}
public void Update(HouseholdAddViewModel model, int id)
{
//check address exists
var address = (from u in context.tAddress
where model.UPRN.Contains(u.UPRN)
select u.UPRN);
var ae = new tAddressEntity();
ae.InjectFrom(model);
ae.entityID = id;
ae.UPRN = model.UPRN;
context.tAddressEntity.Attach(ae);
context.Entry(ae).State = EntityState.Modified;
var e = new tEntity();
e.InjectFrom(model);
e.entityID = id;
e.entityName = model.address;
e.tAddressEntity.Add(ae);
context.tEntity.Attach(e);
context.Entry(e).State = EntityState.Modified;
var a = new tAddress();
a.InjectFrom(model);
context.tAddress.Attach(a);
context.Entry(a).State = address.ToString() == string.Empty ?
EntityState.Added :
EntityState.Modified;
var hs = new tHousingStatus();
hs.InjectFrom(model);
hs.entityID = id;
context.tHousingStatus.Attach(hs);
context.Entry(hs).State = EntityState.Modified;
var k = new tKeyWorker();
k.InjectFrom(model);
k.entityID = id;
context.tKeyWorker.Attach(k);
context.Entry(k).State = EntityState.Modified;
var l = new tLoneParent();
l.InjectFrom(model);
l.entityID = id;
context.tLoneParent.Attach(l);
context.Entry(l).State = EntityState.Modified;
var h = new tHousehold();
h.InjectFrom(model);
h.entityID = id;
h.tHousingStatus.Add(hs);
h.tKeyWorker.Add(k);
h.tLoneParent.Add(l);
context.Entry(h).State = EntityState.Modified;
context.SaveChanges();
}
Controller
[HttpPost]
public ActionResult Edit(HouseholdAddViewModel model, int id)
{
model.entityTypeID = _repo.GetEntityType();
if (ModelState.IsValid)
{
_repo.Update(model, id);
return RedirectToAction("Index");
}
return View("Edit", id);
}
The easiest way to update an entity using EF is to retrieve the entity (using
it's key) and then apply the updates to that object instance. EF will automatically detect the updates to the entity and apply them when you call SaveChanges().
It seems as if you're creating new entities and you're not adding them to context so they
aren't being picked up.
I would change your Edit controller to do this
[HttpPost]
public ActionResult Edit(HouseholdAddViewModel model, int id)
{
model.entityTypeID = _repo.GetEntityType();
if (ModelState.IsValid)
{
var h = _repo.GetHousehold(id);
h.InjectFrom(model);
h.entityID = id;
//...
}
}
Related
eWhy is my Icollection foreign key always blank I have a foreign table called photos which I have created using the Icollection. Im using ef core 3.1.7 and asp.net core 3.1 how does one get the file attachments VesselsId not to be null
Basically one vessel can have many photos but their could also be many vessels.
public class Vessels {
public int Id { get; set; }
[StringLength(400)]
public string Name { get; set; }
public string FlagName { get; set; }
public ICollection<FileAttachments> PhotosAttachments { get; set; }
}
This is the file attachments
public class FileAttachments {
public int Id { get; set; }
public string Title { get; set; }
public string File { set; get; }
}
In where I Wish to display the photos their blank I use the include statement to try and include them in my query.
var vessels = await _context.Vessels.Where(w=>w.Id==id).Include(c=>c.PhotosAttachments).FirstAsync();
But If I look here it will show PhotosAttachments being of null when I look at the field value sure enough its sitting there null.
I think i need to do something here but im not sure as to what
protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
}
Edit 2
Basically i have a generic Upload Files method as such which is called via the submit button
[HttpPost]
public async Task<IActionResult> UploadFiles(List<IFormFile> FormFile, int UploadArea, int PoiId, int VesselId) {
FileAttachments attachments = new FileAttachments {
DocumentPath = filePath,
UploadAreaId = UploadArea,
CaseId = resultCaseId,
FullPath = savedFileName,
FileSize = infoFile.Length,
OrignalFileName = fileAttachments.FileName,
FileAttachmentType = fileAttachmentType,
TennantId = await GetCurrentTennantId(),
Extension = infoFile.Extension.Replace(".", "").ToLower(),
UploadedBy = caseOfficer.Id,
CreatedDate = DateTime.Now,
File = uniqueFilename,
ContentType = fileAttachments.ContentType,
isActive = true,
isDeleted = false
};
if (PoiId != 0) {
attachments.PoiID= PoiId;
}
if (VesselId != 0) {
attachments.VesselId = VesselId;
}
_context.Add(attachments);
await _context.SaveChangesAsync();
}
There is some confusion above i am using to store something else the
collection does not create this field it creates VesselsId with the
extra s this is what is not being filled in.
public int? VesselId { get; set; }
The collection creates this field
Add relation to FileAttachments model like this
public class FileAttachments {
...
[ForeignKey("Vessels")]
public int? VesselId { get; set; }
public Vessels Vessels { get; set; }
}
This is how database structure looks like:
Vehicle has lot of CanNetworks and each CanNetwork has lot of ECUs. And that would save perfectly if that was only I have.
But, each vehicle also has one special ECU called gatewayECU so problem happens with saving because entity framework core does not know how to handle that scenario. It needs to insert vehicle before inserting ecus, but how to insert vehicle when ecu is not inserted.
This is what I tried: ignore (delete, invalidate) gatewayecu field (column is nullable in database), then I insert whole graph and then update vehicle with gatewayEcuId field I stored in some variable before doing anything.
Solution is not pretty. How to handle this scenario.
public class Vehicle : BaseEntity
{
public Vehicle()
{
CANNetworks = new List<CANNetwork>();
}
public List<CANNetwork>? CANNetworks { get; set; }
public ECU? GatewayECU { get; set; } = default!;
public int? GatewayECUId { get; set; }
}
public class CANNetwork : BaseEntity
{
public CANNetwork()
{
ECUs = new List<ECU>();
}
public string Name { get; set; } = default!;
public ICollection<ECU>? ECUs { get; set; }
public int VehicleId { get; set; }
public Vehicle? Vehicle { get; set; } = default!;
}
public class ECU : BaseEntity
{
public int CANNetworkId { get; set; }
public CANNetwork? CANNetwork { get; set; } = default!;
}
This is ugly solution which I don't want:
public async Task<int> Insert(Vehicle vehicleDefinition, ECU vehicleGatewayECU)
{
var result = -1;
using (var transaction = _databaseContext.Database.BeginTransaction())
{
result = await Insert(vehicleDefinition);
if (vehicleGatewayECU != null)
{
var ecu = await _databaseContext.ECUs.FirstOrDefaultAsync(x => x.Name == vehicleGatewayECU.Name && vehicleDefinition.Name == x.CANNetwork.Vehicle.Name);
if (ecu != null)
{
vehicleDefinition.GatewayECUId = ecu.Id;
result = await Update(vehicleDefinition);
transaction.Commit();
return result;
}
}
else
{
transaction.Commit();
}
}
return result;
}
EDIT:
I am now thinking about changing table structure in a way to get rid of gatewayECU field on Vehicle, and put some flag IsGatewayEcu in ECU table
I have the following entities:
namespace SomeDataAccess
{
public partial class Patch
{
public int PatchID { get; set; }
public double Number { get; set; }
}
public partial class PatchFile
{
public int FileID { get; set; }
public int PatchID{ get; set; }
public string Name { get; set; }
public string Type { get; set; }
}
}
And I have the following api model:
namespace Web_API.Models
{
[Table("SomeFiles")]
public class SomeFilesViewModel
{
[Key]
public int FileId { get; set; }
public int PatchNumber{ get; set; }
public string Name { get; set; }
public string Type { get; set; }
}
}
The GET method is implemented successfully as following:
/ GET: api/SomeFiles/5
[ResponseType(typeof(SomeFileViewModel))]
public async Task<IHttpActionResult> GetSomeFileViewModel(int id)
{
var patchFile = await _context.PatchFile.FindAsync(id);
return someFile == null
? (IHttpActionResult)NotFound()
: Ok(new someFileViewModel
{
FileId = patchFile.FileID,
PatchNumber = patch.Number,
Name = patchFile.Name,
Type = patchFile.Type,
});
}
Thus far, I have implemented the PUT method as following:
// PUT: api/SomeFiles
[ResponseType(typeof(void))]
public async Task<IHttpActionResult> PutSomeFileViewModel(SomeFilesViewModel someFileViewModel)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);
var file = new SomeDataAccess.PatchFile
{
FileID = someFileViewModel.FileId,
PatchID = _context.Patch.FirstOrDefault(i => i.Number == someFileViewModel.PatchNumber).PatchID
// How to get the relavent patch id by having the patch Number?
Name = someFileViewModel.Name,
Type = someFileViewModel.Type
};
_context.Entry(file).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!FileExists(file.FileID))
return NotFound();
throw;
}
return StatusCode(HttpStatusCode.NoContent);
}
And a sample payload:
Sample PayLoad:
{
"FileId" = 4
"PatchNumber" = 894
"Name" = "MyFile.exe"
Type = "Application"
}
How can I update or add a record to PatchFile entity if I only have the PatchNumber and not the PatchId to prevent conflicted with the FOREIGN KEY constraint?
_context.Patch.FirstOrDefault(i => i.Number == someFileViewModel.PatchNumber).PatchID
Is above the correct approach? If yes, Is this not making another trip to database? Is there a better approach?
You could add the PatchID to the SomeFilesViewModel along the PatchNumber. Otherwise there will be this extra query to the DB. On the other hand: this might create another possible problem, as the sent data don't have to be accurate and you'll need to check/validate it and that would be another trip to DB.
If you decide to stick with the extra query I would suggest rewriting it as following:
_context.Patch.Where(i => i.Number == someFileViewModel.PatchNumber).Select(i => i.PatchID).FirstOrDefault();
That way you get only the ID from the DB; assuming you don't need to work with other parts of your Patch object.
I want to Find Username by userId
this code snippet working
Discussion_CreateBy = db.AspNetUsers.Find(discussion.CreatedBy).UserName,
and this once not working in following controller class
Comment_CreateBy = db.AspNetUsers.Find(c.CreatedBy).UserName,
this is my model classes
public class DiscussionVM
{
public int Disussion_ID { get; set; }
public string Discussion_Title { get; set; }
public string Discussion_Description { get; set; }
public Nullable<System.DateTime> Discussion_CreateDate { get; set; }
public string Discussion_CreateBy { get; set; }
public string Comment_User { get; set; }
public IEnumerable<CommentVM> Comments { get; set; }
}
public class CommentVM
{
public int Comment_ID { get; set; }
public Nullable<System.DateTime> Comment_CreateDate { get; set; }
public string Comment_CreateBy { get; set; }
public string Comment_Description { get; set; }
}
this is whole controller class
public ActionResult Discussion_Preview()
{
int Discussion_ID = 1;
var discussion = db.AB_Discussion.Where(d => d.Discussion_ID == Discussion_ID).FirstOrDefault();
var comments = db.AB_DiscussionComments.Where(c => c.Discussion_ID == Discussion_ID);
DiscussionVM model = new DiscussionVM()
{
Disussion_ID = discussion.Discussion_ID,
Discussion_Title = discussion.Discussion_Name,
Discussion_Description = discussion.Discussion_Name,
Discussion_CreateBy = db.AspNetUsers.Find(discussion.CreatedBy).UserName,
Discussion_CreateDate = discussion.CreatedDate,
Comments = comments.Select(c => new CommentVM()
{
Comment_ID = c.Comment_ID,
Comment_Description = c.Comment_Discription,
Comment_CreateBy = db.AspNetUsers.Find(c.CreatedBy).UserName,
Comment_CreateDate = c.CreatedDate
})
};
return View(model);
}
Getting following error
Method 'Project.Models.AspNetUser Find(System.Object[])' declared on type 'System.Data.Entity.DbSet1[Project.Models.AspNetUser]' cannot be called with instance of type 'System.Data.Entity.Core.Objects.ObjectQuery1[Project.Models.AspNetUser]'
Discussion_CreateBy = db.AspNetUsers.Find(discussion.CreatedBy).UserName
Works because discussion is an in-memory object because you are executing a query by calling FirstOrDefault on it:
var discussion = db.AB_Discussion.Where(d => d.Discussion_ID == Discussion_ID).FirstOrDefault();
On the other hand in the following statement:
db.AspNetUsers.Find(c.CreatedBy).UserName
c is not queried yet because
db.AB_DiscussionComments.Where(c => c.Discussion_ID == Discussion_ID)
returns an IQueriable and not the actual collection of comments
The easiest way to fix it is to bring all your comments into memory (since you are anyway need them all) :
var comments = db.AB_DiscussionComments.Where(c => c.Discussion_ID == Discussion_ID).ToList();
I have 3 tables,
1. AttributeTypes (Columns: AttributeId (PK), AttributeName, ..)
2. Location (Columns: locationId (PK), LocationName, ...)
3. LocationAttributeType (Columns: locationId (FK), AttributeId (FK))
Whenever I am trying to insert new location record along with its attribute type from GUI, it should create new record for Table- Location and LocationAttributeType. But EF trying to add new record in Table- AttributeTypes as well, which is just used as reference table and should not add new/duplicate records in it. How can I prevent that?
here is my code,
The model which GUI sends is,
public class LocationDataModel
{
[DataMember]
public int Id { get; set; }
[DataMember]
public string Code { get; set; }
[DataMember]
public List<AttributeTypeDataModel> AssignedAttributes = new List<AttributeTypeDataModel>();
}
public class AttributeTypeDataModel
{
protected AttributeTypeDataModel() {}
public AttributeTypeDataModel(int id) { this.Id = id; }
public AttributeTypeDataModel(int id, string name)
: this(id)
{
this.Name = name;
}
[DataMember]
public int Id { get; set; }
[DataMember]
public string Name { get; set; }
[DataMember]
public virtual ICollection<LocationDataModel> Locations { get; set; }
}
The Entities created by EF are,
public partial class Location
{
public Location()
{
this.AttributeTypes = new List<AttributeType>();
}
public Location(int campusId, string code)
: this()
{
CampusId = campusId; Code = code;
}
public int Id { get; set; }
public int CampusId { get; set; }
public string Code { get; set; }
public virtual ICollection<AttributeType> AttributeTypes { get; set; }
}
public partial class AttributeType
{
public AttributeType()
{
this.Locations = new List<Location>();
}
public int AttributeTypeId { get; set; }
public string AttributeTypeName { get; set; }
public virtual ICollection<Location> Locations { get; set; }
}
I have below code to Add these new location to database,
private IEnumerable<TEntity> AddEntities<TModel, TEntity, TIdentityType>
(IEnumerable<TModel> models, Func<TModel, TIdentityType> primaryKey,
IGenericRepository<TEntity, TIdentityType> repository)
{
var results = new List<TEntity>();
foreach (var model in models)
{
var merged = _mapper.Map<TModel, TEntity>(model);
var entity = repository.Upsert(merged);
results.Add(entity);
}
repository.Save();
return results.AsEnumerable();
}
I am using following generic repository to do entity related operations
public TEntity Upsert(TEntity entity)
{
if (Equals(PrimaryKey.Invoke(entity), default(TId)))
{
// New entity
return Context.Set<TEntity>().Add(entity);
}
else
{
// Existing entity
Context.Entry(entity).State = EntityState.Modified;
return entity;
}
}
public void Save()
{
Context.SaveChanges();
}
Whats wrong I am doing here?
The DbSet<T>.Add() method attaches an entire object graph as added. You need to indicate to EF that the 'reference' entity is actually already present. There are two easy ways to do this:
Don't set the navigation property to an object. Instead, just set the corresponding foreign key property to the right value.
You need to ensure that you don't load multiple instances of the same entity into your object context. After creating the context, load the full list of AttributeType entities into the context and create a Dictionary<> to store them. When you want to add an attribute to a Location retrieve the appropriate attribute from the dictionary. Before calling SaveChanges() iterate through the dictionary and mark each AttributeType as unchanged. Something like this:
using (MyContext c = new MyContext())
{
c.AttributeTypes.Add(new AttributeType { AttributeTypeName = "Fish", AttributeTypeId = 1 });
c.AttributeTypes.Add(new AttributeType { AttributeTypeName = "Face", AttributeTypeId = 2 });
c.SaveChanges();
}
using (MyContext c = new MyContext())
{
Dictionary<int, AttributeType> dictionary = new Dictionary<int, AttributeType>();
foreach (var t in c.AttributeTypes)
{
dictionary[t.AttributeTypeId] = t;
}
Location l1 = new Location(1, "Location1") { AttributeTypes = { dictionary[1], dictionary[2] } };
Location l2 = new Location(2, "Location2") { AttributeTypes = { dictionary[1] } };
// Because the LocationType is already attached to the context, it doesn't get re-added.
c.Locations.Add(l1);
c.Locations.Add(l2);
c.SaveChanges();
}
In this specific case you are using a many-to-many relationship, with EF automatically handling the intermediate table. This means that you don't actually have the FK properties exposed in the model, and my first suggestion above won't work.
Therefore, you either need to use the second suggestion, which still ought to work, or you need to forgo the automatic handling of the intermediate table and instead create an entity for it. This would allow you to apply the first suggestion. You would have the following model:
public partial class Location
{
public Location()
{
this.AttributeTypes = new List<LocationAttribute>();
}
public Location(int campusId, string code)
: this()
{
CampusId = campusId; Code = code;
}
public int Id { get; set; }
public int CampusId { get; set; }
public string Code { get; set; }
public virtual ICollection<LocationAttribute> AttributeTypes { get; set; }
}
public partial class LocationAttribute
{
[ForeignKey("LocationId")]
public Location Location { get; set; }
public int LocationId { get; set; }
public int AttributeTypeId { get; set; }
}
public partial class AttributeType
{
public int AttributeTypeId { get; set; }
public string AttributeTypeName { get; set; }
}
With this approach you do lose functionality since you can't navigate from a Location to an AttributeType without making an intermediate lookup. If you really want to do that, you need to control the entity state explicitly instead. (Doing that is not so straightforward when you want to use a generic repository, which is why I've focused on this approach instead.)
Thank you all for your suggestions.
I have to get rid of my generic repository here to save my context changes and do it manually as below,
private IEnumerable<int> AddLocationEntities(IEnumerable<LocationDataModel> locations)
{
var results = new List<int>();
foreach (LocationDataModel l in locations)
{
var entity = _mapper.Map<LocationDataModel, Location>(l);//you can map manually also
var AttributeCode = l.AssignedAttributes.FirstOrDefault().AttributeTypeId;
using (MyContext c = new MyContext())
{
var attr = c.AttributeTypes.Where(a => a.Id == AttributeTypeId ).ToList();
entity.AttributeTypes = attr;
c.Locations.Add(entity);
c.SaveChanges();
var locid = entity.Id;
results.Add(locid);
}
}
return results;
}
In the else statement of yourUpsert you should add
context.TEntity.Attach(entity);