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

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

Related

Updating partial properties of an entity EF Core .net 5

Hello I have a class and I would like to update some of the properties using ef core
I have the class customer
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Address { get; set; }
public DateTime RegisterDate { get; set; }
}
And in the persistence layer
the first method
using var db = new CustomerDbContext(_options);
var entry = db.Customers.Attach(modification.Customer);
entry.Property(customer => customer.Name).IsModified = true;
entry.Property(customer => customer.Email).IsModified = true;
entry.Property(customer => customer.Address).IsModified = true;
await db.SaveChangesAsync();
the second method
using var db = new CustomerDbContext(_options);
var customerEntry = db.Entry(modification.Customer);
customerEntry.Property(customer => customer.Name).IsModified = true;
customerEntry.Property(customer => customer.Email).IsModified = true;
customerEntry.Property(customer => customer.Address).IsModified = true;
await db.SaveChangesAsync();
I try to understand what is the difference between them and what is the most efficient way?
Is there any else?

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

"Violation of PRIMARY KEY constraint '...'. Cannot insert duplicate key in object

I just started to make my first project with Codefirst-Approach with C#, Linq and MSSQLSERVER and run into an problem when trying to insert a new DB-entry that contains an reference to an already existing element from another table.
InnerException {"Violation of PRIMARY KEY constraint 'PK_dbo.Manufacturers'. Cannot insert duplicate key in object 'dbo.Manufacturers'. The duplicate key value is (1d262e43-b9b6-4752-9c79-95d955d460ab).\r\nThe statement has been terminated."} System.Exception {System.Data.SqlClient.SqlException}
I broke the problem down to a simple project that I will upload to a share.
My data structure contains a class.Product that links to a Manufacturer object and a List of possible Suppliers.
public class Product
{
[Key]
public Guid Id { get { return _id; } set { _id = value; } }
private Guid _id = Guid.NewGuid();
public string Name { get; set; }
public Manufacturer Manuf { get; set; }
public List<Supplier> PossibleSupplier { get { return _possibleSupplier; } set { _possibleSupplier = value; } }
private List<Supplier> _possibleSupplier = new List<Supplier>();
}
public class Supplier
{
[Key]
public Guid Id { get { return _id; } set { _id = value; } }
private Guid _id = Guid.NewGuid();
public string Name { get; set; }
}
public class Manufacturer
{
[Key]
public Guid Id { get { return _id; } set { _id = value; } }
private Guid _id = Guid.NewGuid();
public string Name { get; set; }
}
I now generate 2 products.
Both products are produced by the same manufacturer.
The List of PossibleSuppliers does also contain same suppliers
private void GenerateProducts()
{
Manufacturer manufactuer1 = new Manufacturer() { Name = "mainManuf 1" };
Supplier supplier1 = new Supplier() { Name = "first Supplier" };
Supplier supplier2 = new Supplier() { Name = "second Supplier" };
Product firstProduct = new Product() { Name = "Product 1", Manuf = manufactuer1, PossibleSupplier = new List<Supplier>() { supplier1, supplier2 } };
Product secondProduct = new Product() { Name = "Product 2", Manuf = manufactuer1, PossibleSupplier = new List<Supplier>() { supplier1 } };
productList_ = new List<Product>() { firstProduct, secondProduct };
}
The following method is used for storing/updating the DB entries
public static class DbHandler
{
public static bool StoreProduct(Product product)
{
using (ProductDbContext dbObject = new ProductDbContext())
{
try
{
dbObject.Products.AddOrUpdate(product);
dbObject.SaveChanges();
}
catch (Exception ex)
{
//
return false;
}
}
return true;
}
}
public class ProductDbContext : DbContext
{
public ProductDbContext()
{
Database.SetInitializer<ProductDbContext>(new DropCreateDatabaseAlways<ProductDbContext>());
this.Database.Connection.ConnectionString = sqlConnection.ConnectionString;
}
public DbSet<Product> Products { get; set; }
public DbSet<Supplier> Suppliers { get; set; }
public DbSet<Manufacturer> Manufacturers { get; set; }
private static SqlConnectionStringBuilder sqlConnection = new SqlConnectionStringBuilder()
{
DataSource = "localhost\\MSSQLSERVER2019", // update me
UserID = "", // update me
Password = "", // update me
InitialCatalog = "ProductDb",
IntegratedSecurity = true
};
}
The insertion of the first product can be done without problems.
Also inserting additional products that will have unique manufacturers and suppliers will work without problem.
**So I do not have the problem of uniqueness of my primary keys. **
I only receive this error, when I like to add a new entry that has a foreign key to an already existing entry.
Using dbObject.Products.AddOrUpdate(product); instead of dbObject.Products.Add(product); have not solved my problem.
I am also not able to remove the manufacturer entry before adding the second product, because this will violate the foreign key of my first product…
I found a possible solution for manufacturer by adding an additional property for ManufacturerId
public Guid? ManuId { get; set; }
[ForeignKey("ManuId")]
public Manufacturer Manuf { get; set; }
to my data object, but I would not have an idea how to do this with my List PossibleSupplier??
Can someone please push me into the right direction?
!!Many thanks for the fast replays!!
I have updated my DataStructure as following:
public class Product
{
[Key]
public Guid Id { get { return _id; } set { _id = value; } }
private Guid _id = Guid.NewGuid();
public string Name { get; set; }
public virtual Manufacturer Manufacturer { get; set; }
public virtual ICollection<Supplier> PossibleSupplier { get; set; }
}
public class Supplier
{
[Key]
public Guid Id { get { return _id; } set { _id = value; } }
private Guid _id = Guid.NewGuid();
public string Name { get; set; }
[ForeignKey("Product")]
public Guid ProductId { get; set; }
public virtual Product Product { get; set; }
}
public class Manufacturer
{
[Key]
public Guid Id { get { return _id; } set { _id = value; } }
private Guid _id = Guid.NewGuid();
public string Name { get; set; }
[ForeignKey("Product")]
public Guid ProductId { get; set; }
public virtual ICollection<Product> Product { get; set; }
}
But I still get the "Violation of PRIMARY KEY constraint 'PK_dbo.Manufacturers'. Cannot insert duplicate key..." error while trying to insert the second entry.
I have attached how the DB looks in SQL-Server
Okay so I believe I know what your issue is. It lies somewhat with this portion here:
private void GenerateProducts()
{
Manufacturer manufactuer1 = new Manufacturer() { Name = "mainManuf 1" };
Supplier supplier1 = new Supplier() { Name = "first Supplier" };
Supplier supplier2 = new Supplier() { Name = "second Supplier" };
Product firstProduct = new Product() { Name = "Product 1", Manuf = manufactuer1, PossibleSupplier = new List<Supplier>() { supplier1, supplier2 } };
Product secondProduct = new Product() { Name = "Product 2", Manuf = manufactuer1, PossibleSupplier = new List<Supplier>() { supplier1 } };
productList_ = new List<Product>() { firstProduct, secondProduct };
}
When you assign Manuf = manufacturer1 in both portions below it will work for the first insert because the manufacturer does not exist yet. Now the reason why on the second insert it does not work is because of your code below:
using (ProductDbContext dbObject = new ProductDbContext())
{
try
{
dbObject.Products.AddOrUpdate(product);
dbObject.SaveChanges();
}
catch (Exception ex)
{
//
return false;
}
}
Right now when you go to insert the second product, it will throw the duplicate key exception because you are not referencing the existing entity within your context. You should change it to something like the following:
using (ProductDbContext dbObject = new ProductDbContext())
{
try
{
//Need to check if the manufacturer already exists in the db, if it does
//make sure your project references the EXISTING entity within your context
var check = dbObjec.Manufacturer.Where(x => x.Id == product.Manufacturer.Id).FirstOrDefault();
if (check != null)
product.Manufacturer = check;
dbObject.Products.Add(product);
dbObject.SaveChanges();
}
catch (Exception ex)
{
//
return false;
}
}
If you don't reference the existing manufacturer within the context and then assign it, EF will assume you are trying to add a new one not reference the existing one.
as mentioned in the commands I'd like to share my updated and working project for further use....
MainWindow that generates the test data and executes read/write to DB
public partial class MainWindow : Window
{
private List<Product> productList_;
public MainWindow()
{
GenerateProducts();
InitializeComponent();
}
private void InsertFirst_Click(object sender, RoutedEventArgs e)
{
DbHandler.StoreProduct(productList_[0]);
}
private void InsertSecond_Click(object sender, RoutedEventArgs e)
{
DbHandler.StoreProduct(productList_[1]);
}
private void Read_Click(object sender, RoutedEventArgs e)
{
var productList = DbHandler.GetAllProducts();
}
private void GenerateProducts()
{
Manufacturer manufactuer1 = new Manufacturer() { Name = "mainManuf 1" };
Supplier supplier1 = new Supplier() { Name = "first Supplier" };
Supplier supplier2 = new Supplier() { Name = "second Supplier" };
Supplier supplier3 = new Supplier() { Name = "third Supplier" };
Product firstProduct = new Product() { Name = "Product 1", Manufacturer = manufactuer1, PossibleSupplier = new List<Supplier>() { supplier1, supplier2 } };
Product secondProduct = new Product() { Name = "Product 2", Manufacturer = manufactuer1, PossibleSupplier = new List<Supplier>() { supplier2, supplier3 } };
productList_ = new List<Product>() { firstProduct, secondProduct };
}
}
DataStructure: Because of the many to many relation ship between Product and Supplier I have to add
[ForeignKey("Product")]
public ICollection<Guid> ProductId { get; set; }
public virtual ICollection<Product> Product { get; set; }
to the Supplier class. I also decided to add a collection of Products to my manufacturer to make some query-calls more comfortable
public class Product
{
[Key]
public Guid Id { get { return _id; } set { _id = value; } }
private Guid _id = Guid.NewGuid();
public string Name { get; set; }
public virtual Manufacturer Manufacturer { get; set; }
public virtual ICollection<Supplier> PossibleSupplier { get; set; }
}
public class Supplier
{
[Key]
public Guid Id { get { return _id; } set { _id = value; } }
private Guid _id = Guid.NewGuid();
public string Name { get; set; }
[ForeignKey("Product")]
public ICollection<Guid> ProductId { get; set; }
public virtual ICollection<Product> Product { get; set; }
}
public class Manufacturer
{
[Key]
public Guid Id { get { return _id; } set { _id = value; } }
private Guid _id = Guid.NewGuid();
public string Name { get; set; }
//only nice for reverse object from Man --> Product
[ForeignKey("Product")]
public ICollection<Guid> ProductId { get; set; }
public virtual ICollection<Product> Product { get; set; }
}
Before adding a new product to the DB it is important to load possible Manufacturer/Suppliers from the DB and assign them to the current product.
Adding new products is now working fine but as you can see the loading and assigning of the possible suppliers is not really handsome. Therefore I will try to make some modifications on this process in the upcoming days.... I will come back if I have found a "solution".
public static class DbHandler
{
public static List<Product> GetAllProducts()
{
using (ProductDbContext dbObject = new ProductDbContext())
{
//loading with childs and their reverse objects to products
var productList = dbObject.Products.Include("Manufacturer").Include("Manufacturer.Product").Include("PossibleSupplier").Include("PossibleSupplier.Product").Where(i => i.Id != null).ToList();
//loding with childs but without reverse objects
//var productList = dbObject.Products.Include("Manufacturer").Include("PossibleSupplier").Where(i => i.Id != null).ToList();
return productList;
}
}
public static bool StoreProduct(Product product)
{
using (ProductDbContext dbObject = new ProductDbContext())
{
try
{
//this does not solve the loading problem, even when property _id is changed to "private Guid _id = new Guid();
//dbObject.Entry(product).State = product.Id == new Guid() ? EntityState.Added : EntityState.Modified;
//dbObject.Entry(product.Manufacturer).State = product.Manufacturer.Id == new Guid() ? EntityState.Added : EntityState.Modified;
//foreach (var supplier in product.PossibleSupplier)
//{
// dbObject.Entry(supplier).State = supplier.Id == new Guid() ? EntityState.Added : EntityState.Modified;
//}
//Therefore loading must be done manually
Guid manufacturerId = product.Manufacturer.Id;
//Need to check if the manufacturer already exists in the db, if it does
//make sure your project references the EXISTING entity within your context
var checkManuf = dbObject.Manufacturers.Where(x => x.Id == manufacturerId).FirstOrDefault();
if (checkManuf != null)
product.Manufacturer = checkManuf;
List<Supplier> dbSuppliers = new List<Supplier>();
foreach (var posSupplier in product.PossibleSupplier)
{
var checkSupplier = dbObject.Suppliers.FirstOrDefault(x => x.Id == posSupplier.Id);
if (checkSupplier != null)
{
dbSuppliers.Add(checkSupplier);
}
}
foreach (var dbSup in dbSuppliers)
{
product.PossibleSupplier.Remove(product.PossibleSupplier.Single(i => i.Id == dbSup.Id));
product.PossibleSupplier.Add(dbSup);
}
dbObject.Products.Add(product);
dbObject.SaveChanges();
}
catch (Exception ex)
{
//
return false;
}
}
return true;
}
}
public class ProductDbContext : DbContext
{
public ProductDbContext()
{
Database.SetInitializer<ProductDbContext>(new DropCreateDatabaseIfModelChanges<ProductDbContext>());
this.Database.Connection.ConnectionString = sqlConnection.ConnectionString;
}
public DbSet<Product> Products { get; set; }
public DbSet<Supplier> Suppliers { get; set; }
public DbSet<Manufacturer> Manufacturers { get; set; }
private static SqlConnectionStringBuilder sqlConnection = new SqlConnectionStringBuilder()
{
DataSource = "localhost\\MSSQLSERVER2019", // update me
UserID = "", // update me
Password = "", // update me
InitialCatalog = "ProductDb",
IntegratedSecurity = true
};
}
One way seams by using EntityStates but they do not work as expected --> I received duplicated entries in DB.
I have uploaded the current state of the project to the share - filename SqlTestporject_20200414_2027.zip
Br,
-------------------------- Update 2020-04-15 --------------------------
I ended up by writing methods that are handling the decision between update/insert for every single child on its own because I do not have found a way how to update possible offline-changes of already existing dbEntries by simultaneously adding not existing dbEntries.
The main problem was that I received duplicated entries in the DB during adding the second product. The strange thing was, that this duplicates event violates the PK-uniqueness without an error/exception....
So I do have to call down the AddOrUpdate() methods until I reach the last child for my complete data structure.
public static Product AddOrUpdateProduct(Product product)
{
using (ProductDbContext dbObject = new ProductDbContext())
{
try
{
product.Manufacturer = AddOrUpdateManufacturer(dbObject, product.Manufacturer);
List<Supplier> dbSupplierList = new List<Supplier>();
foreach (var supplier in product.PossibleSupplier)
{
dbSupplierList.Add(AddOrUpdateSupplier(dbObject, supplier));
}
product.PossibleSupplier.Clear();
product.PossibleSupplier = dbSupplierList;
if (product.Id == new Guid())
{
//add new product
dbObject.Products.Add(product);
dbObject.SaveChanges();
return product;
}
else
{
//update existing product
var dbProduct = dbObject.Products.Single(x => x.Id == product.Id);
dbProduct.Name = product.Name;
dbObject.SaveChanges();
return dbProduct;
}
}
catch (Exception)
{
throw;
}
}
}
private static Supplier AddOrUpdateSupplier(ProductDbContext dbObject, Supplier supplier)
{
supplier.Address = AddOrUpdateAdress(dbObject, supplier.Address);
if (supplier.Id == new Guid())
{
//add new product
dbObject.Suppliers.Add(supplier);
dbObject.SaveChanges();
return supplier;
}
else
{
//update existing product
var dbSupplier = dbObject.Suppliers.Single(x => x.Id == supplier.Id);
dbSupplier.Name = supplier.Name;
dbObject.SaveChanges();
return dbSupplier;
}
}
private static Manufacturer AddOrUpdateManufacturer(ProductDbContext dbObject, Manufacturer manufacturer)
{
manufacturer.Address = AddOrUpdateAdress(dbObject, manufacturer.Address);
if (manufacturer.Id == new Guid())
{
//add new product
dbObject.Manufacturers.Add(manufacturer);
dbObject.SaveChanges();
return manufacturer;
}
else
{
//update existing product
var dbManufacturer = dbObject.Manufacturers.Single(x => x.Id == manufacturer.Id);
dbManufacturer.Name = manufacturer.Name;
dbObject.SaveChanges();
return dbManufacturer;
}
}
private static Address AddOrUpdateAdress(ProductDbContext dbObject, Address address)
{
if (address.Id == new Guid())
{
//add new product
dbObject.Addresses.Add(address);
dbObject.SaveChanges();
return address;
}
else
{
//update existing product
var dbAddress = dbObject.Addresses.Single(x => x.Id == address.Id);
dbAddress.Street = address.Street;
dbAddress.HouseNumber = address.HouseNumber;
dbAddress.PLZ = address.PLZ;
dbAddress.City = address.City;
dbObject.SaveChanges();
return dbAddress;
}
}
This version can be found here - file SqlTestporject_20200415_1033.zip.
Additionally I'd like to share the following link. Maybe chapter Example 4.18: Creating a Generic Method That Can Apply State Through Any Graph can help others to implement a more comfortable solution.

