Insert relations very slow - c#

I have to insert multiple relations and having issues with the Context.SaveChanges action which takes like forever to complete. I already tried multiple ways to add these entities to database but nothing seems to help me out.
My models are build in the following way:
public class Agreement : GdSoftDeleteEntity
{
public DateTime Date { get; set; }
public AgreementType AgreementType { get; set; }
public virtual ICollection<PersonAgreementRelation> PersonAgreementRelations { get; set; }
public virtual ICollection<ImageSearchAppointment> ImageSearchAppointments { get; set; }
}
public class Person : GdSoftDeleteEntity
{
public string Name { get; set; }
public string FirstName { get; set; }
// E-mail is in identityuser
//public string EmailAddress { get; set; }
public virtual PersonType PersonType { get; set; }
public virtual ICollection<PersonAgreementRelation> PersonAgreementRelations { get; set; }
public virtual ICollection<PersonPersonRelation> PersonMasters { get; set; }
public virtual ICollection<PersonPersonRelation> PersonSlaves { get; set; }
}
public class PersonAgreementRelation : GdSoftDeleteEntity
{
public int PersonId { get; set; }
public virtual Person Person { get; set; }
public int AgreementId { get; set; }
public virtual Agreement Agreement { get; set; }
public virtual PersonAgreementRole PersonAgreementRole { get; set; }
}
public class ImageSearchAppointment : GdSoftDeleteEntity
{
public string Name { get; set; }
public bool ShowResultsToCustomer { get; set; }
public bool HasImageFeed { get; set; }
public int AgreementId { get; set; }
public virtual Agreement Agreement { get; set; }
public Periodicity Periodicity { get; set; }
public PeriodicityCategory PeriodicityCategory { get; set; }
public virtual ICollection<ImageSearchCommand> ImageSearchCommands { get; set; }
public virtual ICollection<ImageSearchAppointmentWebDomainWhitelist> ImageSearchAppointmentWebDomainWhitelists { get; set; }
public virtual ICollection<ImageSearchAppointmentWebDomainExtension> ImageSearchAppointmentWebDomainExtensions { get; set; }
}
public class ImageSearchCommand : GdSoftDeleteEntity
{
public int ImageSearchAppointmentId { get; set; }
public virtual ImageSearchAppointment ImageSearchAppointment { get; set; }
public int? ImageSearchAppointmentCredentialsId { get; set; }
public virtual ImageSearchAppointmentCredentials ImageSearchAppointmentCredentials { get; set; }
public DateTime Date { get; set; }
//public bool Invoiced { get; set; }
public int NumberOfImages { get; set; }
public DateTime ImageCollectionProcessedDate { get; set; }
public virtual ICollection<ImageSearchExecution> ImageSearchExecutions { get; set; }
}
In my service, I have written following code:
public int AddAgreement(int personId, AgreementDto agreementDto)
{
Context.Configuration.LazyLoadingEnabled = false;
//var person = Context.Persons.SingleOrDefault(el => el.Id == personId);
var person = Context.Persons
.SingleOrDefault(x => x.Id == personId);
if (person == null)
{
throw new GraphicsDetectiveInvalidDataTypeException($"No person found for Id: {personId}");
}
if (agreementDto == null)
{
throw new GraphicsDetectiveInvalidDataTypeException("Invalid agreementDto");
}
//TODO: Check if OKAY!!!
if (agreementDto.ImageSearchAppointmentDto.Count == 0)
{
throw new GraphicsDetectiveInvalidDataTypeException("Count of imagesearchappointments can't be lower than 0");
}
//set agreement properties
var agreement = new Agreement
{
Date = agreementDto.DateTime,
AgreementType = AgreementType.WwwImageSearch,
//ImageSearchAppointments = new List<ImageSearchAppointment>(),
//IsDeleted = false
};
Context.Agreements.Add(agreement);
Context.SaveChanges();
//var personAdminId = Context.Users.Single(x => x.Email == ConfigurationManager.AppSettings["DefaultGdAdminEmail"]).PersonId;
// Dit werkt niet. Moet in 2 stappen
//set personagreementrelations for new agreement
var adminEmail = ConfigurationManager.AppSettings["DefaultGdAdminEmail"];
var personAdminId = Context.Users
.SingleOrDefault(x => x.Email == adminEmail)
.PersonId;
var personPmId = Context.Persons.Single(x => x.Name == "My name").Id;
var personAgreementRelations = new List<PersonAgreementRelation>()
{
new PersonAgreementRelation
{
AgreementId = agreement.Id,
PersonId = personId,
PersonAgreementRole = PersonAgreementRole.Client,
},
new PersonAgreementRelation
{
AgreementId = agreement.Id,
PersonAgreementRole = PersonAgreementRole.Supplier,
PersonId = personPmId,
},
new PersonAgreementRelation
{
AgreementId = agreement.Id,
PersonAgreementRole = PersonAgreementRole.Admin,
PersonId = personAdminId,
}
};
foreach (var personAgreementRelation in personAgreementRelations)
{
Context.PersonAgreementRelations.Add(personAgreementRelation);
}
Context.Configuration.ValidateOnSaveEnabled = false;
Context.Configuration.AutoDetectChangesEnabled = false;
Context.SaveChanges();
Context.Configuration.ValidateOnSaveEnabled = true;
Context.Configuration.AutoDetectChangesEnabled = true;
Context.Configuration.LazyLoadingEnabled = true;
return agreement.Id;
}
public void AddFirstImageSearchAppointmentToAgreement(int agreementId, ImageSearchAppointmentDto imageSearchAppointmentDto)
{
Context.Configuration.LazyLoadingEnabled = false;
var agreement = Context.Agreements.SingleOrDefault(x => x.Id == agreementId);
if (agreement == null)
{
throw new GraphicsDetectiveInvalidDataTypeException($"No agreement found for id {agreementId}");
}
var appointmentType = imageSearchAppointmentDto;
if (appointmentType == null)
{
throw new GraphicsDetectiveInvalidDataTypeException($"No valid imageSearchAppointment");
}
if (appointmentType.ImageSearchCommandDto.Count == 0)
{
throw new GraphicsDetectiveInvalidDataTypeException("No imageSearchCommand");
}
var imageSearchAppointment = new ImageSearchAppointment
{
AgreementId = agreement.Id,
Agreement = agreement,
Name = appointmentType.Name,
Periodicity = appointmentType.Periodicity,
PeriodicityCategory = appointmentType.PeriodicityCategory,
ShowResultsToCustomer = appointmentType.ShowResultsToCustomer,
ImageSearchAppointmentWebDomainExtensions = new List<ImageSearchAppointmentWebDomainExtension>(),
ImageSearchCommands = new List<ImageSearchCommand>(),
ImageSearchAppointmentWebDomainWhitelists = new List<ImageSearchAppointmentWebDomainWhitelist>(),
IsDeleted = false
};
var imageSearchCommandDto = appointmentType.ImageSearchCommandDto.Single();
var imageSearchCommand = new ImageSearchCommand()
{
ImageSearchAppointment = imageSearchAppointment,
Date = imageSearchCommandDto.Date,
NumberOfImages = imageSearchCommandDto.NumberOfImages,
ImageCollectionProcessedDate = imageSearchCommandDto.ImageCollectionProcessedDate,
IsDeleted = false
};
if (imageSearchCommandDto.ImageSearchAppointmentCredentialsDto != null)
{
imageSearchCommand.ImageSearchAppointmentCredentials = new ImageSearchAppointmentCredentials
{
FtpProfileType = imageSearchCommandDto.ImageSearchAppointmentCredentialsDto.FtpProfileType,
Location = imageSearchCommandDto.ImageSearchAppointmentCredentialsDto.Location,
Username = imageSearchCommandDto.ImageSearchAppointmentCredentialsDto.Username,
Password = imageSearchCommandDto.ImageSearchAppointmentCredentialsDto.Password,
UsePassive = imageSearchCommandDto.ImageSearchAppointmentCredentialsDto.UsePassive,
IsDeleted = false
};
}
imageSearchAppointment.ImageSearchCommands.Add(imageSearchCommand);
if (!imageSearchAppointment.ShowResultsToCustomer)
{
var webDomainExtensions = appointmentType.WebDomainExtensionDtos
.Select(x => new ImageSearchAppointmentWebDomainExtension()
{
ImageSearchAppointment = imageSearchAppointment,
WebDomainExtensionId = x.Id
})
.ToList();
imageSearchAppointment.ImageSearchAppointmentWebDomainExtensions = webDomainExtensions;
}
Context.ImageSearchAppointments.Add(imageSearchAppointment);
Context.SaveChanges();
Context.Configuration.LazyLoadingEnabled = true;
}
I used dotTrace to profile these functions and it takes about 9 minutes to add the new entities to my database.
The database is an Azure SQL database, tier S3
I tried the proposed solution and adapted my code as follow:
public int AddAgreement(int personId, AgreementDto agreementDto)
{
var agreementId = 0;
using (var context = new GdDbContext())
{
GdDbConfiguration.SuspendExecutionStrategy = true;
context.Configuration.LazyLoadingEnabled = true;
//var person = Context.Persons.SingleOrDefault(el => el.Id == personId);
var person = context.Persons
.SingleOrDefault(x => x.Id == personId);
if (person == null)
{
throw new GraphicsDetectiveInvalidDataTypeException($"No person found for Id: {personId}");
}
//var personAdminId = Context.Users.Single(x => x.Email == ConfigurationManager.AppSettings["DefaultGdAdminEmail"]).PersonId;
// Dit werkt niet. Moet in 2 stappen
//set personagreementrelations for new agreement
var adminEmail = ConfigurationManager.AppSettings["DefaultGdAdminEmail"];
var personAdminId = context.Users
.Where(x => x.Email == adminEmail)
.Include(x => x.Person)
.First()
.Person.Id;
var personPmId = context.Persons.First(x => x.Name == "My name").Id;
using (var dbContextTransaction = context.Database.BeginTransaction())
{
try
{
if (agreementDto == null)
{
throw new GraphicsDetectiveInvalidDataTypeException("Invalid agreementDto");
}
//TODO: Check if OKAY!!!
if (agreementDto.ImageSearchAppointmentDto.Count == 0)
{
throw new GraphicsDetectiveInvalidDataTypeException("Count of imagesearchappointments can't be lower than 0");
}
//set agreement properties
var agreement = new Agreement
{
Date = agreementDto.DateTime,
AgreementType = AgreementType.WwwImageSearch,
//ImageSearchAppointments = new List<ImageSearchAppointment>(),
//IsDeleted = false
};
context.Agreements.Add(agreement);
//Context.SaveChanges();
var personAgreementRelations = new List<PersonAgreementRelation>()
{
new PersonAgreementRelation
{
//Agreement = agreement,
AgreementId = agreement.Id,
PersonId = personId,
//Person = person,
PersonAgreementRole = PersonAgreementRole.Client,
//IsDeleted = false
},
new PersonAgreementRelation
{
//Agreement = agreement,
AgreementId = agreement.Id,
PersonAgreementRole = PersonAgreementRole.Supplier,
PersonId = personPmId,
//Person = personPm,
//IsDeleted = false
},
new PersonAgreementRelation
{
//Agreement = agreement,
AgreementId = agreement.Id,
PersonAgreementRole = PersonAgreementRole.Admin,
PersonId = personAdminId,
//Person = personAdmin,
}
};
foreach (var personAgreementRelation in personAgreementRelations)
{
context.PersonAgreementRelations.Add(personAgreementRelation);
}
//agreement.PersonAgreementRelations = personAgreementRelations;
//Context.Agreements.Add(agreement);
context.Configuration.ValidateOnSaveEnabled = false;
context.Configuration.AutoDetectChangesEnabled = false;
//await Context.SaveChangesAsync();
context.SaveChanges();
dbContextTransaction.Commit();
//await Task.Run(async () => await Context.SaveChangesAsync());
context.Configuration.ValidateOnSaveEnabled = true;
context.Configuration.AutoDetectChangesEnabled = true;
context.Configuration.LazyLoadingEnabled = false;
agreementId = agreement.Id;
}
catch (Exception ex)
{
dbContextTransaction.Rollback();
throw ex;
}
}
GdDbConfiguration.SuspendExecutionStrategy = false;
}
return agreementId;
}
but it's taking as much time as before

