J'essaie de mettre à jour la collection imbriquée (Villes) de l'entité Pays.
De simples entités et dto simples:
// EF Models
public class Country
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<City> Cities { get; set; }
}
public class City
{
public int Id { get; set; }
public string Name { get; set; }
public int CountryId { get; set; }
public int? Population { get; set; }
public virtual Country Country { get; set; }
}
// DTo's
public class CountryData : IDTO
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<CityData> Cities { get; set; }
}
public class CityData : IDTO
{
public int Id { get; set; }
public string Name { get; set; }
public int CountryId { get; set; }
public int? Population { get; set; }
}
Et le code lui-même (testé dans l'application console par souci de simplicité):
using (var context = new Context())
{
// getting entity from db, reflect it to dto
var countryDTO = context.Countries.FirstOrDefault(x => x.Id == 1).ToDTO<CountryData>();
// add new city to dto
countryDTO.Cities.Add(new CityData
{
CountryId = countryDTO.Id,
Name = "new city",
Population = 100000
});
// change existing city name
countryDTO.Cities.FirstOrDefault(x => x.Id == 4).Name = "another name";
// retrieving original entity from db
var country = context.Countries.FirstOrDefault(x => x.Id == 1);
// mapping
AutoMapper.Mapper.Map(countryDTO, country);
// save and expecting ef to recognize changes
context.SaveChanges();
}
Ce code lève une exception:
L'opération a échoué: la relation n'a pas pu être modifiée car une ou plusieurs des propriétés de clé étrangère ne peuvent pas être annulées. Lorsqu'une modification est apportée à une relation, la propriété de clé étrangère associée est définie sur une valeur nulle. Si la clé étrangère ne prend pas en charge les valeurs nulles, une nouvelle relation doit être définie, la propriété de clé étrangère doit être affectée à une autre valeur non nulle ou l'objet non lié doit être supprimé.
même si l'entité après le dernier mappage semble très bien et reflète correctement tous les changements.
J'ai passé beaucoup de temps à trouver une solution, mais je n'ai obtenu aucun résultat. Veuillez aider.
Le problème est que le country
que vous récupérez de la base de données contient déjà certaines villes. Lorsque vous utilisez AutoMapper comme ceci:
// mapping
AutoMapper.Mapper.Map(countryDTO, country);
AutoMapper fait quelque chose comme créer correctement un IColletion<City>
(Avec une ville dans votre exemple) et attribuer cette toute nouvelle collection à votre propriété country.Cities
.
Le problème est que EntityFramework ne sait pas quoi faire avec l'ancienne collection de villes.
En fait, EF ne peut pas décider pour vous. Si vous souhaitez continuer à utiliser AutoMapper, vous pouvez personnaliser votre mappage comme suit:
// AutoMapper Profile
public class MyProfile : Profile
{
protected override void Configure()
{
Mapper.CreateMap<CountryData, Country>()
.ForMember(d => d.Cities, opt => opt.Ignore())
.AfterMap(AddOrUpdateCities);
}
private void AddOrUpdateCities(CountryData dto, Country country)
{
foreach (var cityDTO in dto.Cities)
{
if (cityDTO.Id == 0)
{
country.Cities.Add(Mapper.Map<City>(cityDTO));
}
else
{
Mapper.Map(cityDTO, country.Cities.SingleOrDefault(c => c.Id == cityDTO.Id));
}
}
}
}
La configuration Ignore()
utilisée pour Cities
fait que AutoMapper conserve simplement la référence de proxy d'origine construite par EntityFramework
.
Ensuite, nous utilisons simplement AfterMap()
pour appeler une action faisant exactement ce que vous pensez:
Map
où nous passons l'entité existante en tant que deuxième paramètre et le proxy de ville en tant que premier paramètre, de sorte que le mappeur automatique met simplement à jour les propriétés de l'entité existante.Ensuite, vous pouvez conserver votre code d'origine:
using (var context = new Context())
{
// getting entity from db, reflect it to dto
var countryDTO = context.Countries.FirstOrDefault(x => x.Id == 1).ToDTO<CountryData>();
// add new city to dto
countryDTO.Cities.Add(new CityData
{
CountryId = countryDTO.Id,
Name = "new city",
Population = 100000
});
// change existing city name
countryDTO.Cities.FirstOrDefault(x => x.Id == 4).Name = "another name";
// retrieving original entity from db
var country = context.Countries.FirstOrDefault(x => x.Id == 1);
// mapping
AutoMapper.Mapper.Map(countryDTO, country);
// save and expecting ef to recognize changes
context.SaveChanges();
}
Ce n'est pas une réponse en soi à l'OP, mais quiconque regarde un problème similaire aujourd'hui devrait envisager d'utiliser AutoMapper.Collection . Il prend en charge ces problèmes de collecte parent-enfant qui nécessitaient auparavant beaucoup de code à gérer.
Je m'excuse de ne pas avoir inclus une bonne solution ou plus de détails, mais je ne fais que me mettre au courant maintenant. Il y a un excellent exemple simple directement dans le fichier README.md affiché sur le lien ci-dessus.
Utiliser cela nécessite un peu de réécriture, mais cela de façon drastique réduit la quantité de code que vous devez écrire, surtout si vous utilisez EF et peut utiliser AutoMapper.Collection.EntityFramework
.
lors de l'enregistrement des modifications, toutes les villes sont considérées comme ajoutées, car EF ne les a pas étudiées avant de gagner du temps. EF essaie donc de définir null sur la clé étrangère de la vieille ville et de l'insérer au lieu de la mettre à jour.
en utilisant ChangeTracker.Entries()
vous découvrirez quels changements CRUD va être fait par EF.
Si vous souhaitez simplement mettre à jour la ville existante manuellement, vous pouvez simplement faire:
foreach (var city in country.cities)
{
context.Cities.Attach(city);
context.Entry(city).State = EntityState.Modified;
}
context.SaveChanges();
Très bonne solution d'Alisson. Voici ma solution ... Comme nous le savons, EF ne sait pas si la demande concerne la mise à jour ou l'insertion, alors je voudrais d'abord supprimer avec la méthode RemoveRange () et envoyer la collection pour l'insérer à nouveau. En arrière-plan, voici comment fonctionne la base de données, nous pouvons émuler ce comportement manuellement.
Voici le code:
//country object from request for example
var cities = dbcontext.Cities.Where(x=>x.countryId == country.Id);
dbcontext.Cities.RemoveRange(cities);
/* Now make the mappings and send the object this will make bulk insert into the table related */
Il semble que j'ai trouvé une solution:
var countryDTO = context.Countries.FirstOrDefault(x => x.Id == 1).ToDTO<CountryData>();
countryDTO.Cities.Add(new CityData { CountryId = countryDTO.Id, Name = "new city 2", Population = 100000 });
countryDTO.Cities.FirstOrDefault(x => x.Id == 11).Name = "another name";
var country = context.Countries.FirstOrDefault(x => x.Id == 1);
foreach (var cityDTO in countryDTO.Cities)
{
if (cityDTO.Id == 0)
{
country.Cities.Add(cityDTO.ToEntity<City>());
}
else
{
AutoMapper.Mapper.Map(cityDTO, country.Cities.SingleOrDefault(c => c.Id == cityDTO.Id));
}
}
AutoMapper.Mapper.Map(countryDTO, country);
context.SaveChanges();
ce code met à jour les éléments modifiés et en ajoute de nouveaux. Mais peut-être qu'il y a des pièges que je ne peux pas détecter pour l'instant?