web-dev-qa-db-fra.com

EF et Automapper. Mettre à jour les collections imbriquées

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.

14
Akmal Salikhov

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.

  • Doit-elle supprimer vos anciennes villes et n'assumer que la nouvelle collection?
  • Doit-il simplement fusionner les deux listes et conserver les deux dans la base de données?

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:

  • Pour les nouvelles villes, nous mappons de DTO à Entity (AutoMapper crée une nouvelle instance) et l'ajoutons à la collection du pays.
  • Pour les villes existantes, nous utilisons une surcharge de 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();
    }
29
Alisson

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.

3
pbarranis

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

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 */

0
Raul Gonzalez

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?

0
Akmal Salikhov