You can follow below mentioned suggestions to improve the performance of above methods.
Use FirstOrDefault() instead of SingleOrDefault().FirstOrDefault() is the fastest method.
I can see that you have used Context.SaveChanges() method number of times on the same method.That will degrade the performnce of the method.So you must avoid that.Instead of use Transactions.
Like this : EF Transactions
using (var context = new YourContext())
{
using (var dbContextTransaction = context.Database.BeginTransaction())
{
try
{
// your operations here
context.SaveChanges(); //this called only once
dbContextTransaction.Commit();
}
catch (Exception)
{
dbContextTransaction.Rollback();
}
}
}
You can think about the implementaion of stored procedure if above will not give the enough improvement.

There are some performance issues with your code
Add Performance
foreach (var personAgreementRelation in personAgreementRelations)
{
Context.PersonAgreementRelations.Add(personAgreementRelation);
}
Context.Configuration.ValidateOnSaveEnabled = false;
Context.Configuration.AutoDetectChangesEnabled = false;
You add multiple entities then disabled AutoDetectChanges. You normally do the inverse
Depending on the number of entities in your context, it can severely hurt your performance
In the method "AddFirstImageSearchAppointmentToAgreement", it seems you use an outside context which can be very bad if it contains already multiple thousands of entities.
See: Improve Entity Framework Add Performance
Badly used, adding an entity to the context with the Add method take more time than saving it in the database!
SaveChanges vs. Bulk Insert vs. BulkSaveChanges
SaveChanges is very slow. For every record to save, a database round-trip is required. This is particularly the case for SQL Azure user because of the extra latency.
Some library allows you to perform Bulk Insert
See:
Entity Framework Bulk Insert Library
Entity Framework Bulk SaveChanges Library
Disclaimer: I'm the owner of the project Entity Framework Extensions
This library has a BulkSaveChanges features. It works exactly like SaveChanges but WAY FASTER!
// Context.SaveChanges();
Context.BulkSaveChanges();
EDIT: ADD additional information #1
I pasted my new code in Pastebin: link
Transaction
Why starting a transaction when you select your data and add entities to your context? It simply a VERY bad use of a transaction.
A transaction must be started as late as possible. In since BulkSaveChanges is already executed within a transaction, there is no point to create it.
Async.Result
var personAdminId = context.Users.FirstOrDefaultAsync(x => x.Email == adminEmail).Result.PersonId;
I don't understand why you are using an async method here...
In best case, you get similar performance as using non-async method
In worse case, you suffer from some performance issue with async method
Cache Item
var adminEmail = ConfigurationManager.AppSettings["DefaultGdAdminEmail"];
var personAdminId = context.Users.FirstOrDefaultAsync(x => x.Email == adminEmail).Result.PersonId;
I don't know how many time you call the AddAgreement method, but I doubt the admin will change.
So if you call it 10,000 times, you make 10,000 database round-trip to get the same exact value every time.
Create a static variable instead and get the value only once! You will for sure save a lot of time here
Here is how I normally handle static variable of this kind:
var personAdminId = My.UserAdmin.Id;
public static class My
{
private static User _userAdmin;
public static User UserAdmin
{
get
{
if (_userAdmin == null)
{
using (var context = new GdDbContext())
{
var adminEmail = ConfigurationManager.AppSettings["DefaultGdAdminEmail"];
_userAdmin = context.Users.FirstOrDefault(x => x.Email == adminEmail);
}
}
return _userAdmin;
}
}
}
LazyLoadingEnabled
In the first code, you have LazyLoadingEnabled to false but not in your Pastebin code,
Disabling LazyLoading can help a little bit since it will not create a proxy instance.
Take 10m instead of 9m
Let me know after removing the transaction and disabling again LazyLoading if the performance is a little bit better.
The next step will be to know some statistics:
Around how many time the AddAgreement method is invoked
Around how many persons do you have in your database
Around how many entities in average is Saved by the AddAgreement method
EDIT: ADD additional information #2
Currently, the only way to improve really the performance is by reducing the number of database round-trip.
I see you are still searching the personAdminId every time. You could save maybe 30s to 1 minute just here by caching this value somewhere like a static variable.
You still have not answered the three questions:
Around how many time the AddAgreement method is invoked
Around how many persons do you have in your database
Around how many entities in average is Saved by the AddAgreement method
The goal of theses questions is to understand what's slow!
By example, if you call the AddAgreement method 10,000 times and you only have 2000 persons in the database, you are probably better to cache in two dictionary theses 2000 persons to save 20,000 database round-trip (Saving one to two minutes?).

