ASP.NET MVC a Ajax

Kiedyś już o tym pisałem pobieżnie (kilka lat temu), ale dzisiaj chciałbym pokazać inny przykład wraz ze wszystkimi możliwymi parametrami. Stwórzmy najpierw  klasyczny formularz i kontroler:

<div id="dataView">
    @Html.Action("GetData")
</div>

@using (Html.BeginForm())
{
    
    @Html.DropDownList("selectedGroup",new SelectList(new []{"All","GroupA","GroupB"}))
    <br/>
    <input type=submit  value="Refresh"/>

}

Prosty formularz z ComboBox i przyciskiem, który powoduje ponowne załadowanie strony oraz wykonanie metody GetData z nowym parametrem:

public ActionResult GetData(string selectedGroup)
{
  var data=new List<string>();

  for (int i = 0; i < 5; i++)
  {
      data.Add(string.Format("{0}: {1}",DateTime.Now.ToLongTimeString(),selectedGroup));
      Thread.Sleep(500);
  }

  return PartialView("DataView", data);
}

DataView to widok wyświetlający po prostu kolekcje danych:

@foreach (var item in Model)
{
    <p>@item</p>
}

Wykonanie powyższego formularza w sposób asynchroniczny jest bardzo proste i nie wymagania nawet pisania JavaScript:

<div id="dataView">
    
</div>

@using (Ajax.BeginForm("GetData",new AjaxOptions(){UpdateTargetId = "dataView"}))
{
    
    @Html.DropDownList("selectedGroup",new SelectList(new []{"All","GroupA","GroupB"}))
    <br/>
    <input type=submit  value="Refresh"/>
}

Przykładowy kod korzysta z helpera Ajax zamiast Html. Zaglądając do wygenerowanego kodu zobaczymy:

<div id="dataView">
    
</div>

<form action="/Home/GetData" data-ajax="true" data-ajax-mode="replace" data-ajax-update="#dataView" id="form0" method="post"><select id="selectedGroup" name="selectedGroup"><option>All</option>
<option>GroupA</option>
<option>GroupB</option>
</select>    <br/>
    <input type=submit  value="Refresh"/>
</form>

Warto zwrócić uwagę na wygenerowane atrybuty data-ajax. Aby kod działał należy podlinkować tzw. unobtrusive ajax. Skrypty są domyślnie zdefiniowane jako bundle więc każdy nowy projekt powinien już je mieć.

Powyższy kod nie jest jednak  idealny. W przypadku gdy skrypty są zablokowane w przeglądarce, formularz po prostu przekieruje nas do widoku częściowego zamiast wstrzyknąć go w zdefiniowanym div. Wystarczy jednak skorzystać z właściwości Url i przekazać adres akcji:

<div id="dataView">
    @Html.Action("GetData")
</div>

@using (Ajax.BeginForm(new AjaxOptions(){UpdateTargetId = "dataView",Url = Url.Action("GetData")}))
{
    
    @Html.DropDownList("selectedGroup",new SelectList(new []{"All","GroupA","GroupB"}))
    <br/>
    <input type=submit  value="Refresh"/>
}

Powyższy kod w sytuacji gdy JavaScript jest włączony, wykona asynchroniczne zapytanie, a w przeciwnym razie formularz zachowa się w klasyczny sposób i przeładuje całą stronę.

Kolejną ciekawą właściwością jest LoadingElementId. Gdy operacja trwa dość długo, wtedy prawdopodobnie chcemy wyświetlić jakiś komunikat:

<div id="dataView">
    @Html.Action("GetData")
</div>

<div id="loadingPanel" style="display:none">
<p>Loading...</p>
</div>

@using (Ajax.BeginForm(new AjaxOptions(){LoadingElementId = "loadingPanel",UpdateTargetId = "dataView",Url = Url.Action("GetData")}))
{
    
    @Html.DropDownList("selectedGroup",new SelectList(new []{"All","GroupA","GroupB"}))
    <br/>
    <input type=submit  value="Refresh"/>
}

Jeśli nie chcemy wykonywać zapytania bez zgody użytkownika wtedy możemy skorzystać z Confirm:

@using (Ajax.BeginForm(new AjaxOptions(){Confirm ="Do you want to reload data?",LoadingElementId = "loadingPanel",UpdateTargetId = "dataView",Url = Url.Action("GetData")}))
{
    
    @Html.DropDownList("selectedGroup",new SelectList(new []{"All","GroupA","GroupB"}))
    <br/>
    <input type=submit  value="Refresh"/>
}

Po naciśnięciu przycisku zostanie wyświetlony MessageBox z pytaniem i dwoma przyciskami (Yes, No).

Możemy również kontrolować jak dane będą aktualizowane. Domyślnie są one zastępowane nowymi ale możemy te zachowanie określić za pomocą InsertionMode. Enum przyjmuje wartości InsertAfter, InsertBefore albo Replace. Ostatnią właściwością jest HttpMethod przyjmujący GET albo POST jako wartość.

W analogiczny sposób można również tworzyć ajaxowe linki:

@Ajax.ActionLink("Fetch data","GetData",new{selectedGroup="GroupB"},new AjaxOptions(){LoadingElementId = "loadingPanel",UpdateTargetId = "dataView"})

Tak jak w przypadku formularzy, wystarczy użyć specjalnego helper’a (Ajax).

Do dyspozycji są również callbacki w JavaScript. Czasami chcemy obsłużyć zapytania po stronie klienta. W takich przypadkach, możemy skorzystać z dostarczonych zdarzeń:

<script type="text/javascript">
    function OnBegin() {
        alert("OnBegin");
    }
    function OnSuccess(data) {
        alert("OnSuccess: " + data);
    }
    function OnFailure(request, error) {
        alert("OnFailure: " + error);
    }
    function OnComplete(request, status) {
        alert("OnComplete: " + status);
    }
</script>


@Ajax.ActionLink("Fetch data", "GetData", new { selectedGroup = "GroupB" }, new AjaxOptions()
{
    OnBegin = "OnBegin",
    OnFailure="OnFailure",
    OnComplete = "OnComplete",
    OnSuccess = "OnSuccess",
    LoadingElementId = "loadingPanel", UpdateTargetId = "dataView"
})

Kompresja danych w .NET 4.5 – klasa ZipFile

W starszych wersjach .NET, do dyspozycji był m.in. strumień GZipStream. Do prostych zastosować wystarczał ale zwykle, trzeba było opakowywać go w swoje klasy, aby np. skompresować cały folder.

Od .NET 4.5 mamy klasę ZipFile, która dostarcza kilka przydatnych metod. Najpierw należy dodać referencję do System.IO.Compression.FileSystem, a potem wystarczy skorzystać jednej ze statycznych metod, np.:

string startPath = @"c:\setup\ToZip";
string zipPath = @"c:\setup\result.zip";
string extractPath = @"c:\setup\extracted";

ZipFile.CreateFromDirectory(startPath, zipPath);

ZipFile.ExtractToDirectory(zipPath, extractPath);

CreateFromDirectory dokonuje kompresji wskazanego folderu. Z kolei ExtractToDirectory dekompresuje dany plik zip. API jest naprawdę proste i przejrzyste.

Do dyspozycji mamy również metodę Open, służącą do otworzenia archiwum w celu modyfikacji lub po prostu odczytu plików.

Przykład:

using (ZipArchive archive = ZipFile.Open(zipPath, ZipArchiveMode.Read))
{
 foreach (var entry in archive.Entries)
 {
     Console.WriteLine(entry.Name);
     Console.WriteLine(entry.LastWriteTime);
 }
}    

Możliwe jest również dodanie pliku do już istniejącego archiwum:

string zipPath = @"c:\users\exampleuser\start.zip";
string newFile = @"c:\users\exampleuser\NewFile.txt";

using (ZipArchive archive = ZipFile.Open(zipPath, ZipArchiveMode.Update))
{
    archive.CreateEntryFromFile(newFile, "NewEntry.txt");
}

Warto przyjrzeć się dostępnym metodom, ale generalnie większość operacji, których potrzebujemy przy pracy z kompresją plików jest dostępnych. Możliwe jest np. wypakowanie pojedynczego pliku:

using (ZipArchive archive = ZipFile.Open(zipPath, ZipArchiveMode.Read))
{
 foreach (var entry in archive.Entries)
 {
    entry.ExtractToFile(Path.GetTempPath());
 }
}             

Co prawda od dawna były dostępne już biblioteki dla C#, ale fajnie, że nie trzeba już szukać ich po za standardową biblioteką.

HybridDictionary oraz ListDictionary

Najczęściej programiści korzystają z klasy Dictionary ale warto rozważyć dwie inne kolekcje. ListDictionary przechowuje dane na liście jednokierunkowej. Dla słowników z kilkoma kluczami <10 zwykle jest to szybsze niż standardowy hash table, wykorzystywany w Dictionary. Szybciej jest po prostu przejść przez 10 elementów, niż liczyć skomplikowaną funkcję haszującą. W przypadku hash table, również należy zwrócić uwagę na konflikty – sytuacje gdy funkcja haszująca zwróciła ten sam skrót, dla dwóch różnych kluczy.

Operacje mają złożoność O ( n ) dla ListDictionary ponieważ trzeba za każdym razem przejść przez całą listę. HybridDictionary to  hybryda dwóch rozwiązań. Jeśli liczba kluczy jest niska, wykorzystywana jest lista jednokierunkowa. W przeciwnym wypadku, tworzony jest hash table.

Napiszmy prosty benchmark:

class Program
{
   static void Main(string[] args)
   {
       const int n = 30;

       string[] keys = GetKeys(n).ToArray();

       var hybridDictResults = RunTests((i) => new HybridDictionary(i), n, keys);
       var listDictionaryResults = RunTests((i) => new ListDictionary(), n, keys);

   }
   private static double[] RunTests(Func<int, IDictionary> dictionaryFactory, int n, string[] keys)
   {
       double[] results = new double[n];
       const int tests = 100;

       for (int i = 1; i <= n; i++)
       {
           var subKeys = keys.Take(i).ToArray();
           var hybridDictionary = PopulateDictionary(dictionaryFactory(i), subKeys);
           double averageTicks = 0;
           
           for (int test = 0; test < tests; test++)
           {
               Stopwatch stopwatch = Stopwatch.StartNew();

               foreach (string key in subKeys)
               {
                   var value = hybridDictionary[key];
               }

               long ticks = stopwatch.ElapsedTicks;
               averageTicks += ticks;
           }
           results[i - 1] = (double)averageTicks/tests;
       }

       return results;
   }
   static IDictionary PopulateDictionary(IDictionary dictionary, IEnumerable<string> keys)
   {
       foreach (var key in keys)
           dictionary.Add(key, true);

       return dictionary;
   }
   static IEnumerable<string> GetKeys(int n)
   {
       for (int i = 0; i < n; i++)
       {
           yield return Path.GetRandomFileName();
       }
   }
}

