Controller template

standardisation des methode de travail a mon entreprise

a marqué ce sujet comme résolu.

Bonjour a tous , J’ai récemment été engagé dans mon premier emploie dans le domaine en tant que full-stack dev asp ( :soleil: ).

Apres avoir travaillé sur 1 ou 2 projet , j’ai décidé d’etre pro-actif dans mon entreprise en fournissant un template de model/controlleur qui abstrait ce que nous faisons 95% du temps , ce qui nous permettrais de sauver enormement de temps de developpement.

Ce que j’ai cherché a faire c’est d’isoler les operations entre vue | model n’ayant qu’a s’occuper de : vue -> quoi faire lors d’un success/fail ? model -> quoi faire lors des operatioons : save , delete , get , getlist.

Je crois avoir reussi a isoler une tres grosse partie de notre travail.

Ce que j’aimerais c’est votre avis .. je ne fait du c# que depuis 3 mois : A premiere vu j’ai louper des truc ? Voyez vous ce qui pourrait faire "planter" le fonctionnement ? Suggestions ou autre procédés a me proposer ? Je compte le présenter dans un meeting R&D la semaine prochaine.

Mise en contexte :

je travail sur des projet en petite equipe ( 4 personnes ) et nous sommes tous des full stack. Bien que nos modeles soit un minimum standardisé .. certaines personnes travaillent sur un framework interne afin de mettre un standard sur nos procédés. C’est ce que ce template tente d’apporter : un standard dans le procédé de travail. Le plus gros probleme c’est les différence entre les dev : gestion des erreurs dans le controlleur ou dans le modele ( meme certain le font dans la vue .. ). Operations dans le controlleur ou modele.

Avec ce template je contrains donc ce fonctionnement : les operations du controlleurs reste minimales et ne font qu’apeller la fonction respective du model. Tous les operations sont donc fait dans une fonction static du model.

Q : Pourquoi une struct epresult ?

Est-ce que ta question porte sur le struct vs class ou sur le pourquoi retourner une structure ? En fait c’est qu’en cas d’exception ( pas une Exception mais une différence dans le fonctionnement de notre logiciel comparé a ce que nous faisons habituellement ) , il y’a des cas ou nous voudrions faire des operations différente sur le model. Ce EpResult permet donc dans le cas d’un save de recuperer autant la liste d’erreur que le model. Nous n’avons donc qu’a overload ’Save’ dans le controlleur concret et recuperer ces resultats. ( meme si ce point est discutable du fait que ces exceptions peuvent etre geré dans le model .. j’ai quand meme voulu laisser cette posibilité d’overide les fonctions du controlleur sans cassé le workflow que j’etabli avec le model )

Q : Pourquoi expose tu a certain endroit des List<string>** ?

Afin de contreindre le fonctionnement entre model et vue. Les fonction qui peuvent produire une erreur retournent donc une liste de string qui est la liste des erreurs produits ( a part ceux du modelstate/annotations ) dans les operations du model. Par exemple un save : bien que le model puisse etre valide , identity peut retourner une erreur dans le cas d’un email deja utilisé par exemple. Ces retour contreignent donc nos futur dev a utiliser le model correctement : Si les conditions passent tu save et le controlleur s’occupe de retourner succes. Sinon tu rempli une liste d’erreur que le controlleur s’occupe de mettre dans le modelState afin que la vue les affiche.

Q : Lien entre TModel et TViewModel :

presque aucun a part le fait que certaines personne aime bien MVVM et travailler avec des ViewModel plutot que le model directement. Je leur est donc laissé la possibilité d’envoyé un ViewModel a leur model afin de procédé au ’Save’ désiré.

Q : Index et Detail

En fait , c’est question de convention interne : pour une liste de model nous utilisons toujours "Index" et pour la page de détail "Detail" , c’est donc la convention que j’ai représenté ici. Je veux que notre liste s’affiche lorsque nous allons dans /Controlleur/Index.

Flow du template : flowchart

