O braciach QueryString i ActionParameters

W .net ASP.MVC są różne mechanizmy. Są też takie, które umożliwiają zbadanie argumentów przesłanych do akcji, jak i argumentów oczekiwanych w akcji. I właśnie o nich dzisiaj. Można je wykorzystywać na dobry i zły sposób, jak każde narzędzie. Najpierw mały pokaz a potem filozofowanie. Mamy taki kod:

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #a31515; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }

   1:  public class HomeController : Controller
   2:  {
   3:      public ActionResult Index()
   4:      {
   5:          return this.View("Index");
   6:      }
   7:   
   8:      public string MustHaveString(string text)
   9:      {
  10:          return text;
  11:      }
  12:   
  13:      public string CanHaveString(string text = "")
  14:      {
  15:          return text;
  16:      }
  17:   
  18:      public string MustHaveInt(int input)
  19:      {
  20:          return input.ToString();
  21:      }
  22:   
  23:      public string CanHaveInt(int input = 0)
  24:      {
  25:          return input.ToString();
  26:      }
  27:   
  28:      public string MustHaveBoth(string text, int input)
  29:      {
  30:          string r = string.Empty;
  31:          for (int i = 0; i < input; i++)
  32:          {
  33:              r += text;
  34:          }
  35:          return r;
  36:      }
  37:   
  38:      public string CanHaveBoth(string text = "just a text", int input = 7)
  39:      {
  40:          string r = string.Empty;
  41:          for (int i = 0; i < input; i++)
  42:          {
  43:              r += text;
  44:          }
  45:          return r;
  46:      }
  47:   
  48:      public string Greed(string text, int input, int wantSoMuch, int stillNotUsing, int itAll)
  49:      {
  50:          return text + input;
  51:      }
  52:  }

Idąc od góry oczekujemy stringa, następnie oczekujemy stringa, ale mamy wartość domyślną na wypadek nie przesłania do przez użytkownika. Dalej to samo z intem, obowiązkowo oraz opcjonalnie. Potem wariacja z dwoma parametrami – obowiązkowo i opcjonalnie. I na koniec strasznie poznańska metoda, która potrzebuje wielu paramentrów, ale wykorzystuje tylko kilka z nich. Jeszcze jeszcze index, którego wyświetlenie wygląda tak:

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #a31515; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }

   1:  <div class="container">
   2:      <div class="row">
   3:          <div class="col-md-6">@Html.ActionLink("Must have a string with string set", "MustHaveString", "Home", new { text = "lorem ipsum" }, new { @class = "btn btn-default" })</div>
   4:      </div>
   5:      <div class="row">
   6:          <div class="col-md-6">@Html.ActionLink("Must have a string with string not", "MustHaveString", "Home", new {}, new { @class = "btn btn-default" })</div>
   7:      </div>
   8:      <div class="row">
   9:          <div class="col-md-6">@Html.ActionLink("Can have a string with string set", "CanHaveString", "Home", new { }, new { @class = "btn btn-default" })</div>
  10:      </div>
  11:      <div class="row">
  12:          <div class="col-md-6">@Html.ActionLink("Can have a string with string not set", "CanHaveString", "Home", new {text="litwo ojczyzno moja" }, new { @class = "btn btn-default" })</div>
  13:      </div>
  14:      <hr/>
  15:      <div class="row">
  16:          <div class="col-md-6">@Html.ActionLink("Must have a int with int set", "MustHaveInt", "Home", new { input = 42 }, new { @class = "btn btn-default" })</div>
  17:      </div>
  18:      <div class="row">
  19:          <div class="col-md-6">@Html.ActionLink("Must have a int with int not", "MustHaveInt", "Home", new { }, new { @class = "btn btn-default" })</div>
  20:      </div>
  21:      <div class="row">
  22:          <div class="col-md-6">@Html.ActionLink("Can have a int with int set", "CanHaveInt", "Home", new { input=667 }, new { @class = "btn btn-default" })</div>
  23:      </div>
  24:      <div class="row">
  25:          <div class="col-md-6">@Html.ActionLink("Can have a int with int not set", "CanHaveInt", "Home", new { }, new { @class = "btn btn-default" })</div>
  26:      </div>
  27:      <hr/>
  28:      <div class="row">
  29:          <div class="col-md-6">@Html.ActionLink("Must have both set", "MustHaveBoth", "Home", new { text="plonie ogniosko", input=3}, new { @class = "btn btn-default" })</div>
  30:      </div>
  31:      <div class="row">
  32:          <div class="col-md-6">@Html.ActionLink("Must have both not", "MustHaveBoth", "Home", new { }, new { @class = "btn btn-default" })</div>
  33:      </div>
  34:      <div class="row">
  35:          <div class="col-md-6">@Html.ActionLink("Must have both set too much", "MustHaveBoth", "Home", new { text="a",input="123", x="co ja tu"}, new { @class = "btn btn-default" })</div>
  36:      </div>
  37:      <hr/>
  38:      <div class="row">
  39:          <div class="col-md-6">@Html.ActionLink("Greed only the using ones", "Greed", "Home", new { text = "a", input = "123" }, new { @class = "btn btn-default" })</div>
  40:      </div>
  41:      <div class="row">
  42:          <div class="col-md-6">@Html.ActionLink("Greed all the things", "Greed", "Home", new { text = "a", input = "123", wantSoMuch = -1, stillNotUsing = -1, itAll =-1}, new { @class = "btn btn-default" })</div>
  43:      </div>
  44:  </div>

Czyli razor w linkami do poszczególnych akcji, czasem w linku są wymagane argumenty czasem ich brak. Wygląda to tak:

Mała niespodzianka, która wyszła podczas pisania tego posta. Okazuje się, że gdy na liście argumentów jest string, ale nie zostanie przesłany i nawet jeśli nie posiada on domyślnej wartości to i tak pewne mechanizmy asp zadziałają i zostanie wywołana akcja zdefiniowana w kontrolerze. Ta sama sztuczka z int’em już nie zadziała. Zostanie wyświetlmy zółty ekran błędu, informujący o tym, że nie udało się połączyć wszystkich kropek i jest klops. Ale to nie o tym wpis – ot taka ciekawostka.

W kontrolerze możemy nadpisać wywołanie metody, np. takiej metody w taki sposób:

   1:  protected override void OnActionExecuting(ActionExecutingContext filterContext)
   2:  {
   3:      Debug.WriteLine(
   4:          string.Format("***** New Action *****n***** {0} *****", filterContext.ActionDescriptor.ActionName));
   5:      Debug.WriteLine("*** Action parameteres {0}***", filterContext.ActionParameters.Count);
   6:      filterContext.ActionParameters.ForEach(
   7:          ap => Debug.WriteLine(string.Format("Key: {0}tValue: {1}", ap.Key, ap.Value)));
   8:   
   9:      var queryString = filterContext.RequestContext.HttpContext.Request.QueryString;
  10:      Debug.WriteLine("*** Query string {0}***", queryString.AllKeys.Count());
  11:      queryString.AllKeys.ForEach(k =>
  12:          Debug.WriteLine(string.Format("Key: {0}tValue: {1}", k, queryString[k])));
  13:  }

I teraz docieramy do sedna wpisu, będziemy oglądać QueryString i ActionParameters. Dla starych wyjadaczy różnica pewnie jasna i oczywista, ja się poznałem z nimi dopiero ostatnio, oryginalni autorzy świetnie się spisali przeinaczając (jest takie słowo) ich cel: otóż, wymyślił sobie ktoś że skoro w projekcie jest bardzo dużo kontrolerów, to pewnie znajdzie się część wspólna, którą warto umieścić w jakimś bazowym dla projektu kontrolerze. Sanity check – OK. Wszystko mogło być dobrze, do momenty gdy ktoś inny, nie robiłem git blame, nie wpadł na pomysł aby sprawdzać czy argumenty się zgadzają i sprawdzać, czy action paramentes zawiera np projectId, który ma zostać wysłany od użytkownika. Sanity check – NOK. Każdy kontroler dziedziczący ten wspólny, musi mieć na liście argumentów każdej ze swojej akcji parametr “int projectId”. Co przeciwnym wypadku? Oczywiście że przekierowanie na InvalidUrl i uwaga, strona ta w żaden sposób nie informuje o tym, czego oczekiwała, ani nic. Po prostu invalid url. Sanity check – Reject!
Dlaczego mnie to tak drażni? Bo jest to ukrywanie zależności danej metody, tworzę albo pracuje z taki kodem i gdy na liście argumentów mam nie używane parametry, to je usuwam i oczekuje że kod nadal będzie działać.
Zobaczcie jak wygląda wywołanie poszczególnych linków:
***** New Action *****
***** MustHaveString *****
*** Action parameteres 1***
Key: text    Value: lorem ipsum
*** Query string 1***
Key: text    Value: lorem ipsum

***** New Action *****
***** MustHaveString *****
*** Action parameteres 1***
Key: text    Value:
*** Query string 0***

***** New Action *****
***** CanHaveString *****
*** Action parameteres 1***
Key: text    Value:
*** Query string 0***

***** New Action *****
***** CanHaveString *****
*** Action parameteres 1***
Key: text    Value: litwo ojczyzno moja
*** Query string 1***
Key: text    Value: litwo ojczyzno moja

***** New Action *****
***** MustHaveInt *****
*** Action parameteres 1***
Key: input    Value: 42
*** Query string 1***
Key: input    Value: 42

***** New Action *****
***** MustHaveInt *****
*** Action parameteres 1***
Key: input    Value:
*** Query string 0***

***** New Action *****
***** CanHaveInt *****
*** Action parameteres 1***
Key: input    Value: 667
*** Query string 1***
Key: input    Value: 667

***** New Action *****
***** CanHaveInt *****
*** Action parameteres 1***
Key: input    Value: 0
*** Query string 0***

***** New Action *****
***** MustHaveBoth *****
*** Action parameteres 2***
Key: text    Value: plonie ogniosko
Key: input    Value: 3
*** Query string 2***
Key: text    Value: plonie ogniosko
Key: input    Value: 3

***** New Action *****
***** MustHaveBoth *****
*** Action parameteres 2***
Key: text    Value:
Key: input    Value:
*** Query string 0***

***** New Action *****
***** MustHaveBoth *****
*** Action parameteres 2***
Key: text    Value: a
Key: input    Value: 123
*** Query string 3***
Key: text    Value: a
Key: input    Value: 123
Key: x    Value: co ja tu

***** New Action *****
***** Greed *****
*** Action parameteres 5***
Key: text    Value: a
Key: input    Value: 123
Key: wantSoMuch    Value:
Key: stillNotUsing    Value:
Key: itAll    Value:
*** Query string 2***
Key: text    Value: a
Key: input    Value: 123

***** New Action *****
***** Greed *****
*** Action parameteres 5***
Key: text    Value: a
Key: input    Value: 123
Key: wantSoMuch    Value: -1
Key: stillNotUsing    Value: -1
Key: itAll    Value: -1
*** Query string 5***
Key: text    Value: a
Key: input    Value: 123
Key: wantSoMuch    Value: -1
Key: stillNotUsing    Value: -1
Key: itAll    Value: -1

O ile pierwsze z nich są w porządku, bo korzystają z tego czego wymagają, o tyle mały smród na końcu, niegodziwy zachłanny gostek chce aż pięciu parametrów, podczas gdy wykorzystuje tylko dwa z pięciu. Na szczęcie HomeController nie został jeszcze dotknięty ideą sprawdzania action parameters, ale jeśli ktoś wpadnie na pomysł napisania takiego kodu:

   1:  protected override void OnActionExecuting(ActionExecutingContext filterContext)
   2:  {
   3:      if (!filterContext.ActionParameters.Keys.Contains("itAll"))
   4:      {
   5:          Response.Redirect("http://jstadnicki.blogspot.com/");
   6:      }
   7:  }

To wtedy musicie dostarczyć mieć na liście argumentów metody, uwaga napiszę to jeszcze raz: sygnatura akcji musi mieć “itAll” na liście argumentów aby w ogóle się do niej dostać. Czyli wywołanie localhost/home/index?itAll=”dzialaj” nie zadziała, ponieważ index nie ma itAll na liście parametrów, a to że został on przesłany w query string nie pomoże.
Kończąc mój długi wpis, który miał być krótki. Nie ukrywajcie zależności w kodzie, zgłaszając błąd czy to użytkownikowi czy innemu programiście, podajcie wystarczająco dużo informacji, aby od razu jasne było dlaczego coś się nie udało. I dodając kod do klasy bazowej pamiętajcie o tym, jak wielki wpływ na inne części aplikacji będzie miała wasza zmiana. Dziedziczenie nie jest rozwiązaniem. I ostatnie jeszcze ActionParameters!!!!!!!!!!!!!!!=QueryString