Wartości dla pierwszych 30 próbek przedstawiają się następująco:

image

Jak widać, powyższej 10-15 próbek, ListDictionary jest kompletnie nieopłacalny. Z tego względu dużo bezpieczniej jest użyć HybridDictionay lub po prostu Dictionary. Istnieją jednak przypadki, gdzie ListDictionary jest dobrym rozwiązaniem. Jeśli kluczami są np. cyfry w systemie dziesiętnym wtedy ograniczenie liczby kluczy do 10 jest oczywiście naturalne i poprawne.

LazyInitializer oraz Lazy<T>

.NET zawiera klasy przeznaczone do leniwej inicjalizacji i zwykle nie trzeba samemu implementować tego wzorca. Jeśli jakiś obiekt, chcemy inicjalizować dopiero w momencie, gdy jest on potrzebny to można skorzystać ze wspomnianych typów.

Lazy<T> to typ generyczny i wystarczy w konstruktorze przekazać metodę tworzącą obiekt:

var largeObject = new Lazy<LargeObject>(() =>
{
 Thread.Sleep(5000);
 Console.WriteLine("Inicjalizacja...");
 return new LargeObject();
});

var instance1 = largeObject.Value;
var instance2 = largeObject.Value;

W momencie pierwszego wywołania właściwości Value, obiekt zostanie utworzony, co spowoduje wyświetlenie napisu “Inicjalizacja…”. Kolejne odwołania do właściwości będą już korzystały z tej samej, zbuforowanej wartości. Możliwe jest również stworzenie obiektu Lazy, bez przekazania metody inicjalizacyjnej:

var largeObject = new Lazy<LargeObject>();

W takim przypadku, po prostu domyślny konstruktor zostanie wywołany.

Kolejnym parametrem jaki możemy przekazać w konstruktorze jest isThreadSafe:

var largeObject = new Lazy<LargeObject>(isThreadSafe:false);

Task.Factory.StartNew(() => largeObject.Value);
Task.Factory.StartNew(() => largeObject.Value);
Task.Factory.StartNew(() => largeObject.Value);

Console.ReadLine();

Jeśli isThreadSafe ustawiony jest na false, to znaczy, że instancja nie jest bezpieczna z punktu widzenia wielowątkowości. Z tego względu, konstruktor zostanie wywołany osobno w każdym z wątków. Jeśli przekażemy wartość true, wtedy obiekt będzie współdzielony przez wszystkie wątki.

Podobną funkcję pełni parametr LazyThreadSafetyMode:

var largeObject = new Lazy<LargeObject>(LazyThreadSafetyMode.ExecutionAndPublication);

LazyThreadSafetyMode posiada jedną trzech wartości. Można przekazać NONE, co powinno być wykorzystywane wyłącznie, gdy znana jest kolejność inicjalizacji. Jest to najszybszy z dostępnych trybów, ale nie gwarantuje on spójności danych – efekt jest niezdefiniowany. Z kolei ExecutionAndPublication spowoduje ten sam efekt, co isThreadSafe ustawiony na true – wyłącznie jeden obiekt zostanie stworzony. W tym przypadku blokady są wykorzystywane aby zsynchronizować wywołania. PublicationOnly pełni analogiczną funkcję do isThreadSafety false (każdy wątek tworzy własną kopię).

Kolejną klasą, przydatną w środowisku współbieżnym jest LazyInitializer. Zacznijmy od przykładu:

LargeObject largeObject = null;

LazyInitializer.EnsureInitialized(ref largeObject);
Console.WriteLine(largeObject.GetHashCode());

LazyInitializer.EnsureInitialized(ref largeObject);
Console.WriteLine(largeObject.GetHashCode());

Klasa sprawdza, czy obiekt jest już zainicjalizowany. Jeśli tak to po prostu go zwraca. W przeciwnym wypadku zostanie wywołany domyślny konstruktor. Operacja jest oczywiście thread-safe. Za pomocą LazyInitializer możemy w bezpieczny sposób zadbać, że tylko jedna instancja zostanie utworzona w środowisku wielowątkowym.

Tak jak w przypadku Lazy<T> można przekazać własną fabrykę:

LazyInitializer.EnsureInitialized(ref largeObject,()=>new LargeObject());

Diagnostyka aplikacji webowych – Glimpse

Dzisiaj chciałbym zaprezentować bibliotekę Glimpse, służącą do wyświetlenia informacji m.i.n o czasie i liczbie wykonanych zapytań. Doskonale nadaje się do sprawdzenia, które strony wykonują się dłużej niż powinny. Warto zaważyć, że Glimpse wyświetla wszystkie te informacje  za pomocą HTML a nie dodatkowego plugina w przeglądarce.

Instalacja jest bardzo prosta – przechodzimy do NuGet i wybieramy po prostu Glimpse:

image

Następnie, wystarczy, że po zainstalowaniu przejdziemy do http://localhost:58935/Glimpse.axd, a wyświetli się panel konfiguracyjny:

image

Włączamy glimpse i od teraz odwiedzając każdą stronę zobaczymy wstrzyknięty panel Glimpse (na samym dole):

image

Jak widać, mamy informacje o czasach wykonania poszczególnych części (widok, akcja) jak i całego zapytania. Panel pokazuje również nazwę kontrolera oraz akcji. To jednak dopiero początek jego możliwości, ponieważ można go otworzyć i pokażę się mnóstwo dodatkowych informacji:

image

