ASP.NET MVC: Prawidłowa obsługa wyjątków AntiForgery

ASP.NET MVC dba o to, aby niemożliwe było dokonanie ataku CSRF. Nie musimy sami generować tokenów i wszystko zostanie obsłużone przez framework. Niestety czasami taka obsługa nie jest zbyt user-friendly. Ostatnio spotkałem następujący scenariusz:

  • Otworzyłem stronę do logowania w dwóch osobnych tabach. Sesja jest więc współdzielona.
  • W  pierwszej z nich, kliknąłem loguj. Token w tym momencie na serwerze jest unieważniany.
  • W drugim tabie token wygenerowany i przechowany w ukrytym polu, nie pokrywa się już z tym na serwerze. Z tego względu, klikając loguj otrzymamy wyjątek: “The provided anti-forgery token was meant for user "", but the current user is "test".
  • Po zalogowaniu się, serwer usuwa wszystkie tokeny, które przechowuje w sesji. Nic dziwnego, że drugie zapytanie jest traktowane jako potencjalny atak. Niektóre strony (gmail, hotmail) mają powiadomienia, które umożliwiają automatycznie przekierowanie strony z drugiej zakładki, tak więc nie będzie możliwe kliknięcie drugi raz loguj. Inne podejście to implementacja filtra, który nas przekieruje do konkretnej strony np.:

    public class AntiForgeryHandler : HandleErrorAttribute
    {        
       public override void OnException(ExceptionContext filterContext)
       {
           if (filterContext.Exception is HttpAntiForgeryException &&
               filterContext.HttpContext.User.Identity.IsAuthenticated)
           {
               filterContext.HttpContext.Response.StatusCode = 200;             
               filterContext.ExceptionHandled = true;
    
               var urlHelper = new UrlHelper(filterContext.RequestContext);
               filterContext.HttpContext.Response.Redirect(urlHelper.Action("InvalidToken","Account"), true);
           }
           else
           {
               base.OnException(filterContext);
           }
       }
    }

    Filtr wyłapie wyjątek HttpAntiForgeryException i przekieruje użytkownika do jakieś akcji, która może wyświetlić komunikat o błędzie. Filtr nakładamy na akcję LogIn:

    [HttpPost]
    [AllowAnonymous]
    [AntiForgeryHandler]
    [ValidateAntiForgeryToken]
    public ActionResult Login(LoginModel model, string returnUrl)
    {
        // reszta logiki tutaj
    }

    Rozwiązanie nieperfekcyjne ale lepsze to, niż wyświetlanie nieobsłużonego wyjątku.

    Artykuł: Wprowadzenie do CQRS

    Zachęcam do pierwszej części artykułu o CQRS:

    http://msdn.microsoft.com/pl-pl/library/wprowadzenie-do-cqrs-czesc-1

    Druga część będzie dotyczyć konkretnej implementacji na Azure. Mam nadzieję, że tekst rozjaśni trochę tematykę ponieważ moim zdaniem często temat jest niepotrzebnie komplikowany.

    Ponadto, pracuję nad serią artykułów poświęconą NoSQL i systemami rozproszonymi.

    ASP.NET MVC 5: Przeciążenie atrybutów

    W najnowszej wersji 5, mamy do dyspozycji nowy rodzaj filtrów, implementujący interfejs IOverrideFilter:

    public interface IOverrideFilter
    {
        /// <summary>
        /// Gets the type of filters to override.
        /// </summary>
        /// 
        /// <returns>
        /// The filter to override.
        /// </returns>
        Type FiltersToOverride { get; }
    }

    Atrybuty przydają się, gdy mamy jeden filtr nałożony globalnie i potem chcemy zmienić zachowanie wyłącznie dla specyficznej akcji. Na przykład, możemy nałożyć Authorize globalnie i potem dla konkretnej akcji zmienić reguły autoryzacji. Innymi słowy, IOverrideFilter umożliwia wyczyszczenie wszystkich filtrów dla konkretnej akcji. Domyślnie mamy następujące implementacje wspomnianego interfejsu:

    1. OverrideAuthenticationAttribute
    2. OverrideAuthorizationAttribute
    3. OverrideActionFiltersAttribute
    4. OverrideResultAttribute
    5. OverrideExceptionAttribute

    Jak widać, możemy przeciążyć dowolny typ filtrów. Warto zauważyć OverrideAuthethicationAttribute. Filtry uwierzytelniające zostały dodane również w ASP.NET MVC 5, ale o nich kiedyś indziej…

    Załóżmy, że mamy pewien filtr:

    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));
       }
    }

    Został on dodany globalnie:

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

    Następnie, chcemy go wyłączyć dla konkretnej akcji w HomeController:

    [OverrideActionFilters]
    public ActionResult Index()
    {
      
      return View();
    }

    Analogicznie możemy przeciążyć Authorize co jest bardziej praktycznym scenariuszem. Na przykład można wymagać pozwoleń do całego kontrolera, a potem przeładować jakąś akcję z innymi parametrami (np. inna grupa). Niestety w wersji 5.0 istnieje bug, który został oficjalnie zatwierdzony. Implementacja atrybutu wygląda następująco:

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
    public sealed class OverrideActionFiltersAttribute : Attribute, IOverrideFilter
    {
    /// <summary>
    /// Gets the filters to override for this instance.
    /// </summary>
    /// 
    /// <returns>
    /// The filters to override for this instance.
    /// </returns>
    public Type FiltersToOverride { get; }
    }

    Aby atrybut działał jak należy, powinno dziedziczyć się po FilterAttribute tzn.:

    class CustomOverrideActionFiltersAttribute : FilterAttribute, IOverrideFilter
    {
        public Type FiltersToOverride
        {
            get
            {
                return typeof(IActionFilter);
            }
        }
    }

    Analogicznie sytuacja wygląda z autoryzacją:

    public class OverrideAuthorizeAttribute : AuthorizeAttribute, IOverrideFilter
    {
        public Type FiltersToOverride
        {
            get { return typeof(IAuthorizationFilter); }
        }
    }
    

    W wersji 5.1 zostało to już naprawione i nie trzeba korzystać z własnych atrybutów – te dostarczone dziedziczą po odpowiednich już klasach. Dziwne, że zostało to wydane w takiej postaci, skoro jest to jedna z ważniejszych nowości…

    SideWaffle–standardowe szablony dla Visual Studio

    Zanim przejdziemy do tworzenia własnych szablonów dla ASP.NET MVC scaffolding, warto zainstalować SideWaffle. To nic innego jak zestaw szablonów i projektów. Link:
    http://sidewaffle.com/

    Pełną listę dostępnych szablonów można zobaczyć na stronie sidewaffle. Głównie są to szablony związane z web, szczególnie z AngularJS. Po zainstalowaniu zobaczymy nowe typy projektów jak i szablony dla nowych plików. Na przykład, w aplikacji Web mamy:

    image

    Wybierając szablon requirejs, zostanie wygenerowany szkielet modułu:

    require(["helper/util"], function (util) {
        //This function is called when scripts/helper/util.js is loaded.
        //If util.js calls define(), then this function is not fired until
        //util's dependencies have loaded, and the util argument will hold
        //the module value for "helper/util".
    });
    

    Naprawdę przydatne, bo w końcu pisanie tego ręcznie dla każdego modułu jest monotonne. Ostatnio na blogu pisałem o knockout.js. Również i dla tej biblioteki istnieje szablon:

    //Template taken from http://knockoutjs.com/documentation/custom-bindings.html
    ko.bindingHandlers['binding1'] = {
        init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
            // This will be called when the binding is first applied to an element
            // Set up any initial state, event handlers, etc. here
        },
        update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
            // This will be called once when the binding is first applied to an element,
            // and again whenever the associated observable changes value.
            // Update the DOM element based on the supplied values here.
        }
    };
    

    Najwięcej chyba szablonów jest dostępnych dla AngularJS. Przykład (AngularJS Module):

    (function () {
        'use strict';
    
        var id = 'app1';
    
        // TODO: Inject modules as needed.
        var app1 = angular.module('app1', [
            // Angular modules 
            'ngAnimate',        // animations
            'ngRoute'           // routing
    
            // Custom modules 
    
            // 3rd Party Modules
            
        ]);
    
        // Execute bootstrapping code and any dependencies.
        // TODO: inject services as needed.
        app1.run(['$q', '$rootScope',
            function ($q, $rootScope) {
    
            }]);
    })();

    Oczywiście, mamy również szkielety dla kodu c#, a nie tylko JavaScript. Zobaczmy co wygeneruje nUnit Fixture:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using NUnit.Framework;
    
    namespace MvcApplication1
    {
        [TestFixture]
        public class NUnitFixture1
        {
            [SetUp]
            public void Setup()
            {
    
            }
    
            [Test]
            public void Test()
            {
                throw new NotImplementedException();
            }
        }
    }
    

    Nawet dla SVG mamy małą pomoc w formie:

    <?xml version="1.0" encoding="UTF-8"?>
    <svg xmlns="http://www.w3.org/2000/svg" width="300" height="200">
      <rect width="300" height="120" y="20" fill="green"/>
      <rect width="80" height="150" x="20" y="30" fill="red"/>
      <rect width="140" height="80" x="50" y="50" fill="blue"/>
    </svg>

    Dla programistów Azure, mamy 3 szkielety klas m.in. ułatwiające prace z Azure Table oraz Blob:

    image

    Przykład (Azure Blob):

    using Microsoft.WindowsAzure.Storage;
    using Microsoft.WindowsAzure.Storage.Blob;
    using System.Collections.Generic;
    using System.Configuration;
    using System.IO;
    using System.Linq;
    using System.Web;
    
    namespace WebApplication4.Controllers
    {
        public class BlobUploadHelper1
        {
            private string _connectionString;
            private CloudStorageAccount StorageAccount { get; set; }
    
            public BlobUploadHelper1()
                : this(ConfigurationManager
                    .ConnectionStrings["BlobUploadHelper1.ConnectionString"]
                        .ConnectionString)
            {
            }
    
            public BlobUploadHelper1(string connectionString)
            {
                _connectionString = connectionString;
            }
    
            public CloudBlobClient GetBlobClient()
            {
                StorageAccount = CloudStorageAccount.Parse(_connectionString);
                return StorageAccount.CreateCloudBlobClient();
            }
    
            public CloudBlobContainer GetBlobContainer(string containerName,
                BlobContainerPublicAccessType accessType = BlobContainerPublicAccessType.Blob)
            {
                var container = GetBlobClient().GetContainerReference(containerName);
                container.CreateIfNotExists();
                container.SetPermissions(new BlobContainerPermissions
                {
                    PublicAccess = accessType
                });
                return container;
            }
    
            public string UploadFileToBlob(HttpPostedFileBase postedFile, string containerName = "uploads")
            {
                var container = GetBlobContainer(containerName);
                var blob = container.GetBlockBlobReference(Path.GetFileName(postedFile.FileName));
                using (postedFile.InputStream)
                {
                    blob.UploadFromStream(postedFile.InputStream);
                }
                return blob.Uri.AbsoluteUri;
            }
    
            public List<string> GetBlobList(string containerName)
            {
                var container = GetBlobContainer(containerName);
                var ret = container.ListBlobs().Select(x => x.Uri.AbsoluteUri).ToList();
                return ret;
            }
        }
    }

    Warto zaznaczyć, że SideWaffle to nie tylko nowe typy projektów oraz szablonów, ale również snippet’y. Do dyspozycji mamy m.in. Nancy, WCF Client, Dispose czy Angular. Dispose wygląda następująco:

    #region IDisposable Members
    
    /// <summary>
    /// Internal variable which checks if Dispose has already been called
    /// </summary>
    private Boolean disposed;
    
    /// <summary>
    /// Releases unmanaged and - optionally - managed resources
    /// </summary>
    /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
    protected virtual void Dispose(Boolean disposing)
    {
        if (disposed)
        {
            return;
        }
    
        if (disposing)
        {
            //TODO: Managed cleanup code here, while managed refs still valid
        }
        //TODO: Unmanaged cleanup code here
    
        disposed = true;
    }
    
    /// <summary>
    /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
    /// </summary>
    public void Dispose()
    {
        // Call the private Dispose(bool) helper and indicate 
        // that we are explicitly disposing
        this.Dispose(true);
    
        // Tell the garbage collector that the object doesn't require any
        // cleanup when collected since Dispose was called explicitly.
        GC.SuppressFinalize(this);
    }
    
    /// <summary>
    /// The destructor for the class.
    /// </summary>
    ~ClassNamePlaceholder() 
    { 
        this.Dispose(false); 
    }
    
    
    #endregion

    Z kolei WCF Client:

    try
    {
      //todo: make calls to the client - should be as small a set of work here as possible
      client.Close();
    }
    catch (System.ServiceModel.CommunicationException e)
    {
      //todo: handle communication exceptions, for example cannot connect to service.
      client.Abort();
    }
    catch (TimeoutException e)
    {
      //todo: handle time outs when connecting, calling and closing
      client.Abort();
    }
    catch (Exception e)
    {
      //todo: handle any other client issue
      client.Abort();
      throw;
    }

    Co do snippetów… Istnieje jeszcze nawet szablon na własne snippety, bo w końcu każdy z nich zawiera pewne elementy wspólne:

    <?xml version="1.0" encoding="utf-8" ?>
    <CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
      <CodeSnippet Format="1.0.0">
        <Header>
          <Title>snippet1</Title>
          <Shortcut>snippet1</Shortcut>
          <Description>A description for snippet1</Description>
          <Author>Microsoft Corporation</Author>
          <SnippetTypes>
            <SnippetType>Expansion</SnippetType>
          </SnippetTypes>
        </Header>
        <Snippet>
          <Declarations>
            <Literal Editable="true">
              <ID>message</ID>
              <ToolTip>The message</ToolTip>
              <Default>Hi there!</Default>
            </Literal>
          </Declarations>
          <Code Language="CSharp"><![CDATA[
                Console.WriteLine("$message$");
            ]]>
          </Code>
        </Snippet>
      </CodeSnippet>
    </CodeSnippets>
    

    Przedstawione w tym poście szkielety to bardzo mała część z tych, które są dostępne w pakiecie. Oczywiście z większości prawdopodobnie nie będziemy korzystać w tym samym projekcie, ale warto je przejrzeć, aby następnym razem już nie marnować czasu na pisanie powtarzalnych części kodu.

    ASP.NET MVC 5 Scaffolding

    W Visual Studio 2013 oraz ASP.NET MVC 5 dodano nową funkcję, ułatwiającą wykonywanie powtarzalnych czynności. Od razu dodam, że istnieje możliwość pisania własnych szablonów. Najpierw (dzisiaj) pokażę jak skorzystać z domyślnego szablonu, który korzysta z Entity Framework. Jeśli często musimy tworzyć encje, kontroler, widoki i zapisywać dane w bazie to domyślny szablon zautomatyzuje  to. W praktyce jednak częściej korzysta się z jakiś usług niż bezpośrednio z Entity Framework, stąd opcja tworzenia własnych szablonów jest dosyć ważna, ale o tym w następnym w wpisie.

    Załóżmy, że mamy następujący model:

    public class Person
    {
       public int Id { get; set; }
       public string FirstName { get; set; }        
       public string LastName { get; set; }
    }

    Chcemy wygenerować kontroler, widoki oraz kontekst, który umożliwi nam zapis danych do bazy. Innymi słowy, klasyczny CRUD. Najpierw klikamy w Solution Explorer i wybieramy Add->New Scafffolded Item…:

    image

    Następnie wybieramy szablon, który chcemy użyć. Dla tego przykładu wybieramy “MVC 5 controller with views, using Entity Framework”:

    image

    Kolejne okno dialogowe umożliwia konfiguracje kontrolera, widoków oraz kontekstu danych:

    image

    Po wygenerowaniu zobaczymy nowe pliki w Solution Explorer:

    image

     

    Kontroler zawiera operacje CRUD oraz również zostały wygenerowane stosowne widoki:

    public class PersonController : Controller
    {
       private WebApplication3Context db = new WebApplication3Context();
    
       // GET: /Person/
       public ActionResult Index()
       {
           return View(db.People.ToList());
       }
    
       // GET: /Person/Details/5
       public ActionResult Details(int? id)
       {
           if (id == null)
           {
               return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
           }
           Person person = db.People.Find(id);
           if (person == null)
           {
               return HttpNotFound();
           }
           return View(person);
       }
    
       // GET: /Person/Create
       public ActionResult Create()
       {
           return View();
       }
    
       // POST: /Person/Create
       // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
       // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
       [HttpPost]
       [ValidateAntiForgeryToken]
       public ActionResult Create([Bind(Include="Id,FirstName,LastName")] Person person)
       {
           if (ModelState.IsValid)
           {
               db.People.Add(person);
               db.SaveChanges();
               return RedirectToAction("Index");
           }
    
           return View(person);
       }
    
       // GET: /Person/Edit/5
       public ActionResult Edit(int? id)
       {
           if (id == null)
           {
               return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
           }
           Person person = db.People.Find(id);
           if (person == null)
           {
               return HttpNotFound();
           }
           return View(person);
       }
    
       // POST: /Person/Edit/5
       // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
       // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
       [HttpPost]
       [ValidateAntiForgeryToken]
       public ActionResult Edit([Bind(Include="Id,FirstName,LastName")] Person person)
       {
           if (ModelState.IsValid)
           {
               db.Entry(person).State = EntityState.Modified;
               db.SaveChanges();
               return RedirectToAction("Index");
           }
           return View(person);
       }
    
       // GET: /Person/Delete/5
       public ActionResult Delete(int? id)
       {
           if (id == null)
           {
               return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
           }
           Person person = db.People.Find(id);
           if (person == null)
           {
               return HttpNotFound();
           }
           return View(person);
       }
    
       // POST: /Person/Delete/5
       [HttpPost, ActionName("Delete")]
       [ValidateAntiForgeryToken]
       public ActionResult DeleteConfirmed(int id)
       {
           Person person = db.People.Find(id);
           db.People.Remove(person);
           db.SaveChanges();
           return RedirectToAction("Index");
       }
    
       protected override void Dispose(bool disposing)
       {
           if (disposing)
           {
               db.Dispose();
           }
           base.Dispose(disposing);
       }
    }

    Widoki zawierają podstawowe formularze itp.  Mamy plik Index, który wyświetla listę encji wraz z linkami do edycji (Edit), dodawania (Add), usuwania oraz wyświetlenia szczegółów (Details).

    W praktyce niestety wiele rzeczy trzeba samemu modyfikować. Na przykład, kontekst jest inicjalizowany w samej klasie, co prawie zawsze jest nieakceptowalne – w realnych projektach korzysta się z IoC. Dlatego w następnym poście, pokażę jak pisać własne szablony, które możemy dostosować do konkretnego projektu i infrastruktury. Rozwiązanie generyczne zwykle nadają się wyłącznie do prototypów albo wymaga po prostu wielu modyfikacji.

    MVVM w JavaScript, knockout.js, część II

    W poprzedniej części przedstawiłem bibliotekę knockout.js, ułatwiającą implementację wzorca MVVM w JavaScript. Jest to ciekawa biblioteka, szczególnie dla programistów WPF. W tamtym wpisie, pokazaliśmy jak wiązać dane w dwóch kierunkach: od VM do widoku i odwrotnie. Skończyliśmy na następującym przykładzie:

    var personViewModel = {
    firstName: ko.observable('poczatkowa wartosc'),
    lastName: ko.observable('poczatkowa wartosc') };
    
    ko.applyBindings(personViewModel);

    Gdzie widok to:

    <p data-bind="text: firstName"></p>
    <p data-bind="text: lastName"></p>
    
    <input type="text" data-bind='value: firstName' />
    <input type="text" data-bind='value: lastName' />

    W WPF bardzo często tworzymy właściwości tylko do odczytu, które zwracają jakieś dane na podstawie innych właściwości. W C# wystarczy stworzyć właściwość wyłącznie z getterem, który używa z kolei jakiś innych właściwości. Oczywiście analogiczny scenariusz możemy zaimplementować w JS. Zaktualizujmy najpierw widok:

    <p data-bind="text: firstName"></p>
    <p data-bind="text: lastName"></p>
    <p data-bind="text: fullName"></p>
    
    <input type="text" data-bind='value: firstName' />
    <input type="text" data-bind='value: lastName' />

    FullName zatem jest właściwością, która zwraca połączone string’i firstName i LastName:

    function PersonViewModel() {
       this.firstName = ko.observable('poczatkowa wartosc');
       this.lastName = ko.observable('poczatkowa wartosc');
       this.fullName = ko.computed(function() { return this.firstName() + ' ' + this.lastName(); }, this);
    };
    

    Tak jak to z knockout.js, korzystamy z obiektu ko. W C# wystarczy stworzyć właściwość, niestety w JS nie ma takiej możliwości i należy wywoływać specjalne metody na ko. Proszę zwrócić uwagę, że observable zwraca funkcję, zatem nie jest to zwykłe pole.

    Kolejną przydatną rzeczą są komendy (analogiczne do ICommand w WPF). Zacznijmy od widoku:

    <button data-bind="click: save">Zapisz</button>

    Widzimy tutaj nowy typ wiązania, click. Wcześniej korzystaliśmy z text oraz value.

    Następnie musimy zdefiniować handler:

    function PersonViewModel() {
       this.firstName = ko.observable('poczatkowa wartosc');
       this.lastName = ko.observable('poczatkowa wartosc');
       this.fullName = ko.computed(function () { return this.firstName() + ' ' + this.lastName(); }, this);
       this.save = function() {
           alert('Save: ' + this.fullName());
       };
    };

    Możliwe jest również stworzenie czegoś analogicznego do ObservableCollection:

    function PersonViewModel() {
       this.firstName = ko.observable('poczatkowa wartosc');
       this.lastName = ko.observable('poczatkowa wartosc');
    };
    
    function MainViewModel() {
       this.persons = ko.observableArray();
    }
    
    vm = new MainViewModel();
    
    person1 = new PersonViewModel();
    person1.firstName('imie1');
    person1.lastName('nazwisko');
    
    person2 = new PersonViewModel();
    person2.firstName('imie2');
    person2.lastName('nazwisko2');
    
    vm.persons.push(person1);
    vm.persons.push(person2);

    Widok:

    <ul data-bind="foreach: persons">
        <li>
            <p data-bind="text: firstName"></p>
            <p data-bind="text: lastName"></p>
        </li>
    </ul>
    
    

    Kolejny typ wiązania to foreach. Służy on oczywiście do powtarzania pewnej części kodu HTML. W WPF jest to nic innego jak ItemsSource oraz ObservableCollection.

    Do dyspozycji mamy również zmienną $root, dzięki której możemy wywołać np. komendę z głównego VM:

    <ul data-bind="foreach: persons">
        <li>
            <p data-bind="text: firstName"></p>
            <p data-bind="text: lastName"></p>
            <button data-bind="click: $root.save">Zapisz</button>
        </li>
    </ul>

    Gdzie VM, to:

    function MainViewModel() {
       this.persons = ko.observableArray();
       this.save = function() {
           alert('Zapisz');
       };
    }

    Oczywiście to dopiero początek możliwości knockout. Jeśli kogoś temat zaciekawił to odsyłam do dokumentacji. Na blogu, będę zamieszczał tylko takie krótkie wprowadzania do różnych bibliotek, po to aby zachęcić czytelników, albo po prostu pokazać, że taka możliwość istnieje i została już zaimplementowana. Nie ma sensu bowiem duplikować tego, co jest już w dokumentacji.

    ASP.NET MVC: kompilacja widoków

    Zwykle widoki nie są kompilowane aż do momentu publikacji i pierwszego zapytania. Czasami jednak warto, aby były one kompilowane w czasie pracy z kodem. Bardzo łatwo popełnić literówkę, która nie będzie wykryta aż do momentu, gdy użytkownik będzie chciał wejść na tą stronę. Przy wielu widokach jest to dość niewygodne i może okazać się bardzo uciążliwe. Załóżmy, że mamy następujący widok z błędem:

    @model dynamic
    
    @{
        ViewBag.Title = "title";
        Layout = null;
    }
    
    
    @NieistniejacaZmienna
    

    Strona skompiluje się bez błędu, co jest problemem. Dopiero, gdy użytkownik spróbuje otworzyć stronę dostanie następujący wyjątek:

    Compilation Error
    
    Description: An error occurred during the compilation of a resource required to service this request. Please review the following specific error details and modify your source code appropriately. 
    
    Compiler Error Message: CS0103: The name 'NieistniejacaZmienna' does not exist in the current context
    
    Source Error:
    
    
    Line 7:  
    Line 8:  
    Line 9:  @NieistniejacaZmienna

    Na szczęście istnieje dość łatwy sposób na zmuszenie VS do skompilowania widoków. Wystarczy otworzyć projekt w notatniku i ustawić następującą właściwość na true:

    <PropertyGroup>
      <MvcBuildViews>true</MvcBuildViews>
    </PropertyGroup>

    Poskutkuje to następującym błędem już na etapie kompilacji:

    Error    1    The name 'NieistniejacaZmienna' does not exist in the current context

    Trzeba się oczywiście liczyć, że jest to dodatkowe obciążenie i wydłuży trochę czas publikacji.

    MVVM w JavaScript, knockout.js

    Od jakiegoś czasu, coraz więcej czasu poświęcam nad aplikacjami webowymi stąd na blogu tematyka trochę zmieniła się z kodu czysto server-side na web. Większość programistów C# nie przepada za JavaScript, ale trzeba robić wszystko, aby ułatwić sobie codzienną pracę. Istnieje wiele dodatkowych bibliotek do JS i to dzięki nim środowisko JS jest tak potężne. Na blogu raczej nie będę zajmował się zbytnio tą tematyką ponieważ nie jest to do końca moja specjalizacja. Czasami jednak, tak jak dzisiaj, wrzucę kilka słów o ciekawych według mnie bibliotekach, które powinny znaleźć się w każdym webowym projekcie. Jako, że sporo korzystałem z WPF, to wzorzec MVVM był dla mnie codziennością. Z tego względu, biblioteka knockout od razu zaciekawiła mnie.

    O czystym MVVM pisałem już np. tutaj:

    http://www.pzielinski.com/?p=1055

    Zdefiniujemy najpierw nasz widok. W przypadku WPF, robiło to się za pomocą XAML oraz dyrektyw {Binding}. Zobaczmy jak to wygląda w HTML i knockout.js:

    <p data-bind="text: firstName"></p>
    <p data-bind="text: lastName"></p>

    Nie wygląda to aż tak brzydko… Korzystamy z atrybutu data-bind a następnie określamy za pomocą text, że chcemy powiązać tekst, który znajduje się w danej właściwości. Oczywiście musimy stworzyć również ViewModel:

    var personViewModel =  { firstName: 'Piotr', lastName:'Zielinski' };
    ko.applyBindings(personViewModel);

    Aby wykonać wiązanie należy wywołać metodę applyBindings na obiekcie ko (knockout.js). Jak widać, nie jest to trudne i dzięki temu oddzielimy logikę od warstwy prezentacji. Korzystając z czystego JS, można napisać naprawdę brzydki kod i projekt. Na szczęście mnogość bibliotek (np. RequireJS) pozwalają na zastosowanie tego języka w bardziej skomplikowanych projektach.

    Spróbujmy napisać trochę inny przykład, pokazujący wiązanie w drugą stronę. Innymi słowy, chcemy aby użytkownik mógł modyfikować dane. Typowy scenariusz to oczywiście zwykły formularz. Musimy zmodyfikować nieco nasz ViewModel. W C# zwykle wystarczy dodać SETTER do danej właściwości.

    var personViewModel = { 
    firstName: ko.observable('poczatkowa wartosc'), 
    lastName: ko.observable('poczatkowa wartosc') };
    
    ko.applyBindings(personViewModel);

    Musimy skorzystać z metody observable. W WPF zwykle korzysta się z INotifyPropertyChanged oraz OnPropertyChanged. Pełni to analogiczną funkcję – pozwala wykryć, kiedy właściwość została zmieniona. Następnie musimy dodać dwa nowe pola edycyjne:

    <p data-bind="text: firstName"></p>
    <p data-bind="text: lastName"></p>
    
    <input type="text" data-bind='value: firstName'/>
    <input type="text" data-bind='value: lastName'/>

    To co rzuca tutaj się w oczy, to nowy typ wiązania, a mianowicie “value”. Korzystamy z niego, gdy daną wartością chcemy zaktualizować właściwość. W WPF, korzysta się z typu wiązana (OneWay, TwoWay itp.), aby osiągnąć analogiczny efekt.

    Uruchamiając przykład, zobaczymy, że zmieniając dane w polu edycyjnym, zostaną one również odzwierciedlone w tag’u <p>.

    Na dzisiaj wystarczy. W przyszłych wpisach pokażę trochę więcej przykładów. Osobiście uważam, że biblioteka jest bardzo przydatna i naturalna dla programistów, którzy pracują również sporo z WPF.

    ASP.NET MVC 5 \ Web API 2 – routing za pomocą atrybutów

    O routingu w ASP.NET MVC pisałem już  wielokrotnie. ASP.NET MVC 5 oraz Web API 2 wprowadzają jednak dodatkowy sposób definiowania URL – za pomocą atrybutu. Myślę, że szczególnie przydatne jest to w Web API, gdzie musimy bardzo często wspierać kilka wersji API jednocześnie. Zacznijmy od prostego przykładu:

    public class PersonsController : ApiController
    {
       [Route("api/persons/v1/{id}")]
       public string GetPerson(int id)
       {
           return "test" + id.ToString(CultureInfo.InvariantCulture);
       }
    }

    Atrybut Route definiuje po prostu routing. Bez niego, domyślny URL musiałby być w postaci np. /api/persons/1. Powyższy wzorzec jednak dopasuje linki takie jak /api/v1/persons/5.

    Jak widać sposób użycia jest bardzo prosty. Zamiast routing definiować globalnie, definiuje się go dla konkretnej metody (akcji). Ma to wiele zastosowań, np. wspieranie kilku wersji API (o tym w szczegółach kiedyś indziej). W skrócie, dzięki Route można określić, która metoda ma zostać wykonana dla konkretnych wersji API.

    Inny przykład to filtrowanie za pomocą różnych kryteriów. Załóżmy, że mamy trzy metody, które filtrują informacje na podstawie daty, id i napisu. Za pomocą Web Api można stworzyć następujące  akcje:

    public class PersonsController : ApiController
      
       public string GetPersonById(int id)
       {
           return "GetPersonById=" + id.ToString(CultureInfo.InvariantCulture);
       }
       public string GetPersonByFirstName(string firstName)
       {
           return "GetPersonByFirstName=" + firstName;
       }
       public string GetPersonByDateTime(DateTime dateTime)
       {
           return "GetPersonByDatetime=" + dateTime.ToString(CultureInfo.InvariantCulture);
       }
    }

    Oczywiście w takiej postaci nie zadziała to ponieważ wysłanie zapytania HTTP GET do /api/persons/ spowoduje dopasowanie trzech metod. Za pomocą routingu łatwo jednak zmienić to do:

    public class PersonsController : ApiController
    {
       [Route("api/persons/{id:int}")]
       public string GetPersonById(int id)
       {
           return "GetPersonById=" + id.ToString(CultureInfo.InvariantCulture);
       }
       [Route("api/persons/{firstName:alpha}")]
       public string GetPersonByFirstName(string firstName)
       {
           return "GetPersonByFirstName=" + firstName;
       }
       [Route("api/persons/{dateTime:datetime}")]
       public string GetPersonByDateTime(DateTime dateTime)
       {
           return "GetPersonByDatetime=" + dateTime.ToString(CultureInfo.InvariantCulture);
       }
    }

    Proszę zwrócić uwagę na typ danych, który definiujemy wraz z argumentem. Określamy, że ID musi być typu INT, firstName jest tekstem, a dateTime datą. Istnieje wiele innych ograniczeń takich jak np. bool, decimal, double. Możliwe jest użycie nawet wyrażeń regularnych. Za pomocą ich, możemy określić jakie warunki musi argument spełniać, aby akcja została dopasowana.

    Wracając do przykładu. Jeśli użytkownik wywoła /api/persons/5 to oczywiście akcja GetPersonById zostanie dopasowana i wykonana. GetPersonByFirstName zostanie dopasowana do np. /api/persons/piotr. Z kolei “api/persons/11-11-2012” jest przykładem adresu dopasowanego do akcji GetPersonByDateTime.

    Oprócz wielkiego zestawu typów argumentów zdefiniowanych w MVC, można samodzielnie napisać własne. Wystarczy zaimplementować interfejs IHttpRouteConstraint:

    public interface IHttpRouteConstraint
    {
        /// <summary>
        /// Determines whether this instance equals a specified route.
        /// </summary>
        /// 
        /// <returns>
        /// True if this instance equals a specified route; otherwise, false.
        /// </returns>
        /// <param name="request">The request.</param><param name="route">The route to compare.</param><param name="parameterName">The name of the parameter.</param><param name="values">A list of parameter values.</param><param name="routeDirection">The route direction.</param>
        bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection);
    }

    Innym ciekawym zastosowaniem routingu jest zwrócenie np. kolekcji dla danej encji. Rozważmy następujący kontroler:

    public class PersonsController : ApiController
    {
       public string GetPersonById(int id)
       {
           return "GetPersonById=" + id.ToString(CultureInfo.InvariantCulture);
       }
    
       [Route("api/persons/{id}/contacts")]
       public string[] GetContacts(int id)
       {
           return new[] {"1", "2", "3"};
       }
    }

    Standardowy URL /api/persons/1 zwróci pojedynczą osobę (GetPersonById). Z kolei /api/persons/5/contacts całą kolekcję.

    Bardzo ułatwiającym pracę atrybutem jest RoutePrefix. Zwykle, początek URL w każdym kontrolerze jest taki sam. Zamiast duplikować go dla każdej akcji, możemy określić go raz za pomocą RoutePrefix:

    [RoutePrefix("api/persons")]
    public class PersonsController : ApiController
    {
       [Route("{id:int}")]
       public string GetPersonById(int id)
       {
           return "GetPersonById=" + id.ToString(CultureInfo.InvariantCulture);
       }
       [Route("{firstName:alpha}")]
       public string GetPersonByFirstName(string firstName)
       {
           return "GetPersonByFirstName=" + firstName;
       }
       [Route("{dateTime:datetime}")]
       public string GetPersonByDateTime(DateTime dateTime)
       {
           return "GetPersonByDatetime=" + dateTime.ToString(CultureInfo.InvariantCulture);
       }
    }

    Z kolei, jeśli jakaś akcja w kontrolerze, który ma RoutePrefix posiada inny prefiks, wtedy można skorzystać z pełnej ścieżki, którą należy zacząć znakiem ~:

    [Route("~/api/fullpath/{id:int}")]
    public string GetFullPath(int id)
    {
      return "GetFullPath=" + id.ToString(CultureInfo.InvariantCulture);
    }
    

    Atrybut Route pozwala również zdefiniować nazwę routingu oraz porządek, w którym będzie on dopasowywany (przydatne gdy jest kilka routingów z różnym priorytetem):

    [Route("~/api/fullpath/{id:int}",Name="nazwa_drogi",Order=3)]

    Nazwa z kolei przydaje się, gdy chcemy skorzystać gdzieś w kodzie z metod takich jak Url.Link itp.

    Na zakończenie, warto zaznaczyć, żeby routing za pomocą atrybutów działał poprawnie, należy upewnić się, że jest on aktywowany za pomocą:

    public static void Register(HttpConfiguration config)
    {
      // Web API configuration and services
    
      // Web API routes
      config.MapHttpAttributeRoutes();
    
      config.Routes.MapHttpRoute(
          name: "DefaultApi",
          routeTemplate: "api/{controller}/{id}",
          defaults: new { id = RouteParameter.Optional }
      );
    }
    }