Insert relations very slow

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?).

How to perform addition, update operation on extra table in identity server?

I have added the following UserLog table in the database using asnetidentity but i don't know how to perform add,update operation on the table.
public class User : IdentityUser
{
public virtual ICollection<UserLog> UserLogs { get; set; }
}
public class Context : IdentityDbContext<User, Role, string, IdentityUserLogin, IdentityUserRole, IdentityUserClaim>
{
public Context(string connString)
: base(connString)
{
}
}
public class UserLog
{
[Key]
public Guid UserLogID { get; set; }
public string IPAD { get; set; }
public DateTime LoginDate { get; set; }
public string UserId { get; set; }
[ForeignKey("UserId")]
public virtual ApplicationUser User { get; set; }
}
public System.Data.Entity.DbSet<UserLog> UserLog { get; set; }
Like in the following code, I am using inbuilt methods of these aspnetidentity tables but how to add an entry in the "UserLog" table when user login every time in the following method?
public override async Task AuthenticateLocalAsync(LocalAuthenticationContext ctx)
{
var username = ctx.UserName;
var password = ctx.Password;
var message = ctx.SignInMessage;
ctx.AuthenticateResult = null;
if (userManager.SupportsUserPassword)
{
var user = await FindUserAsync(username);
if (user != null)
{
if (userManager.SupportsUserLockout &&
await userManager.IsLockedOutAsync(user.Id))
{
return;
}
if (await userManager.CheckPasswordAsync(user, password))
{
if (userManager.SupportsUserLockout)
{
await userManager.ResetAccessFailedCountAsync(user.Id);
}
var result = await PostAuthenticateLocalAsync(user, message);
if (result == null)
{
var claims = await GetClaimsForAuthenticateResult(user);
result = new AuthenticateResult(user.Id.ToString(), await GetDisplayNameForAccountAsync(user.Id), claims);
}
ctx.AuthenticateResult = result;
}
else if (userManager.SupportsUserLockout)
{
await userManager.AccessFailedAsync(user.Id);
}
}
}
}
Step 1: Create an instance of UserLog
var userLog = new UserLog() { /* Set value for all the properties here */ };
Step 2: Add instance of UserLog to DbContext
context.Set<UserLog>().Add(userLog);
Step 3: Call DbContext.SaveChanges() to save changes to database.
context.SaveChanges();
Complete source code will looks like:
var userLog = new UserLog {
UserLogID = Guid.NewGuid(),
IPAD = "Some Value",
LoginDate = DateTime.Now,
UserId = "User Id Here"
};
var context = new Context();
context.Set<UserLogs>().Add(userLog);
context.SaveChanges();

Categories

Resources