Template :

  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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace EpController
{
    /// <summary>
    ///     Enum for the different views. Each views MUST be defined
    /// </summary>
    public enum ViewName
    {
        List = 0,
        Detail = 1,
        SuccessSave = 2,
        FailedSave = 3
    }

    /// <summary>
    ///     Result from Save functions to have a list of errors and the model
    /// </summary>
    /// <typeparam name="TModel"></typeparam>
    public struct EpResult<TModel>
    {
        public List<string> Errors { get; set; }
        public TModel Model { get; set; }
    }

    /// <summary>
    ///     Base class for any model used by any EpController
    /// </summary>
    /// <typeparam name="TModel">The model class</typeparam>
    /// <typeparam name="TViewModel">The modelView class</typeparam>
    /// <typeparam name="TListModel">The model list class</typeparam>
    /// <typeparam name="TDataBaseConext">The database context</typeparam>
    public abstract class EpModel<TModel,TViewModel, TListModel, TDataBaseConext>
    {
        public abstract TModel Get(TDataBaseConext db, string id);
        public abstract List<TListModel> GetList(TDataBaseConext db);
        public abstract EpResult<TModel> Save(TDataBaseConext db, TModel model);
        public abstract EpResult<TViewModel> Save(TDataBaseConext db, TViewModel viewModel);
        public abstract List<string> Delete(TDataBaseConext db, List<string> ids);

    }

    /// <summary>
    ///     Ep controller managing an EpModel
    /// </summary>
    /// <typeparam name="TModel">The model class</typeparam>
    /// <typeparam name="TViewModel">The model view class</typeparam>
    /// <typeparam name="TListModel">The model list class</typeparam>
    /// <typeparam name="TDataBaseContext">The database context</typeparam>
    public abstract class EpController<TModel, TViewModel, TListModel, TDataBaseContext> : Controller
        where TModel : EpModel<TModel,TViewModel,TListModel, TDataBaseContext>, new()
        where TViewModel : new()
        where TListModel : new()
        where TDataBaseContext : IDisposable, new()
    {

        /// <summary>
        ///     The controller views [ Detail, List, SucessSave, FailedSave]
        /// </summary>
        private Dictionary<ViewName, string> Views;

        /// <summary>
        ///     Constructor
        /// </summary>
        /// <param name="views">The controller views [ Detail, List, SucessSave, FailedSave]</param>
        public EpController(Dictionary<ViewName,string> views)
        {
            var AllViewDefined = (views[ViewName.List] != null) && (views[ViewName.SuccessSave] != null) && (views[ViewName.FailedSave] != null);
            Debug.Assert(AllViewDefined, "Tous les vues ( sauf detail ) doivent etre définis [ List, SucessSave, FailedSave ]");
            Views = views;
        }

        /// <summary>
        ///     Returns the Model List
        /// </summary>
        /// <returns>The list view</returns>
        public ActionResult Index()
        {
            using (var db = new TDataBaseContext())
            {
                var type = typeof(TModel);
                var method = type.GetMethod("GetList");
                List<TListModel> listModel = (List < TListModel > )(method.Invoke(null, new object[] { db }));
                InitializeViewBag();
                return View(Views[ViewName.List], listModel);
            }

        }

        /// <summary>
        ///     Save the model from the detail page
        /// </summary>
        /// <returns>The SuccessSave or FailedSave view</returns>
        public ActionResult Save(TModel model, string successView, string errorView)
        {
            if (ModelState.IsValid)
            {
                ModelState.Clear();
                using(var db = new TDataBaseContext())
                {
                    var result = (EpResult<TModel>)( typeof(TModel).GetMethod("Save").Invoke(null, new object[] { db, model }) );

                    InitializeViewBag();

                    if (result.Errors.Count() > 0)
                    {
                        foreach(var error in result.Errors)
                        {
                            ModelState.AddModelError("", error);
                        }

                        return this.Json(new { success = false, view = RenderHelper.RenderHelper.PartialViewToString(this, Views[ViewName.FailedSave], model) });
                    }

                    return this.Json(new { success = true , view = RenderHelper.RenderHelper.PartialViewToString(this, Views[ViewName.SuccessSave], model) });
                }
            }

            InitializeViewBag();
            return this.Json(new { success = false, view = RenderHelper.RenderHelper.PartialViewToString(this, Views[ViewName.FailedSave], model) });
        }

        /// <summary>
        ///     Save the model
        /// </summary>
        /// <returns>The SuccessSave or FailedSave view</returns>
        public ActionResult Save(TViewModel viewModel , string successView , string errorView)
        {
            if (ModelState.IsValid)
            {
                ModelState.Clear();
                using (var db = new TDataBaseContext())
                {
                    var result = (EpResult<TViewModel>)( typeof(TModel).GetMethod("Save").Invoke(null, new object[] { db,  viewModel }) );

                    InitializeViewBag();

                    if (result.Errors.Count() > 0)
                    {
                        foreach (var error in result.Errors)
                        {
                            ModelState.AddModelError("", error);
                        }

                        // use result.Model to send it to the view
                        return this.Json(new { success = false, view = RenderHelper.RenderHelper.PartialViewToString(this, Views[ViewName.FailedSave], viewModel) });
                    }

                    return this.Json(new { success = true, view = RenderHelper.RenderHelper.PartialViewToString(this, Views[ViewName.SuccessSave], viewModel) });
                }
            }

            InitializeViewBag();
            return this.Json(new { success = false, view = RenderHelper.RenderHelper.PartialViewToString(this, Views[ViewName.FailedSave], viewModel) });
        }

        /// <summary>
        ///     Get the detail page
        /// </summary>
        /// <param name="id"></param>
        /// <returns>The detail view</returns>
        public ActionResult Detail(string id)
        {
            using (var db = new TDataBaseContext())
            {
                InitializeViewBag();

                if (Views[ViewName.Detail] != null)
                {
                    TModel model = (TModel)typeof(TModel).GetMethod("Get").Invoke(null, new object[] { db, id });
                    return View(Views[ViewName.Detail], model);
                }
                else
                {
                    return Index();
                }
            }
        }

        /// <summary>
        ///     Delete a list of models
        /// </summary>
        /// <param name="ids"></param>
        public void Delete(List<string> ids)
        {
            using (var db = new TDataBaseContext())
            {
                var result = (List<string>)typeof(TModel).GetMethod("Delete").Invoke(null, new object[] { db, ids });
            }
        }

        public abstract void InitializeViewBag();
    }
}