Related

How to load a related entity after call AddAsync without making another roundtrip to the database

How to load a related entity after calling AddAsync?
I have a repository method that looks like this
public virtual async Task<TEntity> AddAsync(TEntity entity)
{
if (entity == null)
throw new ArgumentNullException(nameof(entity));
try
{
entity.CreatedOn = entity.UpdatedOn = DateTime.Now;
var newEntity = await Entities.AddAsync(entity);
var newEntityToRet = newEntity.Entity;
_context.SaveChanges();
return newEntityToRet;
}
catch (DbUpdateException exception)
{
//ensure that the detailed error text is saved in the Log
throw new Exception(GetFullErrorTextAndRollbackEntityChanges(exception), exception);
}
}
Trying to insert an Order for example that looks like this, and only passing the StatusId and the TradingActionId makes the Add safe
public class Order
{
public int Id { get; set; }
public bool IsDeleted { get; set; }
public string CreatedBy { get; set; }
public string UpdatedBy { get; set; }
public DateTime? CreatedOn { get; set; }
public DateTime? UpdatedOn { get; set; }
public string Symbol { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
public int StatusId { get; set; }
public OrderStatus Status { get; set; }
public int TradingActionId { get; set; }
public TradingAction TradingAction { get; set; }
public string Notes { get; set; }
}
var order = new Order
{
TradingActionId = 1,
StatusId = 1,
Notes = source.Notes,
Price = source.Price,
Symbol = source.Symbol,
Quantity = source.Quantity,
CreatedOn = dateTimeNow,
UpdatedOn = dateTimeNow,
UpdatedBy = "test",
CreatedBy = "test"
};
The problem with this is that if I need to return the new entity with certain navigation properties. My following approach doesn't work but shows the idea of what I need to save the instance and at the same time return the child properties.
public virtual async Task<TEntity> AddAsync(TEntity entity, string[] include = null)
{
if (entity == null)
throw new ArgumentNullException(nameof(entity));
try
{
entity.CreatedOn = entity.UpdatedOn = DateTime.Now;
var newEntity = await Entities.AddAsync(entity);
var newEntityToRet = newEntity.Entity;
_context.SaveChanges();
if(include != null)
{
foreach (var navProp in include)
{
try
{
var memberEntry = _context.Entry(newEntityToRet).Member(navProp);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
if (memberEntry is DbCollectionEntry collectionMember)
collectionMember.Load();
if (memberEntry is DbReferenceEntry referenceMember)
referenceMember.Load();
}
}
return newEntityToRet;
}
catch (DbUpdateException exception)
{
//ensure that the detailed error text is saved in the Log
throw new Exception(GetFullErrorTextAndRollbackEntityChanges(exception), exception);
}
}
The version I am using of EF Core is Microsoft.EntityFrameworkCore.SqlServer 3.1.4
Any suggestions of how to create the generic repo method and return the data needed without making another roundtrip to the database?
After inserting a new entity you can always load its related entities explicitly with the Load() or LoadAsync() method. But even though it doesn't execute a typical LINQ method/query (like the one you would write to fetch some related data), EF needs to submit new query to the database for each explicit loading. Therefore, it really doesn't save you any database trip.
Since you are already trying to use the Load() method in your code, I'm assuming your intention is just to avoid writing the new LINQ queries (required to fetch the related data), and not to avoid a database trip. If that is the case, you can try something like bellow -
public async Task<TEntity> AddAsync(TEntity entity, params string[] includes)
{
entity.CreatedOn = entity.UpdatedOn = DateTime.Now;
var newEntry = Entities.Add(entity);
await _context.SaveChangesAsync(); // trip to database
foreach (var navProp in includes)
{
if (newEntry.Navigation(navProp).Metadata.IsCollection())
{
await newEntry.Collection(navProp).LoadAsync(); // trip to database
}
else
{
await newEntry.Reference(navProp).LoadAsync(); // trip to database
}
}
return newEntry.Entity;
}
which you can use like -
var addedOrder = await orderRepository.AddAsync(order, "Status", "TradingAction");
Notice -
I'm using params to pass one or more parameters
To add/insert a new entity use the Add or Attach method. Unless you are dealing with value generators like SequenceHiLo, you really don't need to use the AddAsync method. For details - AddAsync<TEntity>
A type-safe implementation would be -
public async Task<TEntity> AddAsync(TEntity entity, params Expression<Func<TEntity, object>>[] includes)
{
entity.CreatedOn = entity.UpdatedOn = DateTime.Now;
var newEntry = Entities.Add(entity);
await _context.SaveChangesAsync(); // trip to database
foreach (var navProp in includes)
{
string propertyName = navProp.GetPropertyAccess().Name;
if (newEntry.Navigation(propertyName).Metadata.IsCollection())
{
await newEntry.Collection(propertyName).LoadAsync(); // trip to database
}
else
{
await newEntry.Reference(propertyName).LoadAsync(); // trip to database
}
}
return newEntry.Entity;
}
so that you can pass the navigation properties like -
var addedOrder = await orderRepository.AddAsync(order, p => p.Status, p => p.TradingAction);

Map list manually from context

Initially I was using automapper for this but its seems way harder for me to implement it.
Basically, I just want to return an empty list instead of null values. I can do this on projects level but not on teammates level. The API must not return a null because the UI that consumes it will have an error.
Sample of my implementation below:
Projects = !Util.IsNullOrEmpty(x.Projects) ? x.Projects : new List<ProjectsDto>(),
Ill highly appreciate if someone can guide me on how to manually map this with null/empty checking.
If you can also provide and example using automapper that too will be very helpful.
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
public List<ProjectsDto> Projects { get; set; }
}
public class ProjectsDto
{
public string Status { get; set; }
public List<TeammatesDto> Teammates { get; set; }
}
public class TeammatesDto
{
public string TeammateName { get; set; }
public string PreviousProject { get; set; }
}
//Get by Id
var employee = await _context.Employees
.Where(x => x.id.Equals(request.Id)
.FirstOrDefaultAsync(cancellationToken);
//Map employee
EmployeeDto ret = new EmployeeDto()
{
Id = employee.id,
Name = employee.Name,
Projects = null //TODO: map manually
}
//Get all employees
var employees = await _context.Employees.AsNoTracking()
.ToListAsync(cancellationToken);
//Map here
IList<EmployeeDto> list = new List<EmployeeDto>();
foreach (var x in employees)
{
EmployeeDto dto = new EmployeeDto()
{
Id = x.id,
Name = x.Name,
Projects = null //TODO: map manually
};
list.Add(dto);
}
return list;
Instead of materializing full entities, do the following:
var query = _context.Employees
.Select(e = new EmployeeDto
{
Id = e.id,
Name = e.Name,
Projects = e.Projects.Select(p => new ProjectDto
{
Status = p.Status,
Templates = p.Templates.Select(t => new TemplateDto
{
TeammateName = t.TeammateName,
PreviousProject = t.PreviousProject
}).ToList()
}).ToList()
}
);
var result = await query.ToListAsync();

Entity Framework - Duplicate entry '1' for key 'PRIMARY'"

Using MySql DB and Entity Framework, when im trying insert a data that contais a lists of child data i recive this error: InnerException = {"Duplicate entry '1' for key 'PRIMARY'"}
Here is a image of my tables: https://i.stack.imgur.com/bAnVy.png
This is my Models:
public class Etapa
{
public int Id { get; set; }
public string Descricao { get; set; }
[Column("ativo", TypeName = "bit")]
public bool Ativo { get; set; }
[Column("finalizadora", TypeName = "bit")]
public bool Finalizadora { get; set; }
public List<EtapaVinculada> ListaEtapaVinculada { get; set; }
}
[Table("etapa_vinculada")]
public class EtapaVinculada
{
public int Id { get; set; }
[Column("id_etapa_pai")]
public int EtapaPaiId { get; set; }
public Etapa EtapaPai { get; set; }
[Column("id_etapa_filha")]
public int EtapaFilhaId { get; set; }
public Etapa EtapaFilha { get; set; }
public string Descricao { get; set; }
}
Etapa's contexts is here:
public class ContextoEtapa : Contexto
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Etapa>().HasMany(x => x.ListaEtapaVinculada).WithOne(y => y.EtapaPai).HasForeignKey(x => x.EtapaPaiId);
}
public async Task Adicionar(Etapa registro)
{
await AddAsync(registro);
await SaveChangesAsync();
}
}
Filling manually the tables in DB, when i debug my context i can see my Etapa object is filled and the property ListEtapaVinculada is filled too, correctly.
The problem happens when the Etapa object filled with its list of EtapaVinculada is going to be inserted into the database using the Add method. It seems to me that I did some wrong mapping, because it gives the impression that Entity tries to insert 2x the Etapa record in a row, falling into the duplicate key error.
The auto increment is working. If i try to save a object like this:
{
Etapa etapa = new Etapa();
etapa.Descricao = "test";
etapa.Ativo = true;
etapa.Finalizadora = true;
etapa.ListaEtapaVinculada = new List<EtapaVinculada>(); // Without itens
using (var context = new ContextoEtapa())
{
await context.Etapa.AddAsync(etapa);
await context.SaveChangesAsync();
}
}
But, if i do something like this:
{
Etapa etapaFilha = null;
using (var context = new ContextEtapa())
{
etapaFilha = await context.Etapa.Where(x => x.Id == 666).First();
}
Etapa etapa = new Etapa();
etapa.Descricao = "test";
etapa.Ativo = true;
etapa.Finalizadora = true;
etapa.ListaEtapaVinculada = new List<EtapaVinculada>();
EtapaVinculada etapaVinculada = new EtapaVinculada();
etapaVinculada.EtapaPaiId = etapa.Id;
etapaVinculada.EtapaPai = etapa;
etapaVinculada.EtapaFilhaId = etapaFilha.Id;
etapaVinculada.EtapaFilha = etapaFilha;
etapa.listaEtapaVinculada.Add(etapaVinculada);
using (var context = new ContextoEtapa())
{
await context.Etapa.AddAsync(etapa);
await context.SaveChangesAsync();
}
}
Now i got the erros of duplicate Key. Its seems to me that EF is trying to insert 2x Etapa object, when the correct is insert Etapa, then insert all itens of ListaEtapaVinculada.
I think the problem is when trying to assign an instance of the object and an id at the same time, try commenting on the following line of code:
{
Etapa etapaFilha = null;
using (var context = new ContextEtapa())
{
etapaFilha = await context.Etapa.Where(x => x.Id == 666).First();
}
Etapa etapa = new Etapa();
etapa.Descricao = "test";
etapa.Ativo = true;
etapa.Finalizadora = true;
etapa.ListaEtapaVinculada = new List<EtapaVinculada>();
EtapaVinculada etapaVinculada = new EtapaVinculada();
// etapaVinculada.EtapaPaiId = etapa.Id; // this is asigned when asign to collection and savechanges
// etapaVinculada.EtapaPai = etapa;
etapaVinculada.EtapaFilhaId = etapaFilha.Id; //
// etapaVinculada.EtapaFilha = etapaFilha; this is duplicate
etapa.listaEtapaVinculada.Add(etapaVinculada);
using (var context = new ContextoEtapa())
{
await context.Etapa.AddAsync(etapa);
await context.SaveChangesAsync();
}
}

