IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

ASP.NET MVC Discussion :

Ou doit-on instancier le DbContext dans une appli. ASP.NET MVC transactionnelle ?


Sujet :

ASP.NET MVC

  1. #1
    Futur Membre du Club
    Homme Profil pro
    Développeur .NET
    Inscrit en
    Février 2013
    Messages
    5
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Moselle (Lorraine)

    Informations professionnelles :
    Activité : Développeur .NET
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2013
    Messages : 5
    Points : 7
    Points
    7
    Par défaut Ou doit-on instancier le DbContext dans une appli. ASP.NET MVC transactionnelle ?
    Bonjour,

    Je suis développeur d’applications desktop et je suis entrain de me familiariser avec les applications Web. Malgré la facilité d’adaptation vers les Framework WebForm ASP plus proche de la culture des applications desktop, j’ai décidé de me lancer sur ASP.NET MVC (pour la maintenabilité et les tests unitaires).
    Après avoir longuement parcouru les tutos et forum sur ASP.NET MVC regorgeant d’exemples d’applications simples mono-table de type CRUD, je n’ai trouvé aucun exemple de code simple restant à ma porté de débutant sur ASP MVC, sur les problématiques rencontré sur un projet multi-tables avec des mises à jour effectuées sur plusieurs requêtes successive mais englobées dans une transaction unique pour maintenir l’intégrité de la BDD, bien entendu dans un contexte Web StateLess (problème que je n’ai pas sur des appli. Desktop connectées).

    Pour facilité vos réponses, je vais prendre l’exemple d’une application de type facture avec une entité ENTETE et une entité LIGNES. Lors de la mise à jour d’une facture via l’application Web, l’utilisateur va effectuer plusieurs requêtes successives d’ajout de lignes, de modifications de lignes, ainsi que de suppressions de lignes. L’ensemble de ces requêtes ne doivent être persistées en base, que lors de la requête finale de validation de la facture par l’utilisateur. Requête finale qui entraînera également la mise à jour des infos de l’entité ENTETE (total HT, TVA, TTC par exemple).

    Tous les exemples simples que j’ai parcourus dans les différents tutos, instancient le contexte EF au sein du contrôleur. Cela implique que la persistance en base doit avoir lieu dans l’action appelée par la requette. Si ce n’est pas le cas, les modifications apportées au contexte vont êtres perdu à chaque nouvelle requette.
    La solution consisterai a instancié le contexte EF dans la session (dans l’événement session_start de l’instance de la class MVCApplication dans gobal.asax).

    Je n’ai trouvé aucun exemple mettant en œuvre cette solution et je me demande si cela est envisageable dans un environnement Web ou il peut y avoir de nombreuses sessions simultanées avec pour chacune d’elles un objet contexte susceptible d’avoir une empreinte mémoire conséquente sur le serveur. Dans un contexte Desktop, je n’ai pas ce problème dans la mesure où l’empreinte mémoire est reparti sur chaqu’un des postes clients et n’impacte pas le serveur.
    Est-ce que quelqu’un pourrait m’indiquer un lien vers un exemple mettant en œuvre cette solution ou une solution alternative respectant les « Best Practices » en ce domaine.

    Merci à vous. Et bon développement.

  2. #2
    Membre éprouvé Avatar de yonpo
    Homme Profil pro
    Développeur .NET
    Inscrit en
    Mars 2010
    Messages
    617
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 35
    Localisation : France

    Informations professionnelles :
    Activité : Développeur .NET
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Mars 2010
    Messages : 617
    Points : 947
    Points
    947
    Par défaut
    Je pense que le mieux est de faire cela en utilisant un framework javascript tel que Angularjs, Backbonejs, Emberjs, ...
    L'idée est d'avoir une seule page où l'utilisateur ferra les modifications d'une facture. Les opérations effectuées ne seront pas répercutées directement en base, tout sera en mémoire côté client. L'ajout, la modification et la suppression de lignes se feront ainsi de manière dynamique.

    En gros :
    1 - chargement de la facture
    2 - modification de la facture par l'utilisateur
    3 - envoie de la facture modifiée au serveur pour validation et enregistrement.

  3. #3
    Futur Membre du Club
    Homme Profil pro
    Développeur .NET
    Inscrit en
    Février 2013
    Messages
    5
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Moselle (Lorraine)

    Informations professionnelles :
    Activité : Développeur .NET
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2013
    Messages : 5
    Points : 7
    Points
    7
    Par défaut
    Merci yonpo pour ton aide,

    En effet, j'ai avancé depuis sur le sujet et je suis entrain de me familiariser avec KNOCKOUT et les Web API MVC (ou éventuellement BREEZE), utilisés dans les SinglePageApplications, qui permettent respectivement de faire du binding et de requêter la base un peu comme avec Linq to Entity, mais en JavaScript coté client. Je retrouve donc à peu près les mêmes mécanismes que ceux que je connaissais en WPF (MVVM), le contexte étant déporté sur le poste client.
    J’essaye toute fois, s’agissant d’une Appli. Web, de traiter tous les GETs de manière classique en MVC (en rendant une Vue par l’intermédiaire de l’Action d’un Contrôleur) afin de permettre l’indexation par les moteurs de recherche. Par contre toutes les écritures en BDD se font en Ajax par l’intermédiaire d’une Web API RestFull (POST, PUT et DELETE) en attendant de maitriser BREEZE(RIA), Framework qui, par contre, ne permettra pas le référencement vu que les requêtes en lecture se font en Ajax, coté client.

    En ce qui concerne les API RestFull, le problème de l’instanciation du DbContext reste donc entier.

    Ma Solution :
    Après lecture, il est très déconseillé de conserver une référence vers le contexte en Session. En effet, le Context n’est pas TradeSafe, et en plus sur une ferme de Serveurs les MVCApplication sont instanciées sur chaque serveur et les ressources de sessions sont sauvegardées en BDD et récupérées éventuellement par un serveur différent à chaque requête vers le site. Je vous laisse imaginer le problème des connexions BDD instanciées au sein de chaque Context, qui vont rester ouvertes ou se perdre d’un serveur à l’autre.
    Je n’avais pas réfléchi, en fait je peux instancier le Dbcontext dans le Contrôleur et faire un Dispose du Context à l’échéance de l’action entrainant la libération de la connexion vers le serveur SQL. La seule chose dont j’ai besoin pour gérer une Transaction est de conserver (dans le dictionnaire de Session) une référence vers les entités traitées.

    Le schéma serait le suivant :
    1)-> Requête GET(classique) sur une facture :
    a.– Sauvegarde d’une référence vers l’entité facture en session
    b.– rendu de la Vue (classique afin de permettre l’indexation sur les moteurs: inutile pour une appli. facture, mais c’est un cas fictif d’école)
    2)-> Requête PUT(Ajax) pour sauvegarder les modifs(effectuées coté client):
    a.– Récupération d’une référence vers l’entité facture sauvegardée en session
    b.– attacher cette entité au Context nouvellement instancié
    c.– mise à jour des modifs dans l’entité facture récupérée en Session avec les infos de l’entité facture POSTée (enfin PUTée) :
    i.-UPDATE des lignes modifiées
    ii.-DELETE des lignes supprimées
    iii.-INSERT des lignes ajoutées
    3)-> Sauvegarde en BDD :

    Les requêtes POST(INSERT) et DELETE sont triviales et ne nécessitent pas de conservation d'un état initial.

    Ce schéma a 2 avantages par rapport au schéma mis en œuvre en utilisant le Scafolding Visual Studio/ApiControler. Ce dernier effectue une nouvelle lecture en base à l’étape 2 au lieu d’utiliser mon mécanisme de sauvegarde/récupération de l’entité en Session. Ma solution évite donc une deuxième requête vers le serveur de BDD, mais surtout permet de vérifié si l’entité n’a pas été modifiée en base par un autre utilisateur entre le GET et le PUT (imaginer le résultat obtenu si un autre utilisateur à supprimé et rajouté des lignes en facture entre temps).

    Je viendrai Poster mes sources sur ce fils de discussion quand elles seront fonctionnelles.

    Bon dimanche a tous.

  4. #4
    Membre confirmé

    Femme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Novembre 2011
    Messages
    244
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Âge : 46
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Novembre 2011
    Messages : 244
    Points : 574
    Points
    574
    Par défaut
    Hello,
    C'est avec intérêt que j'ai lu ta prose, mais j'avoue ne pas avoir compris par quel mécanisme tu arrives à mettre à jour l'entité récupérée de la session avec les données en base entre l'étape get et l'étape put :
    Ma solution évite donc une deuxième requête vers le serveur de BDD, mais surtout permet de vérifié si l’entité n’a pas été modifiée en base par un autre utilisateur entre le GET et le PUT
    Pourrais-tu m'éclairer ?
    "C'est tellement merdique que toute modification est une amélioration !"

  5. #5
    Futur Membre du Club
    Homme Profil pro
    Développeur .NET
    Inscrit en
    Février 2013
    Messages
    5
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Moselle (Lorraine)

    Informations professionnelles :
    Activité : Développeur .NET
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2013
    Messages : 5
    Points : 7
    Points
    7
    Par défaut
    Merci, plume13 de l'intérêt que tu porte à mes recherches,

    Je vous livre donc mes sources dans leur état actuel. Le coté serveur est fonctionnel, le coté client en JavaScript présente encore quelques Bugs (je manque d’expérience).

    J’attends vos commentaires et toute critique constructive est la bien venu. S’il y a moyen de faire autrement, différemment ou mieux, je suis preneur. J’ai demandé de l’aide, et j’ai finalement résolut mon problème moi-même. Je prends le temps de poster et commenter mon résultat afin que tout le monde puisse en profitez, mais j’attends un minimum d’implications et de retours de votre part. C’est, il me semble le but de ce forum.

    Aller, je me lance (pardonnez moi par avance mes fautes d’orthographe passées et à venir).

    Voici mon Contrôleur traitant les GET « Classiques »:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
     
    public class HomeController : Controller {
    	[Authorize]
    	public ActionResult Factures() { return View(new FactureController().Getfactures()); }
    	[Authorize]
    	public ActionResult Facture(int? id) {
    		id = (id == null) ? 0 : id;
    		facture facture;
    		try { facture = new FactureController().Getfacture(id);	}
    		catch (Exception) { return View("Error", new HandleErrorInfo(new Exception("Facture Inexistante"), "Home", "Facture")); }
    		return View(facture);
    	}
    }
    Ce contrôleur n’instancie pas de DbContext. En fait, j’utilise l’APIControler comme Data Access Layer (lignes 4 et 9). L’accès aux données se fait donc uniquement dans la partie WebAPI (permettant ainsi de factoriser l’ensemble de ces méthodes, facilitant les tests et la maintenance, et de plus cet API peut être utilisé par des clients Desktop (non BROWSER) comme service d’accès aux données de l’application).
    L’Action Factures rend une Vue présentant la liste des factures.
    L’Action Facture/Id rend la Vue Détail de la facture n° Id.
    J’utilise l’Action Facture sans Id ou avec un Id=0 pour créer une nouvelle Facture (ligne 7, 9 et voir plus loin dans Getfacture de l’APIControler).

    Voici la Vue Factures :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
     
    @model IEnumerable<Facture.facture>
    @{ ViewBag.Title = "Liste des Factures";}
     
    <input type="button" value="Ajouter" onclick="document.location.href='@Url.Action("Facture", "Home", new { id = 0 })'" />
    <br /><br />
    @foreach (var f in Model) {
        <div>
            @Html.DisplayFor(m => f.Id)
            @Html.DisplayFor(m => f.date)
            @Html.DisplayFor(m => f.nom)
            @Html.DisplayFor(m => f.prenom)
            @Html.DisplayFor(m => f.rue)
            @Html.DisplayFor(m => f.cp)
            @Html.DisplayFor(m => f.ville)
            @Html.DisplayFor(m => f.dbl)
            @Html.ActionLink("Editer", "Facture", new { id = f.Id }, null)
            <input type="button" value="Supprimer" onclick="Delete(@f.Id)" />
            <br />
        </div>
    }
    <input type="button" value="Seed" onclick="Delete('0')" />
     
    @section scripts
    {
        <script>
        var Token = "@AjaxHeader.TokenHeaderValue()";
            $(document).ready(function () {
     
            });
            function Delete(Id) {
                Ajax("Facture", "DELETE", Id, "",
                function (data, textStatus, jqXHR) { document.location.href = 'Factures'; },
                function (jqXHR, textStatus, exception) { alert(error(jqXHR, textStatus, exception)); },
                Token);
            };
        </script>
        <script src="~/Scripts/AppLibs.js"></script>
    }
    En ligne5 on retrouve le bouton « Ajouter» faisant un appel sur Facture/0 pour créer une nouvelle Facture. Il s’agit d’un GET, mais il est réalisé par un appel de fonction JavaScript et non un <a href> pour évité que les moteurs de recherche me crées plein de factures vides).
    En ligne 18, le bouton « Supprimer » fait un appel Ajax(par l’intermédiaire de la fonction Delete ligne31). Cette fonction rappelle l’URL /Factures (ligne33) pour mettre à jour la liste des factures, faisant disparaitre la facture supprimée.
    Le bouton « Seed » fait un appel Ajax vers l’API DELETE Factures/0. C’est purement conceptuel, cela me permet de vider la base et la réalimenter avec des données initiales correctes pour la phase de mise au point du logiciel.
    Les Appels Ajax encapsulent en Header un RequestVerificationToken, initialisé en ligne 27, pour la sécurité (Les Actions MVC ont un attribut [Authorize] et plus loin l’APIControler a un attribut [Authorize] et [ValidateAntiForgeryToken])

    La 2ème action, rend la vue Détail d’une facture :
    Ce n’est pas la version définitive, elle présente encore des Bugs conceptuels et certains manques fonctionnels.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
     
    @model Facture.facture
    @{ ViewBag.Title = "Facture ";}
     
    @Html.TextBoxFor(m => m.Id, new { data_bind = "value: facture.Id" })
    @Html.TextBoxFor(m => m.date, new { data_bind = "value: facture.date" })
    @Html.TextBoxFor(m => m.dbl, new { data_bind = "value: facture.dbl" })
    @Html.TextBoxFor(m => m.nom, new { data_bind = "value: facture.nom" })
    @Html.TextBoxFor(m => m.prenom, new { data_bind = "value: facture.prenom" })
    @Html.TextBoxFor(m => m.rue, new { data_bind = "value: facture.rue" })
    @Html.TextBoxFor(m => m.cp, new { data_bind = "value: facture.cp" })
    @Html.TextBoxFor(m => m.ville, new { data_bind = "value: facture.ville" })
    <input type="text" id="Id" data-bind="value: facture.id" value="@Model.Id"/>
    <input type="text" data-bind="value: facture.nom" />
    <br />
    <ul data-bind="foreach: facture.lignes">
        @for (int i = 0; i < Model.lignes.Count(); i++) {
            <li>
                <input name="lignes[@i]" type="text" data-bind="value: code" />
                @Html.TextBoxFor(m => m.lignes[i].code, new { data_bind = "value: code" })
                @Html.TextBoxFor(m => m.lignes[i].lib, new { data_bind = "value: lib" })
                @Html.TextBoxFor(m => m.lignes[i].qte, new { data_bind = "value: qte" })
                @Html.TextBoxFor(m => m.lignes[i].pu, new { data_bind = "value: pu" })
                @Html.TextBoxFor(m => m.lignes[i].pt, new { data_bind = "value: pt", disabled = "disabled" })
                <input name="lignes[0]" type="button" value="Ajouter Ligne" onclick="Insert(this)" />
                <input name="lignes[0]" type="button" value="Supprimer Ligne" onclick="Del(this)" />
            </li>
        }
    </ul>
    <input type="button" value="Modifier" onclick="Update()" />
    <input type="button" value="Annuler" onclick="Cancel()" />
    <input type="button" value="Enregistrer" onclick="Save()" />
     
    @section scripts
    {
        <script>
            var vm = {
                facture: null
            }
            var Token = "@AjaxHeader.TokenHeaderValue()";
            $(document).ready(function () {
     
            });
            function Insert() {
                vm.facture.lignes.push({ code: "", lib: "", qte: 0, pu: 0, pt: 0 });
            };
            function Del() {
                vm.facture.lignes.splice(0, 1);
            };
            function Cancel() {
     
            };
            function Update() {
                Ajax("Facture", "GET", $("#Id").val(), null,
                function (data, textStatus, jqXHR) {
                    ko.mapping.fromJS(data, vm);
                    vm.facture.lignes = ko.observableArray(data.lignes);
                    $('[data-bind*="foreach"] input:not([name*="0"])').parent().remove(); // Supprime les input sauf pour i=0
                    if (!ko.dataFor($('#Id').get(0))) { ko.applyBindings(vm); };
                },
                function (jqXHR, textStatus, exception) {
                    if (jqXHR.status == 404) { alert('Facture Inexistante !!!'); }
                    else { alert(error(jqXHR, textStatus, exception)) }
                },
                Token);
            }
            function Save() {
                Ajax("Facture",
                    ($("#Id").val() == 0) ? "POST" : "PUT",
                    $("#Id").val(),
                    vm.facture(),
                    function (data, textStatus, jqXHR) {
                        document.location.href = '@Url.Action("Factures", "Home")';
                    },
                    function (jqXHR, textStatus, exception) { alert(error(jqXHR, textStatus, exception)); },
                    Token);
            }
        </script>
        <script src="~/Scripts/AppLibs.js"></script>
    }
    Cette vue est une vue unique permettant à la fois d’être utilisée pour l’affichage ainsi que pour l’ajout et la modification de la facture (Cela évite d’avoir à maintenir une vue Edit, Create, Delete et Details). Elle utilise les Helpers TextBoxFor rendant des <input> qui seront Disabled au départ pour l’affichage simple, et permettre l’indexation par les moteurs. Puis dans la fonction JavaScript Update (appelé par le bouton Modifier en ligne 26) je vais désactiver le Disabled des <input> pour permettre la saisie utilisateur après avoir supprimé en DOM (ligne 58) toutes les lignes de facture rendu par la vue sauf la ligne0 qui sera utilisée par les mécanismes de binding de KNOCKOUT pour afficher les données réceptionnées par l’appel Ajax.
    Notez en ligne 16 et 17 :
    La ligne 17 est la boucle exécutée coté serveur par le moteur Razor pour rendre chaque ligne en vue.
    La ligne 16 est présente pour afficher dynamiquement les lignes de facture par le moteur de binding de KNOCKOUT coté client lorsque l’on clique sur le bouton « Modifier ».
    Notez également comment il est simple grâce à KNOCKOUT et ses mécanisme de binding d’ajouter (ligne 45) et supprimer (ligne48) une ligne de facture. L’action est réalisée sur le model et knockout se charge de mettre a jour l’UI.
    Le bouton « Enregistrer » -> fonction JavaScript Save, fait un appel Ajax de type PUT ou POST suivant que l’on modifie une facture existante (id<>0 -> PUT) OU que l’on enregistre une nouvelle facture (id=0 -> POST) : rappelez vous, j’utilise le cas particulier id=0 pour créer une nouvelle facture dans mes Actions WebAPI.
    Le bouton « Annuler » n’a pas encore été développé. Il permettra, à terme, d’abandonner les modifications saisies afin qu’elles ne soient pas persistées en base. Cela nécessitera un appel vers une APIaction afin de libérer la référence de l’entité facture enregistrée en session lors du GET.

    Pour terminez, je vous livre le moteur de mon application. Voici les Actions de l’APIControleur :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
     
    	[Authorize]
    	[ValidateAntiForgeryToken]
    	public class FactureController : ApiController {
    		private AppContext db = new AppContext();
     
    		// READ: GET api/Facture/
    		public IEnumerable<facture> Getfactures() { return db.factures.AsEnumerable(); }
     
    		// READ: GET api/Facture/5
    		public facture Getfacture(int? id) {
    			facture facture;
    			if (id == 0) {
    				facture = new facture();
    				facture.lignes.Add(new ligne());
    			}
    			else {
    				facture = db.factures.Find(id);
    				if (facture == null) {
    					throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.NotFound));
    				}
    				if (AjaxHeader.IsAjaxRequest()) {
    					string SessionIdToken = HttpContext.Current.Request.Headers.GetValues("RequestVerificationToken").FirstOrDefault();
    					HttpContext.Current.Session[SessionIdToken] = facture;
    				}
    			}
    			return facture;
    		}
     
    		// UPDATE: PUT api/Facture/5
    		public HttpResponseMessage Putfacture(int id, facture PostedFacture) {
    			if (!ModelState.IsValid) {
    				return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
    			}
     
    			if (id != PostedFacture.Id) {
    				return Request.CreateResponse(HttpStatusCode.BadRequest);
    			}
     
    			string SessionIdToken = HttpContext.Current.Request.Headers.GetValues("RequestVerificationToken").FirstOrDefault();
    			facture SessionFacture = (facture)HttpContext.Current.Session[SessionIdToken];
     
    			db.Entry(SessionFacture).State = EntityState.Unchanged;
    			SessionFacture.nom = PostedFacture.nom;
    			SessionFacture.prenom = PostedFacture.prenom;
    			SessionFacture.rue = PostedFacture.rue;
    			SessionFacture.cp = PostedFacture.cp;
    			SessionFacture.ville = PostedFacture.ville;
     
    			List<ligne> LigsToDel = new List<ligne>();
    			foreach (var SessionLigne in SessionFacture.lignes) {
    				ligne PostedLigne = (ligne)PostedFacture.lignes.SingleOrDefault(m => m.Id == SessionLigne.Id);
    				if (PostedLigne == null) {
    					LigsToDel.Add(SessionLigne);
    				}
    				else {
    					db.Entry(SessionLigne).State = EntityState.Unchanged;
    					SessionLigne.Id = PostedLigne.Id;
    					SessionLigne.ordre = PostedLigne.ordre;
    					SessionLigne.code = PostedLigne.code;
    					SessionLigne.lib = PostedLigne.lib;
    					SessionLigne.qte = PostedLigne.qte;
    					SessionLigne.pu = PostedLigne.pu;
    					SessionLigne.factureId = PostedLigne.factureId;
    				}
    			}
    			foreach (var LigToDel in LigsToDel) {
    				db.Entry(LigToDel).State = EntityState.Deleted;
    			}
    			foreach (var PostedLigne in PostedFacture.lignes.Where(m => m.Id == 0)) {
    				PostedLigne.factureId = SessionFacture.Id;
    				db.lignes.Add(PostedLigne);
    			}
     
    			try {
    				db.SaveChanges();
    			}
    			catch (DbUpdateConcurrencyException ex) {
    				return Request.CreateErrorResponse(HttpStatusCode.NotFound, ex);
    			}
    			HttpContext.Current.Session.Remove(SessionIdToken);
    			return Request.CreateResponse(HttpStatusCode.OK);
    		}
     
    		// CREATE: POST api/Facture
    		public HttpResponseMessage Postfacture(facture facture) {
    			if (ModelState.IsValid) {
    				db.factures.Add(facture);
    				try {
    					db.SaveChanges();
    					HttpResponseMessage response = Request.CreateResponse(HttpStatusCode.Created, facture);
    					response.Headers.Location = new Uri(Url.Link("DefaultApi", new { id = facture.Id }));
    					return response;
    				}
    				catch (DbUpdateConcurrencyException ex) {
    					return Request.CreateErrorResponse(HttpStatusCode.NotFound, ex);
    				}
    			}
    			return Request.CreateErrorResponse(HttpStatusCode.BadRequest, ModelState);
    		}
     
    		// DELETE api/Facture/5
    		public HttpResponseMessage Deletefacture(int id) {
    			if (id == 0) {
    				db.Seed();
    				return Request.CreateResponse(HttpStatusCode.OK);
    			}
    			//
    			// facture TimeStamp pour verif si modif avant suppression
    			//
    			facture facture = db.factures.Find(id);
    			if (facture == null) {
    				return Request.CreateResponse(HttpStatusCode.NotFound);
    			}
    			db.factures.Remove(facture);
    			try {
    				db.SaveChanges();
    			}
    			catch (DbUpdateConcurrencyException ex) {
    				return Request.CreateErrorResponse(HttpStatusCode.NotFound, ex);
    			}
    			return Request.CreateResponse(HttpStatusCode.OK, facture);
    		}
     
    		protected override void Dispose(bool disposing) {
    			db.Dispose();
    			base.Dispose(disposing);
    		}
    	}
    En ligne5 j’instancie mon AppContext qui sera utilisé dans toutes les Actions du contrôleur. Ce Context sera rendu au GarbageCollector lors du Dispose ligne 125, mettant fin à la connexion.

    En ligne8, l’action Getfactures retourne toutes les factures de la base. Rappeler vous, cette méthode est utilisé par l’action Factures de mon contrôleur MVC pour rendre la vue « Liste des factures ».
    Cet API Action, ainsi que les suivantes, sont donc utilisée comme DAL, soit par les méthodes de mon application, mais d’une pierre deux coups, elles peuvent aussi être utilisées comme service d’accès aux données par une application Desktop.

    En ligne 11, l’action Getfacture(id) retourne soit :

    Si id=0 -> une facture vide comportant une ligne vide (ligne15). La ligne vide est utilisée comme modèle de ligne, pour permettre au moteur KNOCKOUT coté client, d’itérer et faire son foreach (ligne 12 de la vue Détail) sur un Template de ligne vide.

    Si id<>0 -> la facture id retrouvée en BDD (si celle-ci existe sinon retour HttpStatusCode.NotFound).
    ---> ET C’EST LA QUE JE VAIS SAUVEGARDER UNE REFERENCE VERS CETTE ENTITEE FACTURE DANS LE DICTIONNAIRE DE SESSION.
    Dans un premier temps, je vérifie (ligne 22) si la requête provient d’un appel Ajax. En effet, si Getfacture(id) a été appelé par l’Action MVC Getfacture(ligne9 du HomeControler), il n’est pas utile d’en conservé une référence -> l’action Getfacture(id) du contrôleur MVC est destiné uniquement à l’affichage de la facture (pour le référencement) et ne donne pas suite à une modification de facture. Ce test est nécessaire, sinon tous les moteurs de recherche ouvriraient inutilement une session avec une référence vers la facture affichée.
    Puis, j’enregistre la référence vers l’entité facture dans le dictionnaire de session. Petite subtilité concernant la clé de dictionnaire associé à la facture : plutôt que d’utilisé une clé avec une valeur fixe comme « facture » par exemple, j’ai choisi d’utilisé la valeur du RequestVerificationToken de la requête. En effet, avec une valeur fixe, si un utilisateur ouvre plusieurs onglets pour travailler sur plusieurs factures simultanément, l’ensemble des onglets ouvert partagerait la même session et entrainerait un conflit car la clé est unique. L’utilisation d’une clé dynamique qui change à chaque requête résout ce problème.

    Je fais l’impasse sur le commentaire concernant les actions Postfacture et Deletefacture. Notez simplement dans Deletefacture en ligne 104-107, l’appel de la méthode Seed de l’AppContext pour nettoyer et réalimenter la base avec des données initiales propres pour la phase de tests (Rappelez-vous le bouton « Seed » dans la vue « Liste des factures ».

    Pour terminer, l’action la plus complexe, Putfacture.
    En ligne 40-41 je récupère la clé «RequestVerificationToken » associé à l’entité facture sauvegardée en session et j’établis une nouvelle référence «SessionFacture » vers cette entité. J’ai donc deux entités, SessionFacture qui correspond à l’état initial de la facture avant modifications, et PostedFacture qui correspond à la facture modifiée postée par une requête Ajax coté client.
    La suite est un mapping dont le but est de mettre à jour la facture initiale SessionFacture avec les valeurs de la facture modifiée PostedFacture, afin de permettre à Entity Framework de faire son boulot, càd générer les requêtes SQL UPDATE sur les colonnes de la table Factures qui ont été modifiées, ainsi que les requêtes UPDATE, DELETE et INSERT sur la table Lignes. Ce mapping mérite une encapsulation dans une class de mapping externe a laquelle on passerai en référence l’entité SessionFacture et PostedFacture afin d’alléger l’action Putfacture.
    En ligne 51, je parcours l’ensemble des lignes de la facture initiale pour trouver les lignes de facture qui ont été supprimées (enregistrées dans LigsToDel), et pour mettre à jour les propriétés modifiées.
    En ligne67, j’indique à EF les lignes qui sont à supprimer (EntityState.Deleted).
    Et en ligne 70, je recherche toutes les lignes de PostedFacture qui ont un id=0, qui correspondent aux lignes nouvellement créées. Je les ajoute au contexte en ligne 72 sans oublier de mettre à jour factureId (foreign key) en ligne 71.

    En ligne76, je lance la magie Entity Framework (db.SaveChanges) qui va tout mettre à jour dans la BDD en prenant soin de vérifié les accès concurrent par un autre utilisateur, qui ont pu avoir lieu sur la facture entre le GET(SessionFacture) et le PUT(PostedFacture).

    Et pour terminer, en ligne 81 je n’oublie pas de rendre au GarbageCollector la référence vers l’entité enregistrée dans le dictionnaire de session, afin de libérer la ressource.

    Voilà tout.

    PS : je précise que le besoin était de mettre au point une méthode permettant d’associer un contexte MVC classique pour les requêtes GET afin de rendre des vues indexables et référensables par les moteurs de recherche, avec un contexte Web APIControler pour les requêtes Ajax POST,PUT et DELETE pour la partie SinglePageApplication coté client.
    Le recourt à l’exemple d’une appli. Facture, est un cas d’école qui ne présente aucun intérêt d’un point de vue référencement. L’application facture n’est utilisée que comme support d’extrapolation aux explications et commentaires que j’attends avec impatience.

  6. #6
    Membre confirmé

    Femme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Novembre 2011
    Messages
    244
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Âge : 46
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Novembre 2011
    Messages : 244
    Points : 574
    Points
    574
    Par défaut
    Hello,
    Je vois que tu as fait un gros travail d'explication de ton code, c'est cool
    Pourrais-tu poster la solution complète, histoire de pouvoir "jouer" un peu avec
    A première vue, ça m'a l'air pas mal, même si il y a quelques petites choses auxquelles je n'adhère pas trop.
    Par exemple, l'appel de FactureController depuis un autre contrôleur, j'ai peur que ce soit une fausse bonne idée.
    De même l'utilisation des entités EF dans les vues : sur un petit projet de test comme cela, ça ne va pas être gênant, mais sur un vrai projet qui va vivre et sera plus imposant, une modification de la base, et faudra repasser sur toutes les pages, même si la modification n'a pas vraiment d'impact sur les UI.
    "C'est tellement merdique que toute modification est une amélioration !"

  7. #7
    Futur Membre du Club
    Homme Profil pro
    Développeur .NET
    Inscrit en
    Février 2013
    Messages
    5
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Moselle (Lorraine)

    Informations professionnelles :
    Activité : Développeur .NET
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Février 2013
    Messages : 5
    Points : 7
    Points
    7
    Par défaut
    Bonjour plume,

    Excuse moi, j'ai eu d'autre priorités cette semaine et je n'ai pas eu le temps de te répondre, ni de me consacrer à ma petite veille techno. sur mon projet d'école.
    Mais, dès que je pourrai à nouveau m’y investir, je vais finaliser la solution et la poster sur le fils comme tu me l’as suggéré. Je mettrais également un lien vers un Repositorie sur GitHub.

    Concernant ta remarque sur l’utilisation des entités du modèle directement dans les actions de contrôleur ou dans les vues, cela ne fait pas parti de mes pratiques habituelles. Mais cela permet de coder rapidement pour la phase de tests, et de s’affranchir de tout le mapping entre le Model et le ViewModel que je ne manquerai pas de mettre en place dans la version définitive. En ce qui me concerne, je pense que le design pattern du Framework ne devrait pas s’appeler MVC mais MVVMC (Model-View-ViewModel-Controler). Cela dit, si je suis effectivement du même avis que toi, force est de constater que des Framework comme BREEZE, offrent non seulement directement les entités aux actions de contrôleur (les vues étant exclues du contexte s’agissant de SinglePageApplications), mais également les Metadatas du contexte tout entier. De plus, les dernières versions de MVC permettent de limiter la portée sur les propriétés du model en utilisant l’attribut [Bind(Include)] ou [Bind(Exclude)] sur les propriétés du model (ou du ViewModel d’ailleurs) afin de limiter les risques et de sécuriser l’application.

    Concernant les évolutions de la base, en cas d’ajout de nouvelles colonnes, il faudra bien mettre à jour les propriétés du Model, du ViewModel ainsi que de mettre à jour l’UI en y intégrant en lieu et place les <input> correspondant aux propriétés nouvellement ajoutées.

    Au sujet de,
    Par exemple, l'appel de FactureController depuis un autre contrôleur, j'ai peur que ce soit une fausse bonne idée.
    Pourrais-tu préciser les implications négatives entrainées par ce genre de pratique.

    Je te donne le cheminement qui m’amené à utiliser se concept :

    J’ai commencé comme traditionnellement à écrire un DataAccessLayer de ce type
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
     
    public class FactureDAL {public IEnumerable<facture> Getfactures() { return db.factures.AsEnumerable(); }
    }
    que j’ai consommé dans mon ApiControleur de la manière suivante :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
     
    public class FactureController : ApiController {
    …
     
    		// READ: GET api/Facture/
    		public IEnumerable<facture> Getfactures() { return dal. Getfactures(); }
    pour me rendre compte que je ne fait qu’appeler en cascade des méthodes de class qui ont la même finalité fonctionnelle et qui ne diffèrent que dans leur baptême (le nom que le codeur a décidé de leur donner).

    En y réfléchissant bien, un DAL est une couche d’abstraction fournissant un service d’accès aux données sous la forme d’une class.
    Un ApiControler est une class fournissant un service d’accès aux données.

    On est bien entrain de parler de la même chose, la seule différence entre les deux est le nom qui par convention pour les Contrôleurs doit être suffixé par Controler et pour les DataAccessLayer par DAL (et ce n’est même pas une obligation en ce qui concerne les DAL).

    Au final dans l’action Facture de mon HomeControleur :

    - que j’appelle le service d’accès aux données par un FactureController().Getfactures()
    - ou par un FactureDAL().Getfactures()

    Dans les deux cas je ne fais qu’appeler une méthode d’une class x ou y.

    En fait, MVC n’est qu’une bibliothèque de class, en partant de MvcApplication jusqu’à Controller ou ApiController et j’en passe. Toutes ses class sont mis à notre disposition pour gagner tu temps, mais on aurait très bien pu les écrire nous-mêmes comme celles que l’on code dans nos solutions et qui viennent compléter les références vers les assembly précitées.

    Enfin, c’est mon point de vue et je suis ouvert au débat.

  8. #8
    Membre confirmé

    Femme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Novembre 2011
    Messages
    244
    Détails du profil
    Informations personnelles :
    Sexe : Femme
    Âge : 46
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Novembre 2011
    Messages : 244
    Points : 574
    Points
    574
    Par défaut
    Hello brunnert,

    Désolé aussi pour le délai, je n'ai pas trop eu le temps non plus de répondre
    Pour compléter mes remarques précédentes :

    - concernant l’utilisation des entités du modèle directement dans les actions de contrôleur ou dans les vues
    En fait, ma remarque est pertinente... ou pas, cela dépend du projet . Comme je le disais précédemment, pour un projet simple, par exemple d'affichage d'une liste d'éléments avec modification basique de ces éléments, ajout/suppression, et peu d'évolutions en vue (comme un projet d'étude ), autant pas s'embêter à faire des couches intermédiaires... Par contre sur un projet professionnel, qui va évoluer et vivre, il vaut mieux passer par des couches spécialisées intermédiaires (cf DDD, Domain Driven Design par exemple). En effet, si tu rajoutes un champ en BDD (ou que tu modifies son nom) pour une raison X ou Y indépendante de l'affichage lui-même, tu n'as peut-être pas envie de repasser sur les interfaces qui n'ont pas bougé (surtout que tu peux en oublier...) J'en parle parce que c'est du vécu Je me suis donc permise de soulever ce problème dans une optique de projet professionnel "d'envergure".

    - pour l'appel du contrôleur par un autre contrôleur, je vais préciser ma pensée
    Ma première réaction quand j'ai vu ton code a été : "argh", mais sans explication formelle à fournir, juste le sentiment que c'est pas le truc à faire (je suis sûre que tu as déjà eu ce sentiment en tombant sur du code !). En y réfléchissant plus, j'ai eu ce sentiment parce que les classes contrôleurs, comme tu le soulignes, sont des classes de "convention", utilisées d'une certaine manière par le Framework MVC. Du coup, créer un objet FactureController en dehors de son utilisation "prévue", ça me plait moyennement. Je serais plus pour l'idée de créer une classe intermédiaire (couche "Application" en DDD), qui serait appelée par les 2 contrôleurs, ces 2 contrôleurs pourraient donc vivre leur vie indépendamment l'un de l'autre.

    D'une manière générale, je n'adhère pas trop à ce que j'appelle du "bricolage" en informatique (en fait, j'en suis revenue plus exactement ). Quand tu le fais, c'est cool, mais quand tu repasses dessus, tu te maudis ou tu maudis celui qui est passé avant ! J'entends par bricolage : les valeurs spéciales de convention (-1 ou 0 alors qu'une valeur null est plus compréhensible) ; l'appel à une méthode publique de la même classe avec des paramètres bizarres, alors qu'une méthode privée commune serait plus logique ; des noms de méthodes ou de propriétés qui ne veulent plus rien dire, parce que leur utilisation a évolué mais pas leur nom ; etc.

    Mais je sais que chacun a sa propre sensibilité au code, et ce n'est que mon point de vue évidemment (qui a évolué et qui peut encore évoluer...)
    "C'est tellement merdique que toute modification est une amélioration !"

Discussions similaires

  1. Réponses: 0
    Dernier message: 24/11/2013, 08h07
  2. Réponses: 1
    Dernier message: 11/05/2011, 10h05
  3. Réponses: 1
    Dernier message: 04/04/2006, 11h29
  4. [D2006] - Utiliser un composant C# dans une appli VCL.NET
    Par RamDevTeam dans le forum Delphi .NET
    Réponses: 1
    Dernier message: 13/02/2006, 16h07

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo