// ========================================================================
//
// Copyright (C) 2016-2017 MARC CHEVALDONNE
//                         marc.chevaldonne.free.fr
//
// Module        : Program.cs
// Author        : Marc Chevaldonné
// Creation date : 2016-11-12
//
// ========================================================================

using static System.Console;
using System.Linq;
using System;
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;

namespace ex_042_019_EF_concurrency_clientWins
{
    /// <summary>
    /// Cet exemple simule des mises à jours concurrentes de données dans la table.
    /// Contrairement à l'exemple 042_017, il montre comment il est possible de gérer facilement ces insertions concurrentes avec EntityFramework.
    /// Contrairement à l'exemple 042_018, il ne laisse pas la priorité à la base de données, mais au client,
    /// c'est-à-dire que si le client essaye de modifier les données alors qu'elles ont été modifiées entre sa récupération et sa mise à jour,
    /// son action écrase les nouvelles données en base.
    /// Cette méthode est souvent appelée "Client Wins" et utilise les données originales et en base (cf. exemple 042_016).
    /// 
    /// Deux méthodes sont proposées pour cela :
    /// - (METHODE 1) : la première consiste à dédier une colonne pour les vérifications (update et delete) qu'on appelle généralement RowVersion et qu'on décore avec l'attribut Timestamp (cf. classe Nounours)
    /// - la seconde permet de ne pas rajouter de colonne spécifique mais de préciser quelles seront les colonnes à utiliser pour les tests de vérification
    ///     - (METHODE 2a) : soit en annotation de données ([ConcurrencyCheck()]) (cf. classe Nounours)
    ///     - (METHODE 2b) : soit en fluent API (modelBuilder.Entity<Nounours>().Property<int>(n => n.NbPoils).IsConcurrencyToken();) (cf. classe NounoursDBEntities)
    ///     
    /// Attention de bien noter que la méthode UpdateNounours a été modifiée pour catcher l'exception DbUpdateConcurrencyException et utiliser OriginalValues et DatabaseValues en cas d'exception.
    /// 
    /// Commentez et décommentez les méthodes pour tester les différentes possibilités
    /// </summary>
    class Program
    {
        /// <summary>
        /// récupère un Nounours à partir de son id
        /// </summary>
        /// <param name="id">l'id du Nounours à récupérer</param>
        /// <returns>le Nounours récupéré (null, si l'id n'est pas connu dans la base)</returns>
        static Nounours GetNounours(Guid id)
        {
            Nounours nounours = null;

            using (NounoursDBEntities db = new NounoursDBEntities())
            {
                nounours = db.NounoursSet.Single(n => n.UniqueId == id);
            }

            return nounours;
        }

        /// <summary>
        /// mise à jour du Nounours dans la base
        /// </summary>
        /// <param name="nounours">nounours à mettre à jour</param>
        static void UpdateNounours(Nounours nounours)
        {
            using (NounoursDBEntities db = new NounoursDBEntities())
            {
                db.NounoursSet.Attach(nounours);
                db.Entry<Nounours>(nounours).State = EntityState.Modified;

                try
                {
                    db.SaveChanges();
                }
                catch (DbUpdateConcurrencyException ex)
                {
                    //observez en mode debug de quelle manière la variable nounours est affectée
                    //récupération de l'entry
                    var entry = ex.Entries.Single();
                    // Using a NoTracking query means we get the entity but it is not tracked by the context
                    // and will not be merged with existing entities in the context.
                    var databaseEntity = db.NounoursSet.AsNoTracking().Single(n => n.UniqueId== (entry.Entity as Nounours).UniqueId);
                    var databaseEntry = db.Entry(databaseEntity);

                    //on change les données OriginalValues en les écrasant avec celles en base.
                    //Ainsi, EF ne voit pas de différences entre les données récupérées en base avant et après les modifications, et croit qu'on peut faire une modification avec CurrentValues
                    foreach (var property in entry.Metadata.GetProperties())
                    {
                        entry.Property(property.Name).OriginalValue = databaseEntry.Property(property.Name).CurrentValue;
                    }

                    //on applique les données courantes en base
                    db.SaveChanges();

                }
            }
        }