Na przykład w zakładce Execution możemy zobaczyć przebieg wykonania zapytania:

image

Bardzo przydatnym narzędziem jest również Routes. Możemy podejrzeć tam zdefiniowany routing jak i dopasowane parametry (wraz z routingiem, który został użyty):

image

Cache z kolei przedstawia parametry buforowania – w praktyce naprawdę bardzo przydatne:

image

Timeline za to to pozwala w szybki sposób dowiedzieć się, które elementy aplikacji zajmują najwięcej czasu:

image

Z kolei w zakładce trace wyświetlane są informacje wygenerowane przez klasę Trace np.:

public ActionResult About()
{
  ViewBag.Message = "Your app description page.";

  Trace.Write("Test message");

  return View();
}

Kod spowoduje, że w Trace zostanie wyświetlona dana wiadomość:

image

Nie będę opisywał tutaj wszystkich zakładek ponieważ najlepiej po prostu ściągnąć i przetestować rozwiązanie na własnym projekcie.  Warto jednak wspomnieć jeszcze o zakładce Ajax, pozwalającej na sprawdzenie, jakie zapytania zostały wysłane.

Filtry w ASP.NET MVC

Filtry w ASP.NET MVC potrafią znaczącą polepszyć czytelność kodu, jeśli dana logika musi być wykonywana dla wielu akcji. Stanowią one tak naprawdę programowanie aspektowe, które jest całkowicie odmienne od powszechnie znanego programowania obiektowego. Ze względu na to, że programiści nie są przyzwyczajeni do tego modelu, należy uważać jak wykorzystuje się filtry.

Klasyczne zastosowanie aspektów czy filtrów to caching, logging czy autoryzacja. Problemy te nalezą do grupy tzw. cross-cutting concerns, czyli problemów, które występują w całej domenie.

W przypadku ASP.NET MVC mamy do czynienia z następującymi typami filtrów:

  1. IAuthorizationFilter – autoryzacja
  2. IActionFilter – umożliwia wstrzyknięcie logi przed i po wykonaniu danej akcji.
  3. IResultFilter – analogicznie do IActionFilter, ale filtr dotyczy rezultatu akcji.
  4. IExceptionFilter – pozwala wyłapać wyjątki.

Pierwszy filtr ma następującą sygnaturę:

public interface IAuthorizationFilter 
{
    void OnAuthorization(AuthorizationContext filterContext);
}

W praktyce, jak chcemy zaimplementować własną autoryzację, korzysta się z klasy bazowej AuthorizeAttribute:

namespace System.Web.Mvc
{
  /// <summary>
  /// Represents an attribute that is used to restrict access by callers to an action method.
  /// </summary>
  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
  public class AuthorizeAttribute : FilterAttribute, IAuthorizationFilter
  {
    /// <summary>
    /// When overridden, provides an entry point for custom authorization checks.
    /// </summary>
    /// 
    /// <returns>
    /// true if the user is authorized; otherwise, false.
    /// </returns>
    /// <param name="httpContext">The HTTP context, which encapsulates all HTTP-specific information about an individual HTTP request.</param><exception cref="T:System.ArgumentNullException">The <paramref name="httpContext"/> parameter is null.</exception>
    protected virtual bool AuthorizeCore(HttpContextBase httpContext);
    /// <summary>
    /// Called when a process requests authorization.
    /// </summary>
    /// <param name="filterContext">The filter context, which encapsulates information for using <see cref="T:System.Web.Mvc.AuthorizeAttribute"/>.</param><exception cref="T:System.ArgumentNullException">The <paramref name="filterContext"/> parameter is null.</exception>
    public virtual void OnAuthorization(AuthorizationContext filterContext);
    /// <summary>
    /// Processes HTTP requests that fail authorization.
    /// </summary>
    /// <param name="filterContext">Encapsulates the information for using <see cref="T:System.Web.Mvc.AuthorizeAttribute"/>. The <paramref name="filterContext"/> object contains the controller, HTTP context, request context, action result, and route data.</param>
    protected virtual void HandleUnauthorizedRequest(AuthorizationContext filterContext);
    /// <summary>
    /// Called when the caching module requests authorization.
    /// </summary>
    /// 
    /// <returns>
    /// A reference to the validation status.
    /// </returns>
    /// <param name="httpContext">The HTTP context, which encapsulates all HTTP-specific information about an individual HTTP request.</param><exception cref="T:System.ArgumentNullException">The <paramref name="httpContext"/> parameter is null.</exception>
    protected virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext);
    /// <summary>
    /// Gets or sets the user roles.
    /// </summary>
    /// 
    /// <returns>
    /// The user roles.
    /// </returns>
    public string Roles { get; set; }
    /// <summary>
    /// Gets the unique identifier for this attribute.
    /// </summary>
    /// 
    /// <returns>
    /// The unique identifier for this attribute.
    /// </returns>
    public override object TypeId { get; }
    /// <summary>
    /// Gets or sets the authorized users.
    /// </summary>
    /// 
    /// <returns>
    /// The authorized users.
    /// </returns>
    public string Users { get; set; }
  }
}

Jeśli w momencie wystąpienia wyjątku chcemy wyświetlić własną stronę, można tego dokonać za pomocą IExceptionFilter. Interfejs wygląda następująco:

namespace System.Web.Mvc 
{
    public interface IExceptionFilter 
    {
        void OnException(ExceptionContext filterContext);
    }
}

Przykładowa implementacja:

public class CustomExceptionAttribute : FilterAttribute, IExceptionFilter
{
   public void OnException(ExceptionContext filterContext)
   {
       if (!filterContext.ExceptionHandled &&
           filterContext.Exception is CustomApplicationException)
       {
           filterContext.Result = new RedirectResult("~/App/Debug");
           filterContext.ExceptionHandled = true;
       }
   }
}

Sprawa nie jest zbyt skomplikowana. Wystarczy zaimplementować metodę OnException i zdecydować jaka ma być następna akcja. Zastosowanie filtra jest bardzo proste:

[CustomException]
public class HomeController : Controller
{
   public ActionResult Index()
   {
       throw new CustomApplicationException();

       return View();
   }      
}

Można go doczepić zarówno do akcji jak i kontrolera. Warto zaznaczyć, że ASP.NET MVC ma już implementacje IExceptionFilter w formie klasy HandleError. Więcej o tej klasie napiszę w następnym poście.

Proszę również zwrócić uwagę na FilterAttribute. Pisząc własny filtr, należy dziedziczyć po tej klasie albo zaimplementować samemu interfejs IMvcFilter.

Przejdźmy do IActionFilter:

public interface IActionFilter 
{
    void OnActionExecuting(ActionExecutingContext filterContext);
    void OnActionExecuted(ActionExecutedContext filterContext);
}

Interfejs powinien być jasny – mamy dwie metody do wykonania logiki przed i po akcji. Klasycznym zastosowaniem filtra jest pomiar czasu wykonania akcji:

public class PerformanceMonitorAttribute : FilterAttribute, IActionFilter
{
   private Stopwatch _timer;

   public void OnActionExecuting(ActionExecutingContext filterContext)
   {
       _timer = Stopwatch.StartNew();
   }

   public void OnActionExecuted(ActionExecutedContext filterContext)
   {
       _timer.Stop();

       filterContext.HttpContext.Response.Write(string.Format("Czas wykonania akcji: {0}",_timer.ElapsedTicks));
   }
}

Przykład użycia:

public class HomeController : Controller
{
   [PerformanceMonitorAttribute]
   public ActionResult Index()
   {
       return View();
   }      
}

Ostatnim filtrem jest IResultFilter:

public interface IResultFilter 
{
    void OnResultExecuting(ResultExecutingContext filterContext);
    void OnResultExecuted(ResultExecutedContext filterContext);
}

Każda akcja zwraca ActionResult. W ASP.NET MVC istnieje wiele implementacji tej klasy. Możliwe jest przekierowanie do kolejnej akcji lub zwrócenie konkretnego widoku za pomocą metody View. OnResultExecuting jest wywołany przed wygenerowaniem rezultatu ale już po wykonaniu logiki akcji. W przypadku powyższej metody Index, OnResultExecuting zostanie wywołany tuż przed wykonaniem View(). Warto chyba pokazać jak wygląda abstrakcyjna klasa ActionResult:

public abstract class ActionResult
{
/// <summary>
/// Enables processing of the result of an action method by a custom type that inherits from the <see cref="T:System.Web.Mvc.ActionResult"/> class.
/// </summary>
/// <param name="context">The context in which the result is executed. The context information includes the controller, HTTP content, request context, and route data.</param>
    public abstract void ExecuteResult(ControllerContext context);
}

Innymi słowy, OnResultExecuting jest wywołany przed ActionResult.ExecuteResult a OnResultExecuted po. Analogiczna implementacja filtru sprawdzającego wydajność może wyglądać następująco:

public class PerformanceMonitorAttribute : FilterAttribute, IResultFilter
{
   private Stopwatch _timer;

   public void OnResultExecuting(ResultExecutingContext filterContext)
   {
       _timer = Stopwatch.StartNew();
   }

   public void OnResultExecuted(ResultExecutedContext filterContext)
   {
       _timer.Stop();

       filterContext.HttpContext.Response.Write(string.Format("Zwrocenie  wynikku akcji: {0}", _timer.ElapsedTicks));
   }
}

Ze względu, że bardzo często IResultFilter i IActionFilter mają podobne zastosowanie, framework dostarcza klasę bazową implementującą oba interfejsy:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public abstract class ActionFilterAttribute : FilterAttribute, IActionFilter, IResultFilter
{
    /// <summary>
    /// Called by the ASP.NET MVC framework before the action method executes.
    /// </summary>
    /// <param name="filterContext">The filter context.</param>
    public virtual void OnActionExecuting(ActionExecutingContext filterContext);
    /// <summary>
    /// Called by the ASP.NET MVC framework after the action method executes.
    /// </summary>
    /// <param name="filterContext">The filter context.</param>
    public virtual void OnActionExecuted(ActionExecutedContext filterContext);
    /// <summary>
    /// Called by the ASP.NET MVC framework before the action result executes.
    /// </summary>
    /// <param name="filterContext">The filter context.</param>
    public virtual void OnResultExecuting(ResultExecutingContext filterContext);
    /// <summary>
    /// Called by the ASP.NET MVC framework after the action result executes.
    /// </summary>
    /// <param name="filterContext">The filter context.</param>
    public virtual void OnResultExecuted(ResultExecutedContext filterContext);
}

Jeśli chcemy zastosować filtr dla kilku kontrolerów, najlepszym sposobem jest po prostu ich implementacja w osobnych klasach jak to zostało pokazane wyżej. Czasami jednak chcemy napisać filtr tylko dla pojedynczego kontrolera. W takich przypadkach łatwiejsze może okazać się po prostu przeładowanie metody w tym konkretnym kontrolerze:

public class HomeController : Controller
{
   [PerformanceMonitorAttribute]
   public ActionResult Index()
   {
       return View();
   }

   protected override void OnActionExecuted(ActionExecutedContext filterContext)
   {
       base.OnActionExecuted(filterContext);
   }

   protected override void OnActionExecuting(ActionExecutingContext filterContext)
   {
       base.OnActionExecuting(filterContext);
   }
}

W każdym kontrolerze można przeładować wszystkie wcześniej opisane metody.

Jeśli natomiast chcemy zastosować filtr do wszystkich metod, wtedy lepiej zdefiniować to globalnie:

public class FilterConfig
{
   public static void RegisterGlobalFilters(GlobalFilterCollection filters)
   {
       filters.Add(new HandleErrorAttribute());
   }
}

Jeśli projekt został automatycznie wygenerowany to wystarczy dodać filtr w RegisterGlobalFilter. Należy jednak upewnić się, że metoda jest wywoływana przy starcie aplikacji:

public class MvcApplication : System.Web.HttpApplication
{
   protected void Application_Start()
   {
       AreaRegistration.RegisterAllAreas();

       WebApiConfig.Register(GlobalConfiguration.Configuration);
       FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
       RouteConfig.RegisterRoutes(RouteTable.Routes);
       BundleConfig.RegisterBundles(BundleTable.Bundles);
       AuthConfig.RegisterAuth();
   }
}

Zwykle to nie powinno mieć znaczenia, ale istnieje możliwość wykonywania filtrów w konkretnym porządku. Służy do tego właściwość Order:

[PerformanceMonitorAttribute(Order = 1)]
[AnotherPerformanceMonitorAttribute(Order = 2)]
public ActionResult Index()
{
    return View();
}

ASP.NET MVC – caching za pomocą DonutOutputCache

Kilka wpisów wcześniej pisałem o atrybucie OutputCache. Dla prostych zastosowań sprawdza się dość dobrze, ale bardzo szybko można dostrzec jego ograniczenia i błędy. Przedstawmy najpierw kilka niedogodności związanych z OutputCache.

Załóżmy, że mamy kontroler z akcją ShowTime:

[OutputCache(Duration = 5*60)]
public ActionResult ShowTime()
{
  return View();
}

Widok zawiera po prostu aktualny czas:

@DateTime.Now.ToString()

Następnie, gdzieś na stronie renderujemy powyższą akcję:

@Html.Action("ShowTime")

Na razie wszystko działa tak jak chcemy – zawartość akcji jest buforowana co 5 minut. Problem polega na tym, że implementacja w MVC nie radzi sobie dobrze z buforowaniem akcji zagnieżdżonych (child actions), takimi jak ta powyższa. Na przykład, nie wspierane są profile zdefiniowane w web.config:

<outputCacheSettings>
   <outputCacheProfiles>
       <add name="CustomProfile" duration="20" varyByParam="*" />
   </outputCacheProfiles>
</outputCacheSettings>
</caching>

Ustawienie atrybutu w następujący sposób po prostu nie zadziała:

[OutputCache(CacheProfile = "CustomProfile"]
public ActionResult ShowTime()
{
  return View();
}

Analogicznie zablokowanie buforowania w web.config działa wyłącznie dla akcji głównych a nie zagnieżdżonych:

 <outputCache enableOutputCache="false"/>

Innym wyzwaniem z jakim spotkałem się to buforowanie całej strony z wyjątkiem pewnego fragmentu np. nazwy użytkownika. Dekorując OutputCache główną akcję, nie mamy możliwości usunięcia z buforowania jakichkolwiek akcji potomnych. Na szczęście możemy ściągnąć pakiet MvcDonutOutput , który rozwiązuje powyższe problemy:

image

Sposób użycia jest analogiczny do standardowego OutputCache:

[DonutOutputCache(CacheProfile = "CustomProfile")]
public ActionResult ShowTime()
{
    return View();
}

Do dyspozycji mamy te same parametry i oczywiście teraz profile zdefiniowane w web.config działają również dla akcji potomnych.

Biblioteka rozszerza również metodę Action o argument umożliwiający nam wyłączenie specyficznej akcji z buforowania:

public static MvcHtmlString Action(this HtmlHelper html, string actionName, 
                                   string controllerName, RouteValueDictionary routeValues, 
                                   bool excludeFromParentCache)

Przykład:

@Html.Action("LoginDetails", "Account", true)

Ustawienie flagi na true spowoduje, że akcja LoginDetails nie będzie buforowana. Częstym scenariuszem jest, że chcemy buforować całą główną akcję z wyjątkiem np. jednej pokazującej dane zmieniające się bardzo często.

Możliwe jest również programowe usunięcie poszczególnych składowych z bufora:

var cacheManager = new OutputCacheManager();
 
//usuwa akcje Index z kontrolera Home
cacheManager.RemoveItem("Home", "Index");
 
// usuwa wszystkie pozycje
cacheManager.RemoveItems(); 

Myślę, że warto zastąpić OutputCache atrybutem DonutOutputCache w swoich projektach. Wcześniej czy później, każdy spotka się ze wspomnianymi ograniczeniami.

ASP.NET MVC: Cross-Site Request Forgery

CSRF jest dzisiaj bardzo dobrze znanym atakiem, ale niestety wciąż wiele aplikacji internetowych pozostaje niezabezpieczonych. W poście nie będę opisywał szczegółowo CSRF ponieważ w Internecie jest już od dawna mnóstwo informacji o tym. Chciałbym jednak pokazać jak dzięki ASP.NET MVC możemy w łatwy sposób uchronić się przed atakiem.

W MVC standardowy formularz tworzymy w następujący sposób:

@using (Html.BeginForm("Manage", "Account")) {

    <fieldset>
        <legend>Change Email Form</legend>
        <ol>
            <li>
                @Html.LabelFor(m => m.Email)
                @Html.TextBoxFor(m => m.Email)
            </li> 
        </ol>
        <input type="submit" value="Change email" />
    </fieldset>
}

Następnie mamy w kontrolerze akcję implementującą obsługę formularza, np.:

[Authorize]
public ActionResult Manage(LocalPasswordModel model)
{
  // pernamenta zmiana stanu np. poprzez modyfikację bazy danych.
  
  return View(model);
}

Akcja oznaczona jest atrybutem Authorize, co oznacza, że tylko zalogowany użytkownik może ją wywołać. Atak CSRF polega na wykonaniu jakieś czynności w kontekście innego użytkownika. W tym przypadku mamy formularz do zmiany adresu email, co oczywiście powinno być wykonane wyłącznie w kontekście zalogowanego użytkownika.

Powyższy formularz jest podatny na CSRF ponieważ ktoś może podesłać użytkownikowi fałszywy formularz tzn.:

<form id="fm1" action="http://test.com/Account/Manage" method="post">
   <input name="Email" value="newEmail@domain.com" />
</form>

Jeśli użytkownik aktualnie zalogowany wywoła powyższy formularz, akcja zostanie wykonana w jego kontekście. Innymi słowy, email zostanie zmieniony bez jego wiedzy. Jedynym wyzwaniem to przekonanie użytkownika, aby wykonał powyższy formularz. Nie trzeba nawet go  jawnie wywoływać bo można to zrobić za pomocą JavaScript w metodzie OnLoad. Potrzebne jest jednak przekonanie użytkownika, aby otworzył daną stronę z podrobionym formularzem.

Pomimo, że nie da się przeprowadzić ataku bez “pomocy” samego użytkownika to takie zaprojektowanie systemu jest bardzo niebezpieczne. W końcu nie zawsze chodzi tu o jawne odpalenie strony, ale jak występuje luka XSS to wystarczy umieścić na stronie np. wcześniej podrobiony obrazek. Aplikacja musi być zaimplementowana w taki sposób,  że dana akcja zostanie wykonana zawsze w prawidłowym kontekście.

Standardowe rozwiązanie to wygenerowanie tokena i umieszczenie go w ukrytym polu formularza. Gdy użytkownik wysyła taki formularz, wtedy porównuje się token z ukrytego pola z wcześniej zapisanym tokenem (wzorcem) w sesji. Jeśli tokeny są takie same, wtedy wiemy,  że formularz to dokładnie ten sam, który sami wygenerowaliśmy. Nikt nie przeprowadzi ataku CSRF ponieważ tokenu nie da się odgadnąć. Jeśli ktoś podrobi formularz, to w akcji wykryjemy to ponieważ token nie będzie się zgadzał ze wzorcem przechowanym w sesji.

Ataki CSRF są tak pospolite, że framework ASP.NET MVC dostarcza gotowe rozwiązanie do użycia. W celu wygenerowania tokena w polu ukrytym wystarczy:

@using (Html.BeginForm("Manage", "Account")) {
     @Html.AntiForgeryToken()
    <fieldset>
        <legend>Change Email Form</legend>
        <ol>
            <li>
                @Html.LabelFor(m => m.Email)
                @Html.TextBoxFor(m => m.Email)
            </li> 
        </ol>
        <input type="submit" value="Change email" />
    </fieldset>
}

AntiForgeryToken wygeneruje wspomniane ukryte pole  oraz zapisze token w ciasteczku:

...

<input type="hidden"
    name="__RequestVerificationToken">
    value="WYGENEROWANY_TOKEN"/>
    
... 
    

Następnie w danej akcji musimy umieścić atrybut ValidateAntiForgeryToken:

[HttpPost]
[ValidateAntiForgeryToken]
[Authorize]
public ActionResult Manage(LocalPasswordModel model)
{
 //...
}

Atrybut sprawdzi czy token występuje zarówno w ciasteczku jak i polu ukrytym. Następnie porówna czy są one dokładnie takie same. Jeśli któryś z nich nie istnieje lub nie jest taki sam to oznacza, że ktoś podrobił formularz.

Ktoś może zadać pytanie, dlaczego przechowujemy wzorzec w ciasteczku a nie sesji? Zwykle implementując samemu proste rozwiązania, wykorzystuje się sesje ponieważ jest ona bezpieczniejsza. Ciasteczko przechowywane jest po stronie klienta i wysyła  je się z każdym zapytaniem. Na szczęście token w ASP.NET MVC to nie jest prosty, losowo wygenerowany string. Framework zadba o integralność danych (podpis cyfrowy). Przechowywanie informacji w sesji zmniejszyłoby skalowalność aplikacji, a wspomniany podpis gwarantuje integralność danych. Integralność jest bardzo ważnym elementem w implementacjach gdzie stary token (wcześniej wygenerowany) nie traci ważności i wciąż może zostać wykorzystany do wysłania formularza. W taki sposób, istnieje kilka tokenów, które mogą zostać wykorzystane. Powoduje to ryzyko, że atakujący podrobi samemu tak token, że zostanie on zaliczony przez serwer jako prawidłowy. Jeśli mamy podpis cyfrowy, to atakujący nie zna oczywiście klucza prywatnego serwera web.

Jeśli chcemy wygenerować kilka tokenów w sposób całkowicie niezależny od siebie wtedy możemy użyć parametru salt tzn.:

<%= Html.AntiForgeryToken("customSalt") %>

[ValidateAntiForgeryToken(Salt="customSalt")]

Salt, tak jak to w funkcjach haszujących, daje nam możliwość wpływania na ostateczną wartość, wygenerowaną przez ASP.NET MVC.

Atrybut ChildActionOnly

Czasami w kilku widokach chcemy wyświetlić taką samą lub podobną treść. Jednym z rozwiązań jest po prostu wyrenderowanie tej samej akcji, tzn.:

@{
    ViewBag.Title = "About";
}

<h2>About</h2>

Glowna tresc

@Html.Action("CommonPart")

Metoda Action wywoła daną akcje i następnie wstawi wyrenderowaną treść. Implementacja CommonPart wygląda następująco:

[ChildActionOnly]
public ActionResult CommonPart()
{
  return View((object)"Hello World");
}

Po co nam atrybut ChildActionOnly? Bez niego efekt końcowy byłby taki sam. Dobrą praktyką jednak jest zaznaczeniem nim akcji, która generuje wyłącznie część dokumentu (np. treść) a nie cały plik HTML. Zabezpiecza to również przed bezpośrednim wywołaniem CommonPart. Próba wywołania akcji przez przeglądarkę zakończy się następującym wyjątkiem:

The action 'CommonPart' is accessible only by a child request. 

Podsumowując, jeśli wykorzystujemy jakąś akcję do wygenerowania wyłącznie fragmentu dokumentu, warto rozważyć wspomniany atrybut ponieważ ułatwia to czytanie kodu oraz zapobiega wygenerowaniu częściowego dokumentu przez przeglądarkę, co często jest niepożądane.

Wielowątkowość a CultureInfo

CultureInfo zawiera informacje regionalne, przydatne, jeśli chcemy dostosować naszą aplikację do różnym krajów. Używamy tej klasy m.in. do określenia formatowania liczb, dat czy po prostu języka w jakim wyświetlamy tekst.

Jeśli korzystamy z domyślnych ustawień regionalnych, wtedy poniższy kod wyświetli prawidłową wartość CultureInfo:

static void Main(string[] args)
{
  DisplayCulture();
  Task.Factory.StartNew(DisplayCulture);

  Console.ReadLine();
}

private static void DisplayCulture()
{
  Console.WriteLine(CultureInfo.CurrentCulture.ToString());
}

Często jednak w aplikacji można zmienić język i tym samym CultureInfoUI. Nierzadko popełnianym błędem jest po prostu ustawienie CultureInfo dla aktualnego wątku:

class Program
{
   static void Main(string[] args)
   {
       Thread.CurrentThread.CurrentCulture=new CultureInfo("en-US");
       DisplayCulture();
       Task.Factory.StartNew(DisplayCulture);

       Console.ReadLine();
   }

   private static void DisplayCulture()
   {
       Console.WriteLine(Thread.CurrentThread.CurrentCulture);
   }
}

Kod zmienia kulturę wyłącznie aktualnego wątku – w tym przypadku głównego. Wszelkie inne wątki, będą używały kultury domyślnej z ustawień regionalnych. W celu zsynchronizowania wątków musimy sami przekazywać kulturę i ustawiać ją ręcznie w każdym z wątków.

Jest to dość czasochłonne i nudne zajęcie więc istnieją pewne usprawnienia od .NET 4.5:

class Program
{
   static void Main(string[] args)
   {
       CultureInfo.DefaultThreadCurrentCulture=new CultureInfo("en-US");
       DisplayCulture();
       Task.Factory.StartNew(DisplayCulture);

       Console.ReadLine();
   }

   private static void DisplayCulture()
   {
       Console.WriteLine(Thread.CurrentThread.CurrentCulture);
   }
}

W .NET 4.5 dodano DefaultThreadCurrentCulture. Ustawienie tej właściwości spowoduje, że wszelkie wątki będą korzystały ze wskazanej kultury. Przed .NET 4.5 trzeba było ręcznie to ustawiać albo zastosować hack, który znajdziecie tutaj:

http://blog.rastating.com/setting-default-currentculture-in-all-versions-of-net/

Kod:

public void SetDefaultCulture(CultureInfo culture)  
{
    Type type = typeof(CultureInfo);

    try
    {
        type.InvokeMember("s_userDefaultCulture",
                            BindingFlags.SetField | BindingFlags.NonPublic | BindingFlags.Static,
                            null,
                            culture,
                            new object[] { culture });

        type.InvokeMember("s_userDefaultUICulture",
                            BindingFlags.SetField | BindingFlags.NonPublic | BindingFlags.Static,
                            null,
                            culture,
                            new object[] { culture });
    }
    catch { }

    try
    {
        type.InvokeMember("m_userDefaultCulture",
                            BindingFlags.SetField | BindingFlags.NonPublic | BindingFlags.Static,
                            null,
                            culture,
                            new object[] { culture });

        type.InvokeMember("m_userDefaultUICulture",
                            BindingFlags.SetField | BindingFlags.NonPublic | BindingFlags.Static,
                            null,
                            culture,
                            new object[] { culture });
    }
    catch { }
}

Warto zwrócić na to uwagę, ponieważ wyświetlanie np. dat w wątku głównym w inny sposób niż w pozostałych wątkach, nie jest dobrym pomysłem.