Concisely Insert/Update/Delete Enumerable Child Entities using Automapper - c#

I am working on an ASP.Net MVC application and I have a "Report" object that has related enumerables such as schedules and comments. Using AutoMapper, it has been easy to convert from a report entity to a View Model and back, but I have issues when I try to save the Report object (mapped to an existing entity from a view model) back to the database.
More specifically, I can't seem to concisely update existing entities, insert new entities, and delete old entities using automapper. For instance whenever I map schedules from the view model to a report entity, it deletes the existing schedules and then creates new ones (with incremented indexes). This is my code:
public class ScheduleViewModel
{
public int ScheduleID { get; set; }
public int ReportID { get; set; }
public int Month { get; set; }
public int Day { get; set; }
public string Description { get; set; }
}
public class ReportViewModel
{
public int ReportID { get; set; }
public List<ScheduleViewModel> Schedules { get; set; }
public void Save()
{
dbContext db = new dbContext();
Report original = db.Reports.SingleOrDefault(o => o.ReportID == ReportID);
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<ReportViewModel, Report>();
});
config.AssertConfigurationIsValid();
var mapper = config.CreateMapper();
mapper.Map(this, original);
db.SaveChanges();
}
}
My Report object has a relational key (and a "Schedules" navigational property), so everything is mapped successfully from my view model to the "original" Report. New schedules have a ScheduleID of 0, since they haven't been assigned, and they get added to the database using the auto-increment, which is what I want. The existing schedules maintain their ScheduleID when mapped to the "original" report object, but then recieve incremented IDs once SaveChanges is called.
As I understand it, I'm attaching new schedules to the context whether or not the view model ID properties match the primary key in the database (in this case it is a composite of ReportID and ScheduleID). Is there a clean way, using some sort of ForMember(report => report.Schedules), expression that makes Entity Framework understand to not destroy my existing entities if a View Model object can map to an existing Key?
I am looking for something that functions similar to the code below, but since I will have many enumerable properties attached to my report objects, I don't want to maintain these sections for each:
foreach (Schedule schedule in db.Schedules.Where(s => s.ReportID == this.ReportID))
{
ScheduleViewModel svm = this.Schedules.FirstOrDefault(s => s.ScheduleID == schedule.ScheduleID);
//Update Existing
if (svm != null)
db.Entry(schedule).CurrentValues.SetValues(svm);
//Delete Missing
else
db.Entry(schedule).State = System.Data.Entity.EntityState.Deleted;
}
//Insert New
foreach(ScheduleViewModel svm in this.Schedules.Where(s => s.ScheduleID == 0))
{
svm.ReportID = ReportID;
Schedule schedule = new Schedule() {};
db.Schedules.Add(schedule);
db.Entry(schedule).CurrentValues.SetValues(svm);
}

Apparently this is not currently possible with AutoMapper. Instead of mapping individual entities, AutoMapper destroys the existing entity collection and creates a new one with the same properties. I'm sure that works fine for some applications, but with Entity Framework's change tracking it is telling the database to delete the existing records and insert new ones, with new IDs. Unfortunately it seems collections have to be mapped individually/manually using the method I posted in my original question. Rather than repeating that for every collection, though, I wrote a generic handler that will map a model collection to an entity collection using a specified key -- without destroying the entities:
public void UpdateEntitySet<T>(IEnumerable<object> models, IEnumerable<T> entities, string key) where T : class
{
Dictionary<object, T> entityDictionary = new Dictionary<object, T>();
foreach(T entity in entities)
{
var entityKey = entity.GetType().GetProperty(key).GetValue(entity);
entityDictionary.Add(entityKey, entity);
}
if (models != null)
{
foreach (object model in models)
{
var modelKey = model.GetType().GetProperty(key).GetValue(model);
var existingEntity = entityDictionary.SingleOrDefault(d => Object.Equals(d.Key, modelKey)).Value;
if (existingEntity == null)
{
var newEntity = db.Set<T>().Create();
db.Set<T>().Add(newEntity);
db.Entry(newEntity).CurrentValues.SetValues(model);
}
else
{
db.Entry(existingEntity).CurrentValues.SetValues(model);
entityDictionary.Remove(entityDictionary.Single(d => Object.Equals(d.Key, modelKey)).Key);
}
}
}
for (int i = 0; i < entityDictionary.Count; i++)
db.Entry(entityDictionary.ElementAt(i).Value).State = System.Data.Entity.EntityState.Deleted;
}
If you have a composite key, you'll have to modify the code a little, otherwise you can ignore the mapping with AutoMapper and then use the method above like so:
IEnumerable<Schedule> ScheduleEntities = db.Set<Schedule>().Where(s => s.ReportID == ReportID);
UpdateEntitySet<Schedule>(ScheduleViewModels, ScheduleEntities, "ScheduleID");

Related

Why is Entity Framework Core attempting to insert records into one of the tables from many to many relationships and NOT the join table?

