Cant' get EF6 to load related entities - c#

I am creating a web application in C# using VS2012 to track contact attempts made to customers. I am saving the contact attempt, with 2 ref tables for contact attempt type, and contact attempt outcome, since these will always be fixed values. The problem I'm having is when I retrieve the App_ContactAttempt from the DB, it will bring back the App_ContactAttempt entity without the attached Ref_ContactOutcome and Ref_ContactType entities. I have lazy loading enabled and proxy creation enabled in the context file, and all ref table properties are set to virtual. But when I get an App_ContactAttempt from the db, there are not ref tables attached. Anyone got any ideas what I can do? If you need more information I can provide it.
UPDATE
Right, I have a service setup to get the App_ContactAttempt, which looks like this:
public App_ContactAttempt GetContactAttempt(int contactAttemptId)
{
using (var logger = new MethodLogger(contactAttemptId))
{
var contactAttempt = new App_ContactAttempt();
try
{
contactAttempt = _unitOfWork.ContactAttempts.Get(contactAttemptId);
}
catch (Exception e)
{
logger.LogException(e.InnerException);
}
return contactAttempt;
}
}
When I use this service, I get back App_ContactAttempt when I call the service, but Ref_ContactType and Ref_ContactOutcome are null. But when I call to the db from within the controller using the db context like so:
var db = new ParsDatabaseContext();
var contactAttemptTest1 = _clientService.GetContactAttempt(contactAttempt.ContactAttemptId);
var contactAttemptTest2 = db.App_ContactAttempt.Where(x => x.ContactAttemptId == contactAttempt.ContactAttemptId);
The contactAttemptTest1 returns the App_ContactAttempt with Ref_ContactType and Ref_ContactOutcome both being null. However, contactAttemptTest2 returns App_ContactAttempt with Ref_ContactType and Ref_ContactOutcome both being populated. Hope this helps narrow down my issue, because I haven't a clue..
UPDATE 2
Here are the context and classes if they help at all:
Context.cs
public partial class ParsDatabaseContext : DbContext
{
public ParsDatabaseContext()
: base("name=ParsDatabaseContext")
{
this.Configuration.LazyLoadingEnabled = true;
this.Configuration.ProxyCreationEnabled = true;
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
public DbSet<App_Client> App_Client { get; set; }
public DbSet<App_ContactAttempt> App_ContactAttempt { get; set; }
public DbSet<Ref_ContactOutcome> Ref_ContactOutcome { get; set; }
public DbSet<Ref_ContactType> Ref_ContactType { get; set; }
public virtual ObjectResult<GetClient_Result> GetClient(Nullable<int> clientID)
{
var clientIDParameter = clientID.HasValue ?
new ObjectParameter("ClientID", clientID) :
new ObjectParameter("ClientID", typeof(int));
return ((IObjectContextAdapter)this).ObjectContext.ExecuteFunction<GetClient_Result>("GetClient", clientIDParameter);
}
}
App_ContactAttempt.cs
public partial class App_ContactAttempt
{
public int ContactAttemptId { get; set; }
public int ClientId { get; set; }
public Nullable<System.DateTime> ContactDate { get; set; }
public Nullable<int> ContactType { get; set; }
public Nullable<int> ContactOutcome { get; set; }
public string Notes { get; set; }
public virtual Ref_ContactOutcome Ref_ContactOutcome { get; set; }
public virtual Ref_ContactType Ref_ContactType { get; set; }
}
Ref_ContactOutcome.cs
public partial class Ref_ContactOutcome
{
public Ref_ContactOutcome()
{
this.App_ContactAttempt = new HashSet<App_ContactAttempt>();
}
public int ContactOutcomeId { get; set; }
public string Description { get; set; }
public virtual ICollection<App_ContactAttempt> App_ContactAttempt { get; set; }
}
Ref_ContactType.cs
public partial class Ref_ContactType
{
public Ref_ContactType()
{
this.App_ContactAttempt = new HashSet<App_ContactAttempt>();
}
public int ContactTypeId { get; set; }
public string Description { get; set; }
public virtual ICollection<App_ContactAttempt> App_ContactAttempt { get; set; }
}

The problem is that lazy loading works only if the DBContext used to create the proxy class is available. In your case is the proxy detached because the DBContext used to crate the proxy object contactAttempt of type App_ContactAttempt has already been disposed.
Also make sure that:
dbContext.Configuration.ProxyCreationEnabled = true;
And you can check if the object is proxy
public static bool IsProxy(object type)
{
return type != null && ObjectContext.GetObjectType(type.GetType()) != type.GetType();
}
https://msdn.microsoft.com/en-us/library/ee835846%28v=vs.100%29.aspx
See this answer to check if your proxy entity is attached to a DBContext.
You can attach existing detached entity to another existing context and make it lazy-loading again:
db.App_ContactAttempts.Attach(contactAttemptTest1);
If you have an entity that you know already exists in the database but
which is not currently being tracked by the context then you can tell
the context to track the entity using the Attach method on DbSet. The
entity will be in the Unchanged state in the context.
See here.
So in your example:
using (var db = new ParsDatabaseContext())
{
var contactAttemptTest1 = _clientService.GetContactAttempt(contactAttempt.ContactAttemptId);
db.App_ContactAttempts.Attach(contactAttemptTest1);
Debug.Print(contactAttemptTest1.Ref_ContactType.Description);
}
should work.

Use includes.
For example:
var contactAttemps = db.App_ContactAttempts
.Includes("Ref_ContactOutcome")
.Includes("Ref_ContactTypes")
.ToList();

Are you returning the entity itself or a DTO (data transfer object)?
if you are returning a DTO, ensure the mapping is properly done.
Post your entity object.

Related

Pass complex object made of complex objects with cross references through POST form and EF into database

I have basic object models with cross references
//Model in which I pass and gather data from view
public class ItemModel
{
public BasicItem BasicItem;
public FoodItem FoodItem;
public LocalItem LocalItem;
public ItemModel()
{
BasicItem = new BasicItem();
FoodItem = new FoodItem();
LocalItem = new LocalItem();
}
}
//And classes represents EF entities
public class BasicItem
{
...//Multiple basic fields: int, string
//EF references for PK-FK connection
public FoodItem FoodItem { get; set; }
public LocalItem LocalItem { get; set; }
}
public class LocalItem
{
...//Multiple basic fields: int, string
//EF reference for PK-FK connection
public BasicItem BasicItem { get; set; }
}
public class FoodItem
{
...//Multiple basic fields: int, string
//EF reference for PK-FK connection
public BasicItem BasicItem { get; set; }
}
And my view in basics seems like this
#model ItemModel
...
<input required asp-for="BasicItem.Price" type="number" name="Price">
...
<input asp-for="FoodItem.Weight" type="number" name="Weight">
...
As now I connect it (so different entities have relation each to other) like this:
public async Task<IActionResult> ProductAdd(ItemModel ItemModel)
{
if (ItemModel.BasicItem != null)
{
if (ItemModel.LocalItem != null)
{
ItemModel.BasicItem.LocalItem = ItemModel.LocalItem;
ItemModel.LocalItem.BasicItem = ItemModel.BasicItem;
await db.LocalItems.AddAsync(ItemModel.LocalItem);
}
//same for FoodItem
await db.BasicItems.AddAsync(ItemModel.BasicItem);
await db.SaveChangesAsync();
}
}
But data from form dosent bind to my ItemModel, so my code fails at point when it trying to Add new entity to db, but it has null fields(which null by default, but setuped in form).
Is there any way I can help bind this model to data Im entering?
As other way I can only see this: create plain model which will have all fields from Basic, Local and Food items and bind it in my action. But it will hurt a much, if I ever wanted to change one of this classes.
For you scenario , BasicItem has a one-to-one relationship with LocalItem and FootItem.When adding data into the database , you need to pay attention to that if the foreign key is nullable or exists and the order in which data is added to the primary table and child table .
Here is a working demo ,you could refer to :
Model definition
public class BasicItem
{
public int BasicItemID { get; set; }
public string Name { get; set; }
public int FoodItemID { get; set; }
[ForeignKey("FoodItemID")]
public FoodItem FoodItem { get; set; }
public int LocalItemID { get; set; }
[ForeignKey("LocalItemID")]
public LocalItem LocalItem { get; set; }
}
public class FoodItem
{
public int FoodItemID { get; set; }
public string Name { get; set; }
//public int BasicItemID { get; set; }
public BasicItem BasicItem { get; set; }
}
public class LocalItem
{
public int LocalItemID { get; set; }
public string Name { get; set; }
//public int BasicItemID { get; set; }
public BasicItem BasicItem { get; set; }
}
public class ItemModel
{
public BasicItem BasicItem;
public FoodItem FoodItem;
public LocalItem LocalItem;
public ItemModel()
{
BasicItem = new BasicItem();
FoodItem = new FoodItem();
LocalItem = new LocalItem();
}
}
Controller
public async Task<IActionResult> ProductAdd(ItemModel ItemModel)
{
if (ItemModel.BasicItem != null)
{
if (ItemModel.LocalItem != null)
{
await db.LocalItems.AddAsync(ItemModel.LocalItem);
await db.FoodItems.AddAsync(ItemModel.FoodItem);
}
//same for FoodItem
ItemModel.BasicItem.LocalItem = ItemModel.LocalItem;
ItemModel.BasicItem.FoodItem = ItemModel.FoodItem;
await db.BasicItems.AddAsync(ItemModel.BasicItem);
await db.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(ItemModel);
}
Okay, we can divide my situation into 2 basic cases:
Creating new entities
Updating entities
In first case it's pretty simple and easy cause you can create new object, fill it up, setup relations (you can only setup relation in one object like basicItem.FoodItem = foodItem, you don't need to do foodItem.BasicItem = basicItem, cause EF will automatically connect them) and send it to db, and it will work.
In second case, it's a little more complicated, cause in case to update data in db, you must get a related entity to a context. It's brings it's own limitations. And again you can have two approaches:
Create new object and manually (or through auto-mapper, but I didn't dig into this) overwrite fields of db related object at the end.
Fetch object from db at the beginning, pass it it through actions and change data on fly (if you want/need, you can even update db record on fly).
They are quite the same in a way, that you need to choose what field to update and write some code dbFoodItem.Weight = userInput.Weight.
So in my case I took second approach, and cause I collected data in multiple actions, I used session to data storage object between them.

Foreign key reference object is returning null

I am referencing the AbpUser table in an entity with the following property definition:
[ForeignKey("LocationManagerId")]
public virtual User LocationManager { get; set; }
public virtual long LocationManagerId { get; protected set; }
My DTO is as follows:
[AutoMapFrom(typeof(Location))]
public class LocationDto : FullAuditedEntityDto<Guid>
{
// <snip>
public virtual User LocationManager { get; set; }
}
Using the AsyncCrudAppService and calling the following from an MVC controller:
var locations = (await _locationAppService.GetAll(new PagedAndSortedResultRequestDto())).Items;
locations.LocationManager is returning null. I've confirmed everything in both the entity and the DTO is set to virtual. I'm at a loss. Anyone have any insight?
If you are using EntityFrameworkCore, you have to use eager-loading.
Override this method in LocationAppService:
protected override IQueryable<Location> CreateFilteredQuery(LocationGetAllInput input)
{
return Repository.GetAllIncluding(x => x.LocationManager);
}

Retrieving parent/child records (object containing list) from Azure Mobile Services using EF

I have .net 4.5.2 test app playing about with Azure Mobile Services and I'm attempting to store data using the TableController. I have my data types as follows:
public class Run:EntityData
{
public int RunId { get; set; }
public DateTime? ActivityStarted { get; set; }
public DateTime? ActivityCompleted { get; set; }
public List<Lap> LapInformation { get; set; }
public Run()
{
LapInformation = new List<Lap>();
}
}
public class Lap
{
[Key]
public int LapNumber { get; set; }
public int CaloriesBurnt { get; set; }
public double Distance {get; set;}
//Some other basic fields in here
public DateTime? LapActivityStarted { get; set; }
public DateTime? LapActivityCompleted { get; set; }
public Lap()
{
}
In my Startup class I call:
HttpConfiguration config = new HttpConfiguration();
new MobileAppConfiguration()
.UseDefaultConfiguration()
.ApplyTo(config);
And in my MobileServiceContext class:
public class MobileServiceContext : DbContext
{
private const string connectionStringName = "Name=MS_TableConnectionString2";
public MobileServiceContext() : base(connectionStringName)
{
}
public DbSet<Run> Runs { get; set; }
public DbSet<Lap> Laps { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Add(
new AttributeToColumnAnnotationConvention<TableColumnAttribute, string>(
"ServiceTableColumn", (property, attributes) => attributes.Single().ColumnType.ToString()));
}
}
In my controller then, I have:
[MobileAppController]
public class RunController: TableController<Run>
{
protected override void Initialize(HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
MobileServiceContext context = new MobileServiceContext();
DomainManager = new EntityDomainManager<Run>(context, Request);
}
public IList<Run> GetAllRuns()
{
var runs = context.Runs.Include("LapInformation").ToList();
return runs;
}
public SingleResult<Run> GetRun(string id)
{
return Lookup(id);
}
public async Task<IHttpActionResult> PostRun(Run run)
{
Run current = await InsertAsync(run);
return CreatedAtRoute("Tables", new { id = current.Id }, current);
}
public Task DeleteRun(string id)
{
return DeleteAsync(id);
}
}
I can then POST a record in fiddler which responds with a 201 and the Location of the newly created Item. An Example of the data I'm posting is:
{RunId: 1234, LapInformation:[{LapNumber:1,Distance:0.8, LapActivityStarted: "2017-06-19T00:00:00", LapActivityCompleted: "2017-06-19T00:00:00", CaloriesBurnt: 12}]}
However, when I GET that object, I'm only getting the fields from Run, without the list of Detail records (Lap). Is there anything I have to configure in Entity Framework so that when I GET a Run record from the DB, it also gets and deserializes all associated detail records?
Hopefully that makes sense.
EDIT
Turns out that it is pulling back all the lap information, but when I return it to the client, that information is getting lost.
You can use custom EF query with Include() method instead of Lookup call preferably overload that takes function from System.Data.Entity namespace.
var runs = context.Runs.Include(r => r.LapInformation)
Take a look at https://msdn.microsoft.com/en-us/library/jj574232(v=vs.113).aspx
AFAIK, you could also use the $expand parameter to expand your collections as follows:
GET /tables/Run$expand=LapInformation
Here is my sample, you could refer to it:
You could mark your action with a custom ActionFilterAttribute for automatically adding the $expand property to your query request as follows:
// GET tables/TodoItem
[ExpandProperty("Tags")]
public IQueryable<TodoItem> GetAllTodoItems()
{
return Query();
}
For more details, you could refer to adrian hall's book chapter3 relationships.
EDIT Turns out that it is pulling back all the lap information, but when I return it to the client, that information is getting lost.
I defined the following models in my mobile client:
public class TodoItem
{
public string Id { get; set; }
public string UserId { get; set; }
public string Text { get; set; }
public List<Tag> Tags { get; set; }
}
public class Tag
{
public string Id { get; set; }
public string TagName { get; set; }
}
After execute the following pull operation, I could retrieve the tags as follows:
await todoTable.PullAsync("todoItems", todoTable.CreateQuery());
Note: The Tags data is read-only, you could only update the information in the ToDoItem table.
Additionally, as adrian hall mentioned in Data Access and Offline Sync - The Domain Manager:
I prefer handling tables individually and handling relationship management on the mobile client manually. This causes more code on the mobile client but makes the server much simpler by avoiding most of the complexity of relationships.

DbSet.Add() not working

I have a class like this:
[Table("member_activation")]
public partial class MemberActivation
{
[Key]
public Int64 member_id { get; set; }
public String token { get; set; }
}
My db:
public class SMADbContext : DbContext
{
public SMADbContext() : base("SMADB")
{
Database.SetInitializer<SMADbContext>(new NullDatabaseInitializer<SMADbContext>());
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
public DbSet<Member> Members { get; set; }
public DbSet<MemberActivation> MemberActivations { get; set; }
public DbSet<ApiAccount> ApiAccounts { get; set; }
public DbSet<ApiHardware> ApiHardwares { get; set; }
public DbSet<MemberRelation> MemberRelations { get; set; }
}
In my controller:
[Route("tester")]
[AllowAnonymous]
public IHttpActionResult tester()
{
using (var db = new SMADbContext())
{
var memberActivation = new MemberActivation();
memberActivation.member_id = 10155;
memberActivation.token = "hello";
db.MemberActivations.Add(memberActivation);
return Json(new { dbset = db.MemberActivations.ToList(), memberAct = memberActivation });
}
}
db.MemberActivations.Add(memberActivation); does not work. When I return the json, the dbset does not include the newly created memberActivation. I do not have db.SaveChanges() because it will not save until the memberActivation is pushed to the dbset
You cant set member_id, it is the key and ef uses it as identity. It will be ignored. You can configure ef so that member_id is not identity but that's another topic.
db.MembershipActivations.Add( new MemberActivation { token = "hello"}):
db.SaveChanges();
should work fine.
if however , as it would appear , you have an existing member and you are trying to set a relationship with that entity via a join table. Then you should retrieve that entity and set the memberactivation. Ef will sort the rest out for you. Bit of guessing here as i would need to see the involved entities.

EF adding duplicate records into lookup/reference table

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);

Categories

Resources