        static void Main(string[] args)
        {
            List<Guid> ids = new List<Guid>();

            //création du DbContext et injection de la dépendance à MyStubDataInitializationStrategy
            using (NounoursDBEntities db = new NounoursDBEntities())
            {
                DbContextInitializer.Seed(db);

                //récupère la liste des ids
                ids = db.NounoursSet.Select(n => n.UniqueId).ToList();
            }

            PrintTable("Au démarrage, la table contient :");

            WriteLine();

            //DEBUT DE LA SIMULATION DE MISES A JOUR CONCURRENTES

            //1. Le premier utilisateur récupère les données du premier nounours
            Nounours nounours1 = GetNounours(ids[0]);
            WriteLine("L'utilisateur 1 récupère le premier nounours");

            //2. Le deuxième utilisateur réupère les données du premier nounours
            Nounours nounours2 = GetNounours(ids[0]);
            WriteLine("L'utilisateur 2 récupère le premier nounours");

            //jusque-là, la table contient :
            // "Chewbacca", 27/05/1977, 1234567 poils
            // "Yoda", 21/05/1980, 3 poils
            // "Ewok", 25/05/1983, 3456789 poils

            //3. Le premier utilisateur tente de mettre à jour le nombre de poils, mais ne met pas tout de suite la table à jour
            nounours1.NbPoils = 2;

            WriteLine("Soudain, la table est modifiée par des facteurs extérieurs... (un recommandé peut-être...)");
            //4. La table est mise à jour par un autre biais
            using (NounoursDBEntities db = new NounoursDBEntities())
            {
                db.Database.ExecuteSqlCommand($"UPDATE dbo.TableNounours SET NbPoils = 5 WHERE UniqueId = {ids[0]}");
                db.SaveChanges();
            }
            PrintTable("Maintenant, la table contient :");
            //maintenant, la table contient :
            // "Chewbacca", 27/05/1977, 5 poils <==========================
            // "Yoda", 21/05/1980, 3 poils
            // "Ewok", 25/05/1983, 3456789 poils

            //5. L'utilisateur 1 tente de mettre à jour le nombre de poils
            UpdateNounours(nounours1);
            PrintTable("Après la tentative de modification du nombre de poils de Chewbacca à 2 par l'utilisateur 1 :");
            //une exception est lancée.
            //En client wins, nounours1 écrase les données dans la table.

            //maintenant, la table contient :
            // "Chewbacca", 27/05/1977, 2 poils <==========================
            // "Yoda", 21/05/1980, 3 poils
            // "Ewok", 25/05/1983, 3456789 poils

            //6. Le deuxième utilisateur tente de mettre à jour la propriété Date De Naissance
            nounours2.DateDeNaissance = DateTime.Today;
            UpdateNounours(nounours2);
            PrintTable("Après la tentative de modification de la date de naissance de Chewbacca à aujourd'hui par l'utilisateur 2 :");
            //une exception est lancée.
            // En client wins, nounours2 écrase les données en table

            //maintenant, la table contient :
            // "Chewbacca", 10/11/2016, 1234567 poils <=========================
            // "Yoda", 21/05/1980, 3 poils
            // "Ewok", 25/05/1983, 3456789 poils
            //Notez que le nombre de poils a à nouveau changé, car nounours2 avait récupéré les données avant la première insertion
        }

        static void PrintTable(string intro)
        {
            WriteLine();
            WriteLine(intro);
            using (NounoursDBEntities db = new NounoursDBEntities())
            {
                //affiche les nounours après les modifications
                WriteLine("Après modifications");
                foreach (var n in db.NounoursSet)
                {
                    WriteLine($"\t{n}");
                }
            }
        }
    }
}