How to return model with virtual property

I have a model like this,
[Table("ClientAccessories")]
public class ClientAccessory
{
public ClientAccessory()
{
LastModifiedDateTime = DateTime.UtcNow;
}
public string AccessoryId { get; set; }
public Guid ClientReference { get; set; }
public DateTime LastModifiedDateTime { get; set; }
public bool IsActive { get; set; }
public virtual Accessory Accessory { get; set; }
}
and I have this code in repository method,
public IEnumerable<ClientAccessory> GetClientAccessories(Guid ClientReference)
{
var _context = new DBContext();
var results = from a in _context.Accessories
join ca in _context.ClientAccessories
on new { AccessoryId = a.Id, ClientReference = new Guid(ClientReference) }
equals new { ca.AccessoryId, ca.ClientReference } into ca_join
from ca in ca_join.DefaultIfEmpty()
where
ca.IsActive == true ||
ca.IsActive == null
select new {};
}
Now problem is that, I am not sure how to return ClientAccessory including Accessory object together even though it's a virtual property.
Also Is it Okay to call 2 entities in one repository or should I return IQueryable and do it in domain service class. thank you.
I don't want to flat the values like this,
select new {
Id = a.Id,
ClientReference = ca.ClientReference
and so on...
};
if you query from ClientAccessory and include the Accessory, you should get what you what. Something like this:
public IEnumerable<ClientAccessory> GetClientAccessories(Guid ClientReference)
{
var _context = new DBContext();
var results = from ca in _context.ClientAccessory.Include("Accessory")
where ca.IsActive == true || ca.IsActive == null
select ca;
return results;
}