Exemple model :

 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
public class Client : EpController.EpModel<Client,ClientViewModel,ClientListModel,ClientContext>
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public int RoleId { get; set; }


    public override List<ClientListModel> GetList(ClientContext db)
    {
        return db.GetList();
    }

    public override EpResult<Client> Save(ClientContext db, Client model)
    {
        //do operations
        db.Save(model);
        return new EpResult<Client>() { Errors = null, Model = model }; // return the errors and model
    }

    public override EpResult<ClientViewModel> Save(ClientContext db, ClientViewModel viewModel)
    {
        // do operations
        db.Save(viewModel);
        return new EpResult<ClientViewModel>() { Errors = null, Model = viewModel }; // return the errors and model
    }

    public override List<string> Delete(ClientContext db, List<string> ids)
    {
        // do operations
        db.Delete(ids);
        return new List<string>(); // return the errors

    }

    public override Client Get(ClientContext db, string id)
    {
        throw new NotImplementedException();
    }
}

public class ClientListModel
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public string Role { get; set; }
}

public class ClientViewModel
{
    public string Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public string Role { get; set; }
}

Exemple de controller :

 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
namespace EpControllerTemplate.Controllers
{
    public class Views
    {
        public static Dictionary<EpController.ViewName, string> ClientViews = new Dictionary<EpController.ViewName, string>();

        static Views()
        {
            ClientViews.Add(EpController.ViewName.Detail, "_ClientDetail");
            ClientViews.Add(EpController.ViewName.List, "Index" );
            ClientViews.Add(EpController.ViewName.SuccessSave, "Index");
            ClientViews.Add(EpController.ViewName.FailedSave, "_ClientDetail");
        }
    }

    public class ClientController : EpController.EpController<Client, ClientViewModel, ClientListModel, ClientContext>
    {

        public ClientController() : base(Views.ClientViews)
        {

        }

        public override void InitializeViewBag()
        {
            ViewBag.SomeData = "Some data";
        }
    }
}

Merci d’avance pour vos commentaires :)

+0 -0

Bon .. apres avoir testé je me suis rendu compte qu’une fonction abstraite ne peut etre static .. ce qui est logique .. donc je ne peut pas faire un model abstrait avec method static. Comme mon but etait d’imposer a nos dev une forme pour le model , j’ai du changer mon code et ai mis des assertion sur l’existance de la fonction static sur le modele ainsi que sur le type de retour ( try cast / catch assert() ).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
 // Get the TModel 'Get' method
 var method = typeof(TModel).GetMethod("Get");
 // Assert that the method exists
 AssertMethodExist(method, "Get");

 // try a 'TModel' cast
 try
 {
     TModel model = (TModel)(method.Invoke(null, new object[] { db, id }));
     return View(ViewBuilder_[ViewName.Detail].Keys.FirstOrDefault(), model);

  }catch(Exception ex)
  {
      Debug.Assert(false, "The model 'Get' function must return a 'TModel' ");
      return null;
  }
+0 -0

J’ai reussi a reduire encore plus le code requis dans les controlleur concret en creant une classe ViewBuilder et en ajoutant un attribut static dans la EpController.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class ClientController : EpController.EpController<Client, ClientViewModel, ClientListModel, ClientContext>
    {
        static ClientController()
        {
            ViewBuilder_.AddView(EpController.ViewName.List, "Index");
            ViewBuilder_.AddView(EpController.ViewName.Detail, "Detail");
            ViewBuilder_.AddView(EpController.ViewName.SuccessSave, "Index" , true);
            ViewBuilder_.AddView(EpController.ViewName.FailedSave, "Detail" );
        }

        public ClientController() : base(){}

        public override void InitializeViewBag()
        {
            ViewBag.SomeData = "Some data";
        }
    }

C’est maintenant tout ce qui est nécéssaire pour avoir un controlleur fonctionnel sans avoir a réécrire du code dans chaque controlleur qui se repetent et de plus on s’assure que tous les dev utilisent les model et la vue de la meme facon ( pas le choix vu le retour du controlleur et les assert sur la forme du model ).

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte