niedziela, 31 marca 2013

ASP.NET MVC Mass Assignment

Mass Assignment to sposób na wysłanie parametrów które normalnie nie powinny się znaleźć w żądaniu POST.

Zobaczmy na prosty przykład obrazujący opisany powyżej problem:

Model:

Code:
    public class ApplicationUser
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public bool SecretField { get; set; }
    }

Na podstawie modelu tworzymy widok edycji danych użytkownika:

Code:
@model MassAssignment.Models.ApplicationUser

@{
    ViewBag.Title = "Edit";
}

<h2>Edit</h2>

@using (Html.BeginForm()) {
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>ApplicationUser</legend>

        @Html.HiddenFor(model => model.Id)

        <div class="editor-label">
            @Html.LabelFor(model => model.FirstName)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.FirstName)
            @Html.ValidationMessageFor(model => model.FirstName)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.LastName)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.LastName)
            @Html.ValidationMessageFor(model => model.LastName)
        </div>
        @*
        <div class="editor-label">
            @Html.LabelFor(model => model.SecretField)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.SecretField)
            @Html.ValidationMessageFor(model => model.SecretField)
        </div>
        *@
        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}


Pole SecreetField zostało za komentowane (nie pojawi się w źródłach wygenerowanego dokumentu).

W kontrolerze wprowadzamy prosty kod który uaktualni obiekt użytkownika na podstawie przekazanych przez Binding wartości:

Code:
        [HttpPost]
        public ActionResult Edit(ApplicationUser user)
        {
            if (ModelState.IsValid)
            {
                var u = userService.GetUserById(user.Id);
                UpdateModel(u);
            }

            return RedirectToAction("GetUsers");
        }

Rozwiązanie można powiedzieć książkowe - poza jednym szczegółem - brakuje SaveChanges na obiekcie Context, którego akurat w tym przypadku nie mam (nie korzystam z bazy danych a ze statycznej kolekcji).

Wydawałoby się że powyższy przykład jest poprawny. Jednak jest jedna rzecz którą przeoczyliśmy podczas tworzenia tego rozwiązania - nasi użytkownicy. Zobaczmy co mogłoby się stać, gdyby ktoś chciał wykorzystać aplikację w sposób który nie chcemy:


Przepraszam za słabą jakość, ale chodzi tutaj o samo sedno problemu. Mianowicie jeżeli użytkownik będzie chciał zaatakować naszą stroną i w naszym modelu znajdzie się pole typu IsAdmin, użytkownik może przesłać ten parametr bezpośrednio do mechanizmu bindowania (np. tak jak ja za pomocą QueryString).

W jaki sposób możemy zapobiec takiemu zachowaniu? Otóż istnieje kilka rozwiązań:

1. Stworzenie osobnych modeli do edycji, wyświetlania
2. Skorzystanie z atrybutu Bind:

Code:
public ActionResult Edit([Bind(Exclude = "SecretField")]ApplicationUser user)

Kolejne pola, które nie mają brać udziału w bindowaniu wymieniamy po przecinku. Możemy oczywiście także wskazać, które pola mają brać udział w bindowaniu.

3. Metoda UpdateModel jako jeden z argumentów przyjmuje tablicę pól, które mają zostać uaktualnione:

Code:
UpdateModel(u, includeProperties: new string[] {"FirstName", "LastName"});

4. Kolejną możliwością jest stworzenie np. interfejsu:

Code:
    public interface IEditModel
    {
        string FirstName { get; set; }
        string LastName { get; set; }
    }

następnie w metodzie UpdateModel podajemy ten model:

Code:
UpdateModel<IEditModel>(u);


5. Jeszcze jedną możliwością, jest nałożenie atrybuty ReadOnly na dane pole w modelu:

Code:
[ReadOnly(true)]

Mechanizm bindowania respektuje tak nałoży atrybut na dane pole.

Zakończenie certyfikacji

29 marca oficjalnie zakończyłem ścieżkę certyfikacyjną - przynajmniej na ten moment :)

Efektem nauki jest zdanie 4 egzaminów MCTS, oraz dwóch MCPD.

70-513
Windows Communication Foundation - egzamin prosty, chociaż w niektórych przypadkach czytanie XMLów może zająć trochę więcej czasu. Materiały z których się uczyłem to Training Kit dla .NET 3.5, tematy które nie są zawarte w podręczniku można doczytać w internecie. Warto też przejrzeć materiały zebrane w formie linków na stronie http://www.jamesjfoster.com/blog/2010/resource-links-for-70-513-wcf-certification-exam/. Wiedza ta wystarcza w 100% do zdania egzaminu.

70-518
Egzamin różni się od pozostałych tym, że kładzie nacisk na znajomość ograniczeń i możliwości poszczególnych rozwiązań Microsoft podczas tworzenia aplikacji desktopowych. Większość pytań dotyczy tworzenia systemów i pytań mających na celu określenie wyboru prawidłowego frameworka / rozwiązania danego zagadnienia.

70-515
Tworzenie aplikacji webowych w ASP.NET WebForms oraz MVC. Pierwszym zastrzeżeniem jakie mam co do egzaminu to użycie MVC w wersji 2.0. Mamy już 4 wersję tego frameworka i raczej żadnego nowego projektu nie rozpoczniemy pisać w czymś co powstało kilka lat wstecz. Jeżeli chodzi o poziom pytań - zróżnicowany od bardzo prostych i wręcz oczywistych aż do zagadnień, których często nie wykorzystuję.

70-519
Egzamin który ma tę samą formę co 70-518. Najdłuższy ze wszystkich, największa ilość pytań którą do tej porty otrzymałem - 78. Podczas egzaminu dwa przypadki systemów do których zostało przygotowanych 14 pytań. Ogólnie egzamin podobał mi się, ciekawe pytania i zagadnienia. Jeżeli ktoś pracuje przy tworzeniu aplikacji webowych po przeczytaniu materiałów zawartych w Training Kit i skorzystaniu z linków do materiałów zawartych w nim, nie będzie mieć problemów ze zdaniem ich.



Obecnie nie zamierzam przystępować do kolejnych egzaminów. Sama certyfikacja posłużyła mi głównie w celu segregacji posiadanej wiedzy, nauczeniu się nowych metod rozwiązywania często spotykanych problemów.
W sieci można spotkać się z opiniami, że certyfikacja nie ma sensu. Nie zgodzę się z tymi stwierdzeniami. W wielu przypadkach samo podejście do egzaminu zmusza nas do przeczytania odpowiednich materiałów, dzięki którym być może nauczymy się rozwiązywać dane problemy w lepszy, szybszy i bardziej przenośny sposób.
Wszystkim, którzy są w trakcie bądź też przymierzają się do certyfikacji życzę wytrwałości i sukcesów.

sobota, 30 marca 2013

Parsowanie HTML w .NET

Parsowanie HTML chociaż wydaje się rzeczą trywialną potrafi przysporzyć wielu problemów. Związanie jest to nie tyle ze skomplikowaniem zagadnienia co z formatem w jakim otrzymujemy dane.
Szukając tagu w wielu przykładach możemy natrafić na nie zamknięte tagi, niepoprawnie sformatowane znaczniki itp.

Jednym z lepszych parserów dostępnych w .NET jest biblioteka Html Agility Pack http://htmlagilitypack.codeplex.com/

Do projektu bibliotekę można dodać poprzez ściągnięcie odpowiedniej dll z codeplex, bądź korzystając z NuGet-a:

PM> Install-Package HtmlAgilityPack

Po zainstalowaniu biblioteki możemy przystąpić do przetwarzania HTML:

Code:
using System;
using System.Collections.Generic;
using System.Net;
using HtmlAgilityPack;
using System.Linq;

namespace HtmlAgilityPack_Sample
{
    class Program
    {
        private static void Main(string[] args)
        {
            var webClient = new WebClient();
            string pageContent = webClient.DownloadString(@"http://www.onet.pl/");
            var htmlDocument = new HtmlDocument();
            htmlDocument.LoadHtml(pageContent);

            foreach (HtmlNode link in htmlDocument.DocumentNode.SelectNodes("//a[@href]"))
            {
                HtmlAttribute att = link.Attributes["href"];
                Console.WriteLine(att.Value);
            }

            foreach (var div in htmlDocument.DocumentNode.SelectNodes("//div"))
            {
                HtmlAttribute classAttribute = div.Attributes["class"];
                if (classAttribute != null)
                {
                    Console.WriteLine(classAttribute.Value);
                }
            }

        }
    }
}


Przykładowy kod powyżej pokazuje w jaki sposób pobrać ze strony wszystkie linki i znaczniki div. Zapytania pisane są w języku XPath.

Biblioteka jest dostępna jako opensource - możemy więc pobrać kod i dodawać nową funkcjonalność.

Na temat biblioteki można znaleźć krytyczne wypowiedzi, dotyczące m.in.:
  • w przypadku gdy metoda SelectNodes() nie zwróci rezultatów, zwracany jest null co uniemożliwia użycie takiego rezultatu jako rezultatu wyrażenia np. pętli foreach
  • kiedy szukamy dzieci podanego węzła, metoda SelectNode szuka od początku dokumentu (aby szukała od aktualnego węzła należy podać parametr descendant::)
Z moich testów wynika, że biblioteka dobrze radzi sobie z parsowaniem nawet bardzo kiepskiej jakości kodu HTML. Wyżej wymienione problemy można samemu poprawić (kod jest dostępny). Zachęcam do eksperymentowania i podzieleniem się uwagami na temat biblioteki.