Trying to get arround "Multiplicity constraint violated." / SaveChanges is Changing Values

I'm trying to overcome a "Multiplicity constraint violated." error. I have created a simple / contrived example to demonstrate the issue. In this example, I have a Task, that has a collection of sub-tasks and Task can be a sub-task of one or more Tasks. I want to be able to order the sub-tasks. I'm open to other suggestions on how to have a many-to-many relationship that also keeps track of the order.
There are 3 Test bellow all have different problems. The one that I find the most interesting is the third test where the OrderedTask is created and the values are correct until the value for TaskId is changed someplace inside of 'context.SaveChanges()'
Solution on Git: https://github.com/jrswenson/OrderedManyToMany
public class Task
{
private ICollection<OrderedTask> subTasks;
public Task()
{
subTasks = new List<OrderedTask>();
}
public int Id { get; set; }
public string Description { get; set; }
public virtual User AssignedUser { get; set; }
public int? AssignedUserId { get; set; }
[InverseProperty("Parent")]
public virtual ICollection<OrderedTask> SubTasks
{
get { return subTasks; }
set { subTasks = value; }
}
}
public class OrderedTask
{
public virtual Task Parent { get; set; }
[Key, Column(Order = 1), ForeignKey("Parent")]
public int ParentId { get; set; }
public virtual Task Task { get; set; }
[Key, Column(Order = 2), ForeignKey("Task")]
public int TaskId { get; set; }
public int Order { get; set; }
}
In a Unit Test:
[TestClass]
public class TaskTest
{
[TestMethod]
public void CreateTasks()
{
var context = new Context();
var task = context.Tasks.FirstOrDefault(i => i.Description == "Lev1") ?? new Task { Description = "Lev1" };
if (context.Tasks.Any(i => i.Id == task.Id) == false)
context.Tasks.Add(task);
var sub1 = context.Tasks.FirstOrDefault(i => i.Description == "Lev2-1") ?? new Task { Description = "Lev2-1" };
if (context.Tasks.Any(i => i.Id == sub1.Id) == false)
context.Tasks.Add(sub1);
context.SaveChanges();
}
//This will throw a "Multiplicity constraint violated" error
[TestMethod]
public void InsertSubTasks()
{
var context = new Context();
var task = context.Tasks.FirstOrDefault(i => i.Description == "Lev1");
Assert.IsNotNull(task);
var sub1 = context.Tasks.FirstOrDefault(i => i.Description == "Lev2-1");
Assert.IsNotNull(sub1);
if (task.SubTasks.Any(i => i.TaskId == sub1.Id) == false)
{
var ot = new OrderedTask { Parent = task, Task = sub1, Order = task.SubTasks.Count + 1 };
task.SubTasks.Add(ot);
}
context.SaveChanges();
}
//This doesn't throw an exception.
//The OrderedTask is added to the database and
//the table has the correct values.
//Unfortunately, if it this is ran a second time,
//task.SubTasks is empty and causes a duplicate key error.
[TestMethod]
public void InsertSubTasks2()
{
var context = new Context();
var task = context.Tasks.FirstOrDefault(i => i.Description == "Lev1");
Assert.IsNotNull(task);
var sub1 = context.Tasks.FirstOrDefault(i => i.Description == "Lev2-1");
Assert.IsNotNull(sub1);
if (task.SubTasks.Any(i => i.TaskId == sub1.Id) == false)
{
var ot = new OrderedTask { Parent = task, Task = sub1, Order = task.SubTasks.Count + 1 };
context.OrderedTasks.Add(ot);
}
context.SaveChanges();
}
//This doesn't throw an exception the first time, but does on the
//second time.
//The OrderedTask is created and has the correct values after it is
//added to task.SubTasks, but somewhere in context.SaveChanges the value
//for ParentId and TaskId are both set to the same value of ParentId. The
//second time the test is ran task.SubTasks has a value (unlike the test above)
//, but the values are not correct.
[TestMethod]
public void InsertSubTasks3()
{
var context = new Context();
var task = context.Tasks.FirstOrDefault(i => i.Description == "Lev1");
Assert.IsNotNull(task);
var sub1 = context.Tasks.FirstOrDefault(i => i.Description == "Lev2-1");
Assert.IsNotNull(sub1);
if (task.SubTasks.Any(i => i.TaskId == sub1.Id) == false)
{
var ot = new OrderedTask { Parent = task, Task = sub1, Order = task.SubTasks.Count + 1 };
context.OrderedTasks.Add(ot);
task.SubTasks.Add(ot);
}
context.SaveChanges();
}
}

Categories

Resources