Given the following set up where there are many Teams and there are many LeagueSessions. Each Team belongs to zero or more LeagueSessions but only ever one LeagueSession is active. LeagueSessions have many teams, and the teams will be repeated. Many-to-many relationship is established between Teams and LeagueSessions with a join table called TeamsSessions.
Team model looks like this:
public class Team
{
public string Id { get; set; }
public string Name { get; set; }
public League League { get; set; }
public string LeagueID { get; set; }
public bool Selected { get; set; }
public ICollection<Match> Matches { get; set; }
public virtual ICollection<TeamSession> TeamsSessions { get; set; }
}
Team model fluent api configuration:
`
public class TeamConfiguration
{
public TeamConfiguration(EntityTypeBuilder<Team> model)
{
// The data for this model will be generated inside ThePLeagueDataCore.DataBaseInitializer.DatabaseBaseInitializer.cs class
// When generating data for models in here, you have to provide it with an ID, and it became mildly problematic to consistently get
// a unique ID for all the teams. In ThePLeagueDataCore.DataBaseInitializer.DatabaseBaseInitializer.cs we can use dbContext to generate
// unique ids for us for each team.
model.HasOne(team => team.League)
.WithMany(league => league.Teams)
.HasForeignKey(team => team.LeagueID);
}
}
`
Each team belongs to a single League. League model looks like this:
`public class League
{
public string Id { get; set; }
public string Type { get; set; }
public string Name { get; set; }
public IEnumerable<Team> Teams { get; set; }
public bool Selected { get; set; }
public string SportTypeID { get; set; }
public SportType SportType { get; set; }
public IEnumerable<LeagueSessionSchedule> Sessions { get; set; }
}`
fluent API for the League:
`public LeagueConfiguration(EntityTypeBuilder<League> model)
{
model.HasOne(league => league.SportType)
.WithMany(sportType => sportType.Leagues)
.HasForeignKey(league => league.SportTypeID);
model.HasMany(league => league.Teams)
.WithOne(team => team.League)
.HasForeignKey(team => team.LeagueID);
model.HasData(leagues);
}`
SessionScheduleBase class looks like this:
public class SessionScheduleBase
{
public string LeagueID { get; set; }
public bool ByeWeeks { get; set; }
public long? NumberOfWeeks { get; set; }
public DateTime SessionStart { get; set; }
public DateTime SessionEnd { get; set; }
public ICollection<TeamSession> TeamsSessions { get; set; } = new Collection<TeamSession>();
public ICollection<GameDay> GamesDays { get; set; } = new Collection<GameDay>();
}
Note: LeagueSessionSchedule inherits from SessionScheduleBase
The TeamSession model looks like this:
`public class TeamSession
{
public string Id { get; set; }
public string TeamId { get; set; }
public Team Team { get; set; }
public string LeagueSessionScheduleId { get; set; }
public LeagueSessionSchedule LeagueSessionSchedule { get; set; }
}`
I then configure the relationship with the fluent API like this:
`public TeamSessionConfiguration(EntityTypeBuilder<TeamSession> model)
{
model.HasKey(ts => new { ts.TeamId, ts.LeagueSessionScheduleId });
model.HasOne(ts => ts.Team)
.WithMany(t => t.TeamsSessions)
.HasForeignKey(ts => ts.TeamId);
model.HasOne(ts => ts.LeagueSessionSchedule)
.WithMany(s => s.TeamsSessions)
.HasForeignKey(ts => ts.LeagueSessionScheduleId);
}`
The problem arises whenever I attempt to insert a new LeagueSessionSchedule. The way I am adding a new TeamSession object onto the new LeagueSessionSchedule is like this:
`foreach (TeamSessionViewModel teamSession in newSchedule.TeamsSessions)
{
Team team = await this._teamRepository.GetByIdAsync(teamSession.TeamId, ct);
if(team != null)
{
TeamSession newTeamSession = new TeamSession()
{
Team = team,
LeagueSessionSchedule = leagueSessionSchedule
};
leagueSessionSchedule.TeamsSessions.Add(newTeamSession);
}
}`
Saving the new LeagueSessionSchedule code:
public async Task<LeagueSessionSchedule> AddScheduleAsync(LeagueSessionSchedule newLeagueSessionSchedule, CancellationToken ct = default)
{
this._dbContext.LeagueSessions.Add(newLeagueSessionSchedule);
await this._dbContext.SaveChangesAsync(ct);
return newLeagueSessionSchedule;
}
Saving the new LeagueSessionSchedule object throws an error by Entity Framework Core that it cannot INSERT a duplicate primary key value into the dbo.Teams table. I have no idea why its attempting to add to dbo.Teams table and not into TeamsSessions table.
ERROR:
INSERT INTO [LeagueSessions] ([Id], [Active], [ByeWeeks], [LeagueID], [NumberOfWeeks], [SessionEnd], [SessionStart])
VALUES (#p0, #p1, #p2, #p3, #p4, #p5, #p6);
INSERT INTO [Teams] ([Id], [Discriminator], [LeagueID], [Name], [Selected])
VALUES (#p7, #p8, #p9, #p10, #p11),
(#p12, #p13, #p14, #p15, #p16),
(#p17, #p18, #p19, #p20, #p21),
(#p22, #p23, #p24, #p25, #p26),
(#p27, #p28, #p29, #p30, #p31),
(#p32, #p33, #p34, #p35, #p36),
(#p37, #p38, #p39, #p40, #p41),
(#p42, #p43, #p44, #p45, #p46);
System.Data.SqlClient.SqlException (0x80131904): Violation of PRIMARY KEY constraint 'PK_Teams'. Cannot insert duplicate key in object 'dbo.Teams'. The duplicate key value is (217e2e11-0603-4239-aab5-9e2f1d3ebc2c).
My goal is to create a new LeagueSessionSchedule object. Along with the creation of this object, I also have to create a new TeamSession entry to the join table (or not if join table is not necessary) to then be able to pick any given team and see what session it is currently a part of.
My entire PublishSchedule method is the following:
`
public async Task<bool> PublishSessionsSchedulesAsync(List<LeagueSessionScheduleViewModel> newLeagueSessionsSchedules, CancellationToken ct = default(CancellationToken))
{
List<LeagueSessionSchedule> leagueSessionOperations = new List<LeagueSessionSchedule>();
foreach (LeagueSessionScheduleViewModel newSchedule in newLeagueSessionsSchedules)
{
LeagueSessionSchedule leagueSessionSchedule = new LeagueSessionSchedule()
{
Active = newSchedule.Active,
LeagueID = newSchedule.LeagueID,
ByeWeeks = newSchedule.ByeWeeks,
NumberOfWeeks = newSchedule.NumberOfWeeks,
SessionStart = newSchedule.SessionStart,
SessionEnd = newSchedule.SessionEnd
};
// leagueSessionSchedule = await this._sessionScheduleRepository.AddScheduleAsync(leagueSessionSchedule, ct);
// create game day entry for all configured game days
foreach (GameDayViewModel gameDay in newSchedule.GamesDays)
{
GameDay newGameDay = new GameDay()
{
GamesDay = gameDay.GamesDay
};
// leagueSessionSchedule.GamesDays.Add(newGameDay);
// create game time entry for every game day
foreach (GameTimeViewModel gameTime in gameDay.GamesTimes)
{
GameTime newGameTime = new GameTime()
{
GamesTime = DateTimeOffset.FromUnixTimeSeconds(gameTime.GamesTime).DateTime.ToLocalTime(),
// GameDayId = newGameDay.Id
};
// newGameTime = await this._sessionScheduleRepository.AddGameTimeAsync(newGameTime, ct);
newGameDay.GamesTimes.Add(newGameTime);
}
leagueSessionSchedule.GamesDays.Add(newGameDay);
}
// update teams sessions
foreach (TeamSessionViewModel teamSession in newSchedule.TeamsSessions)
{
// retrieve the team with the corresponding id
Team team = await this._teamRepository.GetByIdAsync(teamSession.TeamId, ct);
if(team != null)
{
TeamSession newTeamSession = new TeamSession()
{
Team = team,
LeagueSessionSchedule = leagueSessionSchedule
};
leagueSessionSchedule.TeamsSessions.Add(newTeamSession);
}
}
// update matches for this session
foreach (MatchViewModel match in newSchedule.Matches)
{
Match newMatch = new Match()
{
DateTime = match.DateTime,
HomeTeamId = match.HomeTeam.Id,
AwayTeamId = match.AwayTeam.Id,
LeagueID = match.LeagueID
};
leagueSessionSchedule.Matches.Add(newMatch);
}
try
{
leagueSessionOperations.Add(await this._sessionScheduleRepository.AddScheduleAsync(leagueSessionSchedule, ct));
}
catch(Exception ex)
{
}
}
// ensure all leagueSessionOperations did not return any null values
return leagueSessionOperations.All(op => op != null);
}
`
This is not a many-to-many relationship.
It is two separate one-to-many relationships, which happen to refer to the same table on one end of the relationship.
While it is true that on the database level, both use cases are represented by three tables, i.e. Foo 1->* FooBar *<-1 Bar, these two cases are treated differently by Entity Framework's automated behavior - and this is very important.
EF only handles the cross table for you if it is a direct many-to-many, e.g.
public class Foo
{
public virtual ICollection<Bar> Bars { get; set; }
}
public class Bar
{
public virtual ICollection<Foo> Foos { get; set; }
}
EF handles the cross table behind the scenes, and you are never made aware of the existence of the cross table (from the code perspective).
Importantly, EF Core does not yet support implicit cross tables! There is currently no way to do this in EF Core, but even if there were, you're not using it anyway, so the answer to your problem remains the same regardless of whether you're using EF or EF Core.
However, you have defined your own cross table. While this is still representative of a many-to-many relationship in database terms, it has ceased to be a many-to-many relationship as far as EF is concerned, and any documentation you find on EF's many-to-many relationships no longer applies to your scenario.
Unattached but indirectly added objects are assumed to be new.
By "indirectly added", I mean you that it was added to the context as part of another entity (which you directly added to the context). In the following example, foo is directly added and bar is indirectly added:
var foo = new Foo();
var bar = new Bar();
foo.Bar = bar;
context.Foos.Add(foo); // directly adding foo
// ... but not bar
context.SaveChanges();
When you add (and commit) a new entity to the context, EF adds it for you. However, EF also looks at any related entities that the first entity contains. During the commit in the above example, EF will look at both the foo and bar entities and will handle them accordingly. EF is smart enough to realize that you want bar to be stored in the database since you put it inside the foo object and you explicitly asked EF to add foo to the database.
It is important to realize that you've told EF that foo should be created (since you called Add(), which implies a new item), but you never told EF what it should do with bar. It's unclear (to EF) what you expect EF to do with this, and thus EF is left guessing at what to do.
If you never explained to EF whether bar already exists or not, Entity Framework defaults to assuming it needs to create this entity in the database.
Saving the new LeagueSessionSchedule object throws an error by Entity Framework Core that it cannot INSERT a duplicate primary key value into the dbo.Teams table. I have no idea why its attempting to add to dbo.Teams table
Knowing what you now know, the error becomes clearer. EF is trying to add this team (which was the bar object in my example) because it has no information on this team object and what its state in the database is.
There are a few solutions here.
1. Use the FK property instead of the navigational property
This is my preferred solution because it leaves no room for error. If the team ID does not yet exist, you get an error. At no point will EF try to create a team, since it doesn't even know the team's data, it only knows the (alleged) ID you're trying to create a relationship with.
Note: I am omitting LeagueSessionSchedule as it is unrelated to the current error - but it's essentially the same behavior for both Team and LeagueSessionSchedule.
TeamSession newTeamSession = new TeamSession()
{
TeamId = team.Id
};
By using the FK property instead of the nav prop, you are informing EF that this is an existing team - and therefore EF no longer tries to (re)create this team.
2. Ensure that the team is tracked by the current context
Note: I am omitting LeagueSessionSchedule as it is unrelated to the current error - but it's essentially the same behavior for both Team and LeagueSessionSchedule.
context.Teams.Attach(team);
TeamSession newTeamSession = new TeamSession()
{
Team = team
};
By attaching the object to the context, you are informing it of its existence. The default state of a newly attached entity is Unchanged, meaning "this already exists in the database and has not been changed - so you don't need to update it when we commit the context".
If you have actually made changes to your team that you want to be updated during commit, you should instead use:
context.Entry(team).State = EntityState.Modified;
Entry() inherently also attaches the entity, and by setting its state to Modified you ensure that the new values will be committed to the database when you call SaveChanges().
Note that I prefer solution 1 over solution 2 because it's foolproof and much less likely to lead to unexpected behavior or runtime exceptions.
String primary keys are undesirable
I'm not going to say that it doesn't work, but strings cannot be autogenerated by Entity Framework, making them undesirable as the type of your entity's PK. You will need to manually set your entity PK values.
Like I said, it's not impossible, but your code shows that you're not explicitly setting PK values:
if(team != null)
{
TeamSession newTeamSession = new TeamSession()
{
Team = team,
LeagueSessionSchedule = leagueSessionSchedule
};
leagueSessionSchedule.TeamsSessions.Add(newTeamSession);
}
If you want your PK's to be automatically generated, use an appropriate type. int and Guid are by far the most commonly used types for this.
Otherwise, you're going to have to start setting your own PK values, because if you don't (and the Id value thus defaults to null), your code is going to fail when you add a second TeamSession object using the above code (even though you're doing everything else correctly), since PK null is already taken by the first entity you added to the table.

When do you need to .Include related entities in Entity Framework?

This seems arbitrary to me when I have to actually .Include() related entities and when I don't. In some cases, EF gives me the info for the related entities without it and in other cases, it can't do anything with the related entities because I didn't include them:
Works without .Include();
This is an example where I'm loading data without .Include();
public class InvoiceService
{
private ApplicationDbContext db { get; set; }
public InvoiceService(ApplicationDbContext context)
{
db = context;
}
public Invoice Get(int id)
{
return db.Invoices.SingleOrDefault(x => x.Id == id);
}
}
public partial class ShowInvoice : System.Web.UI.Page
{
private InvoiceService invoiceService;
private readonly ApplicationDbContext context = new ApplicationDbContext();
protected void Page_Load(object sender, EventArgs e)
{
invoiceService = new InvoiceService(context);
if (!IsPostBack)
{
int.TryParse(Request.QueryString["invoiceId"].ToString(), out int invoiceId);
LoadInvoice(invoiceId);
}
}
private void LoadInvoice(int invoiceId)
{
var invoice = invoiceService.Get(invoiceId);
// Other code irrelevant to the question goes here.
}
}
Here follows the result which includes the data for the Company associated with the invoice I'm requested:
As you can see, the information for the company definitely comes through but was not explicitly included.
Doesn't work without .Include();
Conversely, I've done some mapping to do with invoices in this same project and I got NullReferenceExceptions when fetching the related entities property values because I didn't .Include().
This method gets all the approved timesheet entries for the specified company. This viewmodel is exclusively to be used when manipulating the association of timesheet entries for an invoice (so you're invoicing based on the timesheet entries selected).
public List<InvoiceTimesheetViewModel> GetInvoiceTimesheetsByCompanyId(int companyId)
{
var factory = new TimesheetViewModelsFactory();
var timesheets = db.Timesheets.Where(x => x.Approved && x.Company.Id == companyId && !x.Deleted).ToList();
return factory.GetInvoiceTimesheetsViewModel(timesheets);
}
NullReferenceExceptions occurred in the factory that maps the timesheet entities to the viewmodel:
public List<InvoiceTimesheetViewModel> GetInvoiceTimesheetsViewModel(List<Timesheet> timesheets)
{
var model = new List<InvoiceTimesheetViewModel>();
foreach (var timesheet in timesheets)
{
var start = DateTime.Parse((timesheet.DateAdded + timesheet.StartTime).ToString());
var finished = DateTime.Parse((timesheet.DateCompleted + timesheet.EndTime).ToString());
DateTime.TryParse(timesheet.RelevantDate.ToString(), out DateTime relevant);
model.Add(new InvoiceTimesheetViewModel
{
RelevantDate = relevant,
BillableHours = timesheet.BillableHours,
Finished = finished,
Id = timesheet.Id,
StaffMember = timesheet.StaffMember.UserName, // NRE here.
Start = start,
Task = timesheet.Task.Name // NRE here.
});
}
return model;
}
To fix these, I had to change the query that fetches the data to the following:
var timesheets = db.Timesheets.Include(i => i.StaffMember).Include(i => i.Task)
.Where(x => x.Approved && x.Company.Id == companyId && !x.Deleted).ToList();
Why is Entity Framework sometimes happy to give me data without me explicitly requesting that data and sometimes it requires me to explicitly request the data or else throws an error?
And how am I to know when I need to explicitly include the data I'm looking for and when I don't?
Entity framework uses lazy loading to load child relationships. For lazy loading to work property in the model should be marked with virtual keyword. Ef overrides it and adds lazy loading support.
When you have no virtual property EF has no way to load your child relationship data later, so the only time it's possible to do - during initial data loading using Include.
public class Timesheet
{
...
public virtual StaffMember StaffMember { get; set; }
public virtual Task Task { get; set; }
...
}
It depends on your models. If you have marked relational properties as virtual then you'll need to use .Include so EF knows that you need it. It is Lazy Loading. Preserves machine's memory and DB requests.

Persisting Entity Framework nested/related entities

I have the following entities:
public class ModuleCriteria
{
public int ModuleCriteriaId { get; set; }
public string Criteria { get; set; }
public List<ModuleCriteriaLookup> ModuleCriteriaLookups { get; set;
}
}
public class ModuleCriteriaLookup
{
public int ModuleCriteriaLookupId { get; set; }
public int ModuleCriteriaId { get; set; } // ** (Foreign Key) **
public int SiteId { get; set; }
public string CategoryId { get; set; }
public ModuleCriteria ModuleCriteria { get; set; }
}
I have the following EF configuration in my Context class (edited for brevity):
protected override void OnModelCreating( DbModelBuilder modelBuilder )
{
base.OnModelCreating( modelBuilder );
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Entity<ModuleCriteriaLookup>().HasRequired( mc => mc.ModuleCriteria );
// I tried adding the below line but it made no difference.
//modelBuilder.Entity<ModuleCriteria>().HasMany( mc => mc.ModuleCriteriaLookups );
}
...and I have the following DbSet properties defined in my Context class:
public DbSet<ModuleCriteria> ModuleCriteria { get; set; }
public DbSet<ModuleCriteriaLookup> ModuleCriteriaLookup { get; set; }
I have a CriteriaRepository class, which has a Save method, for persisting changes for my ModuleCriteria entities:
public void Save( ModuleCriteria moduleCriteria )
{
using ( var ctx = new MyAppContext() )
{
ctx.Entry( moduleCriteria ).State = moduleCriteria.ModuleCriteriaId == 0 ? EntityState.Added : EntityState.Modified;
ctx.SaveChanges();
}
}
A ModuleCriteria object can exist without a ModuleCriteriaLookup object, but ModuleCriteriaLookup object has to relate to an existing ModuleCriteria object (related on ModuleCriteriaId).
You can have multiple ModuleCriteraLookup objects all relating to the same one ModuleCriteria object.
The behaviour that I would like, and expect with EF, is:
1) If I create a new ModuleCriteria object (without any ModuleCriteriaLookups), call the Save method in my repository, I would expect to see new ModuleCriteria record in the db with no related ModuleCriteriaLookup records in the db.
2) If I create a new ModuleCriteria object and assign a List<ModuleCriteriaLookup> to it, call the Save method in my repository, I would expect to see new ModuleCriteria record in the db and x new ModuleCriteriaLookup rows in the db which relate to that particular ModuleCriteria.
3) If I add/edit/remove any of the ModuleCriteriaLookup objects that related to one of my ModuleCriteria objects, then call the Save method in my repository, I would expect to see any of the ModuleCriteria's deleted ModuleCriteriaLookup objects to get removed from the db, any new ones added and any edited ones simply to get updated.
So all I ever need worry about is that whatever the ModuleCriteria.ModuleCriteriaLookups property contains for a given ModuleCriteria, that's what will be reflected in the 2 tables in my DB by simply calling the Save method for the ModuleCriteria object in my repository.
Unfortunately at the moment, if I'm adding a new ModuleCriteria object with associated List<ModuleCriteriaLookup> it adds both ModuleCriteria and x ModuleCriteriaLookup rows in the db nicely. But when I want to edit or delete entries in the ModuleCriteria.ModuleCriteriaLookups property, this is not being reflected in the db. Nothing is happening with the ModuleCriteriaLookups rows.
I'm not sure where exactly the problem is, whether its whether the EF mapping configuration, or something to do with how the repository works?
The problem is located in the repository. The DbContext needs to be aware of the existence of entities. So when editing and/or deleting entities the entities need to be fetched from the database first.
This description clearly states:
The .Entry property returns objects from the context that are being
tracked by the context.
Because you directly use this properties right after creating the context, the context isn't tracking these entities and is therefore not aware that something has changed. And thereby is unable to generate the correct SQL statements.
There are several ways to deal with this, depending on the rest of your design.
One way to delete it would be:
public void DeleteModuleCriteriaLookup(ModuleCriteriaLookup[] lookups)
{
using (var ctx = new MyAppContext())
{
var moduleCriteriaId = lookups.First().ModuleCriteriaId;
var moduleCritria = (
from criteria in ctx.ModuleCriteria
where criteria.ModuleCriteriaId == moduleCriteriaId
select criteria
).Single();
var lookupIdsToDelete = lookups.Select(l => l.ModuleCriteriaLookupId);
var lookupsToDelete = (
from lookup in moduleCritria.ModuleCriteriaLookups
where lookupIdsToDelete.Contains(lookup.ModuleCriteriaLookupId)
select lookup
).ToArray();
foreach (var lookup in lookupsToDelete)
{
moduleCritria.ModuleCriteriaLookups.Remove(lookup);
}
ctx.SaveChanges();
}
}

Entity Framework 6.1: navigation properties not loading

This is my first time using Entity Framework 6.1 (code first). I keep running into a problem where my navigation properties are null when I don't expect them to be. I've enabled lazy loading.
My entity looks like this:
public class Ask
{
public Ask()
{
this.quantity = -1;
this.price = -1;
}
public int id { get; set; }
public int quantity { get; set; }
public float price { get; set; }
public int sellerId { get; set; }
public virtual User seller { get; set; }
public int itemId { get; set; }
public virtual Item item { get; set; }
}
It has the following mapper:
class AskMapper : EntityTypeConfiguration<Ask>
{
public AskMapper()
{
this.ToTable("Asks");
this.HasKey(a => a.id);
this.Property(a => a.id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
this.Property(a => a.id).IsRequired();
this.Property(a => a.quantity).IsRequired();
this.Property(a => a.price).IsRequired();
this.Property(a => a.sellerId).IsRequired();
this.HasRequired(a => a.seller).WithMany(u => u.asks).HasForeignKey(a => a.sellerId).WillCascadeOnDelete(true);
this.Property(a => a.itemId).IsRequired();
this.HasRequired(a => a.item).WithMany(i => i.asks).HasForeignKey(a => a.itemId).WillCascadeOnDelete(true);
}
}
Specifically, the problem is that I have an Ask object with a correctly set itemId (which does correspond to an Item in the database), but the navigation property item is null, and as a result I end up getting a NullReferenceException. The exception is thrown in the code below, when I try to access a.item.name:
List<Ask> asks = repo.GetAsksBySeller(userId).ToList();
List<ReducedAsk> reducedAsks = new List<ReducedAsk>();
foreach (Ask a in asks)
{
ReducedAsk r = new ReducedAsk() { id = a.id, sellerName = a.seller.username, itemId = a.itemId, itemName = a.item.name, price = a.price, quantity = a.quantity };
reducedAsks.Add(r);
}
Confusingly, the seller navigation property is working fine there, and I can't find anything I've done differently in the 'User' entity, nor in its mapper.
I have a test which recreates this, but it passes without any problems:
public void canGetAsk()
{
int quantity = 2;
int price = 10;
//add a seller
User seller = new User() { username = "ted" };
Assert.IsNotNull(seller);
int sellerId = repo.InsertUser(seller);
Assert.AreNotEqual(-1, sellerId);
//add an item
Item item = new Item() { name = "fanta" };
Assert.IsNotNull(item);
int itemId = repo.InsertItem(item);
Assert.AreNotEqual(-1, itemId);
bool success = repo.AddInventory(sellerId, itemId, quantity);
Assert.AreNotEqual(-1, success);
//add an ask
int askId = repo.InsertAsk(new Ask() { sellerId = sellerId, itemId = itemId, quantity = quantity, price = price });
Assert.AreNotEqual(-1, askId);
//retrieve the ask
Ask ask = repo.GetAsk(askId);
Assert.IsNotNull(ask);
//check the ask info
Assert.AreEqual(quantity, ask.quantity);
Assert.AreEqual(price, ask.price);
Assert.AreEqual(sellerId, ask.sellerId);
Assert.AreEqual(sellerId, ask.seller.id);
Assert.AreEqual(itemId, ask.itemId);
Assert.AreEqual(itemId, ask.item.id);
Assert.AreEqual("fanta", ask.item.name);
}
Any help would be extremely appreciated; this has been driving me crazy for days.
EDIT:
The database is SQL Server 2014.
At the moment, I have one shared context, instantiated the level above this (my repository layer for the db). Should I be instantiating a new context for each method? Or instantiating one at the lowest possible level (i.e. for every db access)? For example:
public IQueryable<Ask> GetAsksBySeller(int sellerId)
{
using (MarketContext _ctx = new MarketContext())
{
return _ctx.Asks.Where(s => s.seller.id == sellerId).AsQueryable();
}
}
Some of my methods invoke others in the repo layer. Would it better for each method to take a context, which it can then pass to any methods it calls?
public IQueryable<Transaction> GetTransactionsByUser(MarketContext _ctx, int userId)
{
IQueryable<Transaction> buyTransactions = GetTransactionsByBuyer(_ctx, userId);
IQueryable<Transaction> sellTransactions = GetTransactionsBySeller(_ctx, userId);
return buyTransactions.Concat(sellTransactions);
}
Then I could just instantiate a new context whenever I call anything from the repo layer: repo.GetTransactionsByUser(new MarketContext(), userId);
Again, thanks for the help. I'm new to this, and don't know which approach would be best.
Try to add
Include call in your repository call:
public IQueryable<Ask> GetAsksBySeller(int sellerId)
{
using (MarketContext _ctx = new MarketContext())
{
return _ctx.Asks
.Include("seller")
.Include("item")
.Where(s => s.seller.id == sellerId).AsQueryable();
}
}
Also, there is an extension method Include which accepts lambda expression as parameter and provides you type checks on compile time
http://msdn.microsoft.com/en-us/data/jj574232.aspx
As for the context lifespan, your repositories should share one context per request if this is a web application. Else it's a bit more arbitrary, but it should be something like a context per use case or service call.
So the pattern would be: create a context, pass it to the repositories involved in the call, do the task, and dispose the context. The context can be seen as your unit of work, so no matter how many repositories are involved, in the end one SaveChanges() should normally be enough to commit all changes.
I can't tell if this will solve the lazy loading issue, because from what I see I can't explain why it doesn't occur.
But although if I were in your shoes I'd like to get to the bottom of it, lazy loading is something that should not be relied on too much. Take a look at your (abridged) code:
foreach (Ask a in asks)
{
ReducedAsk r = new ReducedAsk()
{
sellerName = a.seller.username,
itemName = a.item.name
};
If lazy loading would work as expected, this would execute two queries against the database for each iteration of the loop. Of course, that's highly inefficient. That's why using Include (as in Anton's answer) is better anyhow, not only to circumvent your issue.
A further optimization is to do the projection (i.e. the new {) in the query itself:
var reducedAsks = repo.GetAsksBySeller(userId)
.Select(a => new ReducedAsk() { ... })
.ToList();
(Assuming – and requiring – that repo.GetAsksBySeller returns IQueryable).
Now only the data necessary to create ReducedAsk will be fetched from the database and it prevents materialization of entities that you're not using anyway and relatively expensive processes as change tracking and relationship fixup.

Updating many-to-many relationships with a generic repository

I have a database context with lazy loading disabled. I am using eager loading to load all of my entities. I cannot update many to many relationships.
Here's the repository.
public class GenericRepository<TEntity> : IGenericRepository<TEntity>
where TEntity : class
{
... other code here...
public virtual void Update(TEntity t)
{
Set.Attach(t);
Context.Entry(t).State = EntityState.Modified;
}
...other code here...
}
Here's the User model.
public partial class User
{
public User()
{
this.Locks = new HashSet<Lock>();
this.BusinessModels = new HashSet<BusinessModel>();
}
public int UserId { get; set; }
public string Username { get; set; }
public string Name { get; set; }
public string Phone { get; set; }
public string JobTitle { get; set; }
public string RecoveryEmail { get; set; }
public Nullable<double> Zoom { get; set; }
public virtual ICollection<Lock> Locks { get; set; }
public virtual ICollection<BusinessModel> BusinessModels { get; set; }
}
If I modify the business models collection, it does not save the business models collection although I have attached the entire entity.
Worker.UserRepository.Update(user);
I'm not sure what is going on. I don't want to break my generic repository/unit of work pattern just to update many-to-many relationships.
Edit 2: I've got this working...but it is extremely different from the pattern that I'm going for. Having hard implementations means I will need to create a method for each type that has a many to many relationship. I am investigating now to see if I can make this a generic method.
Edit 3: So the previous implementation I had did not work like I thought it would. But now, I have a slightly working implementation. If someone would please help me so I can move on from this, I will love you forever.
public virtual void Update(TEntity updated,
IEnumerable<object> set,
string navigationProperty,
Expression<Func<TEntity, bool>> filter,
Type propertyType)
{
// Find the existing item
var existing = Context.Set<TEntity>().Include(navigationProperty).FirstOrDefault(filter);
// Iterate through every item in the many-to-many relationship
foreach (var o in set)
{
// Attach it if its unattached
if (Context.Entry(o).State == EntityState.Detached)
// Exception "an object with the same key already exists"
// This is due to the include statement up above. That statement
// is necessary in order to edit the entity's navigation
// property.
Context.Set(propertyType).Attach(o);
}
// Set the new value on the navigation property.
Context.Entry(existing).Collection(navigationProperty).CurrentValue = set;
// Set new primitive property values.
Context.Entry(existing).CurrentValues.SetValues(updated);
Context.Entry(existing).State = EntityState.Modified;
}
I then call it like this:
Worker.UserRepository.Update(user, user.BusinessModels, "BusinessModels", i => i.UserId == user.UserId, typeof (BusinessModel));
Extremely messy, but it lets me update many-to-many relationships with generics. My big problem is the exception when I go to attach new values that already exist. They're already loaded because of the include statement.
This works:
This doesn't:
After many painful hours, I have finally found a way to update many-to-many relationships with a completely generic repository. This will allow me to create (and save) many different types of entities without creating boilerplate code for each one.
This method assumes that:
Your entity already exists
Your many to many relationship is stored in a table with a composite key
You are using eager loading to load your relationships into context
You are using a unit-of-work/generic repository pattern to save your entities.
Here's the Update generic method.
public virtual void Update(Expression<Func<TEntity, bool>> filter,
IEnumerable<object> updatedSet, // Updated many-to-many relationships
IEnumerable<object> availableSet, // Lookup collection
string propertyName) // The name of the navigation property
{
// Get the generic type of the set
var type = updatedSet.GetType().GetGenericArguments()[0];
// Get the previous entity from the database based on repository type
var previous = Context
.Set<TEntity>()
.Include(propertyName)
.FirstOrDefault(filter);
/* Create a container that will hold the values of
* the generic many-to-many relationships we are updating.
*/
var values = CreateList(type);
/* For each object in the updated set find the existing
* entity in the database. This is to avoid Entity Framework
* from creating new objects or throwing an
* error because the object is already attached.
*/
foreach (var entry in updatedSet
.Select(obj => (int)obj
.GetType()
.GetProperty("Id")
.GetValue(obj, null))
.Select(value => Context.Set(type).Find(value)))
{
values.Add(entry);
}
/* Get the collection where the previous many to many relationships
* are stored and assign the new ones.
*/
Context.Entry(previous).Collection(propertyName).CurrentValue = values;
}
Here's a helper method I found online which allows me to create generic lists based on whatever type I give it.
public IList CreateList(Type type)
{
var genericList = typeof(List<>).MakeGenericType(type);
return (IList)Activator.CreateInstance(genericList);
}
And from now on, this is what calls to update many-to-many relationships look like:
Worker.UserRepository.Update(u => u.UserId == user.UserId,
user.BusinessModels, // Many-to-many relationship to update
Worker.BusinessModelRepository.Get(), // Full set
"BusinessModels"); // Property name
Of course, in the end you will need to somewhere call:
Context.SaveChanges();
I hope this helps anyone who never truly found how to use many-to-many relationships with generic repositories and unit-of-work classes in Entity Framework.
#dimgl Your solution worked for me. What I've done in addition was to replace the hard-coded type and name of the primaryKey with dynamically retrieved ones:
ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext;
ObjectSet<TEntity> set = objectContext.CreateObjectSet<TEntity>();
IEnumerable<string> keyNames = set.EntitySet.ElementType.KeyMembers.Select(k => k.Name);
var keyName = keyNames.FirstOrDefault();
var keyType = typeof(TEntity).GetProperty(keyName).PropertyType
foreach (var entry in updatedSet
.Select(obj =>
Convert.ChangeType(obj.GetType()
.GetProperty(keyName)
.GetValue(obj, null), keyType))
.Select(value => context.Set<TEntity>().Find(value)))
{
values.Add(entry);
}
Like this your code won't depend on the Entity key's name and type.

Categories

Resources