Storing list of custom objects in Entity Framework - c#

Started to learn asp.net and DB manipulations. Trying to implement some simple functionality - two models, one has list of references to another.
Here is an error that I currently get:
An exception occurred while initializing the database. See the InnerException for details.
Inner exception:
Unable to determine a valid ordering for dependent operations. Dependencies may exist due to foreign key constraints, model requirements, or store-generated values.
My models:
public class Killer
{
public Killer(string name, string biography)
{
Name = name;
Biography = biography;
KillerId = Guid.NewGuid();
}
public Guid KillerId { get; set; }
public string Name { get; set; }
public string Biography { get; set; }
public virtual Contract Contract { get; set; }
}
public class Contract
{
public Contract(Status status, Killer target, string description, params Killer[] targets)
{
ContractId = Guid.NewGuid();
this.status = status;
Target = target;
Description = description;
Killers = new HashSet<Killer>();
foreach (var t in targets) Killers.Add(t);
}
public Guid ContractId { get; set; }
public enum Status { active, done, failed, rejected, abandoned }
public Status status { get; set; }
public Killer Target { get; set; }
public string Description { get; set; }
//[ForeignKey("ContractID")]
public virtual ICollection<Killer> Killers { get; set; }
}
In context I initialize db with lists of objects
public class KillerContext : DbContext
{
public DbSet<Killer> Killers { get; set; }
public DbSet<Contract> Contracts { get; set; }
}
In controller I do:
KillerContext k = new KillerContext();
public ActionResult Index()
{
var contracts = k.Contracts.ToList();
ViewBag.contracts = contracts;
return View();
}
In Global.asax:
Database.SetInitializer(new KillerContextInitialization());
Here is how I enter first data in db:
public sealed class KillerContextInitialization : DropCreateDatabaseAlways<KillerContext>
{
protected override void Seed(KillerContext db)
{
List<Killer> killers = new List<Killer>();
//List<Contract> contracts = new List<Contract>();
killers.Add(new Killer(name: "Ivan Firstein", biography: "He was born in the shadows."));
killers.Add(new Killer(name: "Oleg Gazmanov", biography: "test man"));
db.Contracts.Add(new Contract(
Contract.Status.active,
killers.SingleOrDefault(x => x.Name == "Ivan Firstein"),
"KILL OR BE KILLED. As always with love.",
killers.SingleOrDefault(x => x.Name == "Oleg Gazmanov")
));
db.Killers.AddRange(killers);
base.Seed(db);
}
}

Looks like you need add ForeignKey attribute for killer Model, and store this key in property ContractId:
public class Killer
{
[ForeignKey(nameof(ContractId)] //Name of added property in line below
public Contract Contract { get; set; } //no need "virtual"
public Guid? ContractId { get; set; }
// other properties...
}
public class Contract
{
[ForeignKey("ContractId")] //Name of added property in Killer Model
public virtual ICollection<Killer> Killers { get; set; }
// other code...
}
EDIT
You should do something similar to the Contract.Target property:
[ForeignKey(nameof(TargetId)]
public Killer Target { get; set; }
public Guid TargetId { get; set; }
For enum types you should add attributes like this:
[Column(nameof(status), TypeName = "int")]
public Status status { get; set; }

Find out that problem was in public Killer Target { get; set; }
When i was adding data, that field was considered as NOT NULL, and all what i need to do, is save changes after filling killers, like so:
public sealed class KillerContextInitialization : DropCreateDatabaseAlways<KillerContext>
{
protected override void Seed(KillerContext db)
{
List<Killer> killers = new List<Killer>();
killers.Add(new Killer(name: "Ivan Firstein", biography: "He was born in the shadows."));
killers.Add(new Killer(name: "Oleg Gazmanov", biography: "test man"));
db.SaveChanges(); // - save killers first, then add them to contract
db.Contracts.Add(new Contract(
Contract.Status.active,
killers.SingleOrDefault(x => x.Name == "Ivan Firstein"),
"KILL OR BE KILLED. As always with love.",
killers.SingleOrDefault(x => x.Name == "Oleg Gazmanov")
));
db.Killers.AddRange(killers);
base.Seed(db);
}
}

Related

Microsoft Entity Framework is not saving changes

Im currently trying to implement CRUD functionality with a dbfactory and generics with microsoft EF, but while listing entries is working, making changes to the db is currently not working.
public class AbstractDataModel
{
[Key]
public Guid gid { get; set; }
}
Model
class SalesOrder : AbstractDataModel
{
public int salesOrderID { get; set; }
public int productID { get; set; }
public int customerID { get; set; }
public Guid createdBy { get; set; }
public string dateCreated { get; set; }
public string orderDate { get; set; }
public string orderStatus { get; set; }
public string dateModified { get; set; }
}
A DBCore with some other functionality besides the ones listed here, which are not relevant for the factory
public class DBCore : DbContext
{
public static string connectionString = "myConnectionStringToDb";
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(connectionString);
}
Data Service which calls factory
class SalesOrderService : DBCore
{
public DbSet<SalesOrder> SalesOrders { get; set; }
public OkObjectResult GetAllSalesOrders()
{
DBFactory factory = new DBFactory();
return new OkObjectResult(JsonConvert.SerializeObject(factory.GetAll(SalesOrders)));
}
public OkObjectResult AddSalesOrder(SalesOrder order)
{
order.gid = Guid.NewGuid();
return DBFactory.AddOne(order);
}
public OkObjectResult UpdateSalesOrder(SalesOrder order)
{
return DBFactory.UpdateOne(order);
}
public OkObjectResult DeleteSalesOrder(SalesOrder order)
{
return DBFactory.DeleteOne(order);
}
}
simple CRUD-Factory,
class DBFactory : DBCore
{
public DbSet<UserModel> Users { get; set; }
public DbSet<SalesOrder> SalesOrders { get; set; }
public List<T> GetAll<T>(DbSet<T> dbset) where T : class
{
using (this)
{
return dbset.ToList();
}
}
public static OkObjectResult AddOne<T>(T data)
{
using (DBFactory factory = new DBFactory())
{
factory.Add(data);
factory.SaveChanges();
return new OkObjectResult("Entry was sucessfully added");
}
}
public static OkObjectResult UpdateOne<T>(T data)
{
using (DBFactory factory = new DBFactory())
{
factory.Update(data);
factory.SaveChanges();
return new OkObjectResult("Entry was sucessfully updated");
}
}
public static OkObjectResult DeleteOne<T>(T data)
{
using (DBFactory factory = new DBFactory())
{
factory.Attach(data);
factory.Remove(data);
factory.SaveChanges();
return new OkObjectResult("Entry was sucessfully removed");
}
}
}
Edit: Following the advices i changed the code so it should SaveChanges for the Factory, which also contains the context as a property. But it still doesnt seem to work for all database operations except listing all entries
Editv2: Thanks for the adivces it seems i have solved that problem, but a new one appeared :D
I can now do database operations like deleting entries, but now i cant list the entries anymore because the following error occurs, although the code there didnt really change:
"Executed 'GetAllOrders' (Failed, Id=5fb95793-572a-4545-ac15-76dffaa7a0cf, Duration=74ms)
[2020-10-23T14:33:43.711] System.Private.CoreLib: Exception while executing function: GetAllOrders. Newtonsoft.Json: Self referencing loop detected for property 'Context' with type 'FicoTestApp.Models.SalesOrder'. Path '[0].ChangeTracker'."
try adding
services.AddControllers().AddNewtonsoftJson(x => x.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore);
to your
startup.cs
it should to the job

How to work only with the Entity Framework Scaffolded Models from Database

I have been working with .Net Core Entity Framework database first approach with the Scaffolding technique.
It generated me a couple Models/Classes from my Database Tables, but for now, I will just minimize the issue I am having to this two tables... a relation one to many on the both ChampionID column:
So, after scaffolding/mapping the models with EntityCore Tools it generated the following two classes (and several others that are not relevant):
Champion.cs:
public partial class Champion
{
public Champion()
{
ChampionScreenshot = new HashSet<ChampionScreenshot>();
ChampionUser = new HashSet<ChampionUser>();
ChampionUserRate = new HashSet<ChampionUserRate>();
}
public int ChampionId { get; set; }
public string Name { get; set; }
public string Nickname { get; set; }
public string Description { get; set; }
public string ImagePath { get; set; }
public byte AttackDamageScore { get; set; }
public byte AbilityPowerScore { get; set; }
public byte ResistanceScore { get; set; }
public byte PlayingDifficult { get; set; }
public int PrimaryClassId { get; set; }
public int SecondaryClassId { get; set; }
public ChampionClass PrimaryClass { get; set; }
public ChampionClass SecondaryClass { get; set; }
public ICollection<ChampionScreenshot> ChampionScreenshot { get; set; }
public ICollection<ChampionUser> ChampionUser { get; set; }
public ICollection<ChampionUserRate> ChampionUserRate { get; set; }
}
ChampionScreenshot.cs:
public partial class ChampionScreenshot
{
public int ChampionScreenshotId { get; set; }
public string ImagePath { get; set; }
public int ChampionId { get; set; }
public Champion Champion { get; set; }
}
My doubt is: what is the correct way to retrieve a Champion object with the ChampionScreenshot attribute filled?
For example, this is what I am doing in my Service layer:
public async Task<Champion> GetChampion(int id)
{
Champion champion = await _context.Champion.FirstAsync(m => m.ChampionId == id);
champion.ChampionScreenshot = _context.ChampionScreenshot.ToListAsync().Result.FindAll(m => m.ChampionId == champion.ChampionId);
return champion;
}
So I am basically getting a specific Champion and then filling the ChampionScreenshot attribute (which is also a Class) separately, but the thing is that inside my ChampionScreenshot there is also a Champion class attribute which fully loads once again:
Which is obviously generating an error once it is exposed in the endpoint of the Restful Service:
[Produces("application/json")]
[Route("api/Champions")]
public class ChampionsController : Controller
{
[HttpGet("{id}")]
public async Task<IActionResult> GetChampion([FromRoute] int id)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var champion = await _service.GetChampion(id);
if (champion == null)
{
return NotFound();
}
return Ok(champion);
}
...
Error:
Newtonsoft.Json.JsonSerializationException: Self referencing loop detected for property 'champion' with type 'ChampionsService.Models.Champion'. Path 'championScreenshot[0]'.
So, I was thinking in just creating my custom model and fill it with the data extracted from my DbContext instead of returning the models already created but I really think that there should be a way to fully use only the mapped Models, I was wondering that...
Champion references itself:
Champion > multiple ChampionScreenshot > Champion (back to the original object)
That's easy to solve:
return Json(champion, new JsonSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore });
Or you could do it for the entire application:
services.AddMvc().AddJsonOptions(opts =>
{
opts.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
});
And then just:
return Json(champion);
The following troubles me, though:
Champion champion = await _context.Champion.FirstAsync(m => m.ChampionId == id);
champion.ChampionScreenshot = _context.ChampionScreenshot.ToListAsync().Result.FindAll(m => m.ChampionId == champion.ChampionId);
You are saying "go to the database, download every single championscreenshot and find the ones I want through an in-memory search". That's not only horrible slow, it also wastes a lot of resources in your application and in the database. For including data, you use Include:
Champion champion = await _context.Champion
.Include(x => x.ChampionScreenshot)
.FirstAsync(x => x.ChampionId == id);
(this says "go to the database and bring me the champion but also include all the ChampionScreenshot that correspond, through an inner join).

Is EntityFramework performance affected by specifying information like primary key?

Say I have this simple entity:
public class Person
{
public int PersonID { get; set; }
public string Name { get; set; }
}
The entity framework can infer by convention that the PersonID field is the primary key. However, if I give the model builder this class:
public class PersonCfg : EntityTypeConfiguration<Person>
{
public PersonCfg()
{
ToTable("Person", "Person");
HasKey(p => p.PersonID);
}
}
Would that improve startup performance? My thought was it might allow EF to do less reflecting and startup the app faster but I don't know how it works behind the scenes to know if it has any impact.
To test this, you can use the DbModelBuilder class to build the model yourself and track the speed of the "Compile" step.
Here's my example code (LinqPad script):
void Main()
{
// Initialize the overall system, but don't count the result.
BuildC();
DateTime startDateA = DateTime.Now;
BuildA();
DateTime.Now.Subtract(startDateA).TotalMilliseconds.Dump("A");
DateTime startDateB = DateTime.Now;
BuildB();
DateTime.Now.Subtract(startDateB).TotalMilliseconds.Dump("B");
}
public class PersonA
{
public int PersonAId { get; set; }
public string Name { get; set; }
}
private void BuildA()
{
var builder = new DbModelBuilder();
builder.Entity<PersonA>();
var model = builder.Build(new DbProviderInfo("System.Data.SqlClient", "2008"));
model.Compile();
}
public class PersonB
{
public int PersonBId { get; set; }
public string Name { get; set; }
}
private void BuildB()
{
var builder = new DbModelBuilder();
builder.Conventions.Remove<IdKeyDiscoveryConvention>();
builder.Entity<PersonB>()
.HasKey(p => p.PersonBId);
var model = builder.Build(new DbProviderInfo("System.Data.SqlClient", "2008"));
model.Compile();
}
public class PersonC
{
public int PersonCId { get; set; }
public string Name { get; set; }
}
private void BuildC()
{
var builder = new DbModelBuilder();
builder.Entity<PersonC>()
.HasKey(p => p.PersonCId);
var model = builder.Build(new DbProviderInfo("System.Data.SqlClient", "2008"));
model.Compile();
}
I get the result of 2.0004ms to 2.0009ms. Curiously, removing conventions made the operation take longer.

AutoMapper isn't recognizing profile-specific prefixes

I'm trying to use AutoMapper to take data from a class that has prefixes before property names and map it to a second class that doesn't have those prefixes. However, I don't necessarily want it to always strip out that prefix: I just want it to do it for this particular mapping.
My source class looks like this:
public class AdvancedSearchFilterDataModel
{
// ....
public string ServiceMeterNumber { get; set; }
// ....
}
My destination class looks like this:
[DataContract]
public class ServicesAdvancedSearchFilterData : AdvancedSearchFilterData
{
// ....
[DataMember]
public string MeterNumber { get; set; }
// ....
}
When I try to map values like this, it works:
Mapper.Configuration.RecognizePrefixes("Service");
Mapper.CreateMap<AdvancedSearchFilterDataModel, ServicesAdvancedSearchFilterData>();
ServicesAdvancedSearchFilterData servciesFilterData =
Mapper.Map<ServicesAdvancedSearchFilterData>(model);
But I only want "Service" to be recognized as a prefix for certain mappings, since it's also used as a normal part of property names in other mappings. I tried to handle this with a profile, but this didn't work -- no data was mapped:
Mapper.CreateProfile("ServicePrefix").RecognizePrefixes("Service");
Mapper.CreateMap<AdvancedSearchFilterDataModel, ServicesAdvancedSearchFilterData>()
.WithProfile("ServicePrefix");
ServicesAdvancedSearchFilterData servciesFilterData =
Mapper.Map<ServicesAdvancedSearchFilterData>(model);
How can I make it recognize the prefix only when I want it to, either using profiles or some other technique? (I also have other prefixes that I'm going to need it to recognize for other mappings in the same way.)
I achieved this functionality by creating following structure:
I have Person model for my view which is flattened from PersonCombined
public class PersonCombined
{
public Person Person { get; set; }
public Address DefaultAddress { get; set; }
public Contact EmailContact { get; set; }
public Contact PhoneContact { get; set; }
public Contact WebsiteContact { get; set; }
}
public class Person : IWebServiceModel
{
public int ID { get; set; }
public string PersonFirstName { get; set; }
public string PersonSurname { get; set; }
public string PersonDescription { get; set; }
public Nullable<bool> PersonIsActive { get; set; }
}
Then I have separate class for this mapping only that looks like this:
public class PersonCustomMapping : ICustomMapping
{
const string separator = " ";
private static IMappingEngine _MappingEngine;
public IMappingEngine MappingEngine
{
get
{
if (_MappingEngine == null)
{
var configuration = new ConfigurationStore(new TypeMapFactory(), AutoMapper.Mappers.MapperRegistry.Mappers);
configuration.RecognizePrefixes("Person");
configuration.RecognizeDestinationPrefixes("Person");
configuration.CreateMap<Person, MCIACRM.Model.Combine.PersonCombined>();
configuration.CreateMap<MCIACRM.Model.Combine.PersonCombined, Person>();
_MappingEngine = new MappingEngine(configuration);
}
return _MappingEngine;
}
}
}
In my generic view I have mappingEngine property like this:
private IMappingEngine mappingEngine
{
get
{
if (_mappingEngine == null)
{
_mappingEngine = AutoMapper.Mapper.Engine;
}
return _mappingEngine;
}
}
Finally in my generic view constructor i have:
public GenericEntityController(IGenericLogic<S> logic, ICustomMapping customMapping)
: base()
{
this._mappingEngine = customMapping.MappingEngine;
this.logic = logic;
}
And that's how I do mapping:
result = items.Project(mappingEngine).To<R>();
or
logic.Update(mappingEngine.Map<S>(wsItem));
Because I use 1 entity per view I can define custom mapping configuration per entity.
Hope this helps

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