poniedziałek, 23 kwietnia 2012

Egzamin 70-511 / 72-511

W najbliższym czasie zamierzam zdać egzamin MCTS 70-511 (lub jego odpowiednik 72-511 wersja studencka o połowę tańsza jeżeli ktoś jest jeszcze studentem).

Egzamin ten dotyczy Tworzenia aplikacji desktopowych z wykorzystaniem .NET 4.0
Egzamin ten zawiera pytania z takich obszarów projektowania aplikacji jak:
  1. Tworzenie prostego interfejsu użytkownika
    • Selekcja kontrolki najbardziej dopasowanej do wymagań danego zadania
    • Tworzenie hierarchicznego układu kontrolek na formatce (użycie różnorakich kontrolek pozwalających na ustawienie pozycji kontrolek)
    • Tworzenie i używanie stylów oraz skórek
    • Zarządzanie reużywalnymi zasobami (czcionki, style, źródła danych, obrazki)
    • Animacja w WPF (tworzenie storyboard, kontrola linii czasu, kontrola animacji za pomocą kodu i XAML)
  2. Udoskonalanie interfejsu użytkownika przy zastosowaniu zaawansowanych technik:
    • Routet events
    • Komendy (commands)
    • Modyfikacja interfejsu użytkownika w czasie działania programu (dodawanie/usuwanie kontrolek w czasie działania programu, dynamiczne generowanie szablonu)
    • Definiowanie własnych kontrolek
    • Tworzenie i wyświetlanie grafiki - bez uwzględnienia grafiki trójwymiarowej (3D)
    • Multimedia w aplikacji WPF
    • Szablony kontrolek w WPF
    • Tworzenie triggerów
  3. Zarządzanie danymi w warstwie prezentacji
    • Bindowanie danych do kontrolek
    • Tworzenie konwerterów danych (Value Converter)
    • Implementacja walidacji danych
    • Implementacja powiadamiania o zmianie danych
    • Przygotowanie kolekcji danych do wyświetlenia (filtrowanie, grupowanie, sortowanie, LINQ)
    • Bindowanie do hierarchicznych źródeł danych
    • Implementacja wyświetlania danych w DataGrid
    • Tworzenie szablonu wyświetlania danych
  4.  Poprawa i używalności aplikacji desktopowych
    • Integracja WPF i WindowsForms (ControlHosts, PropertyMap)
    • Implementacja wielowątkowości (TPL, parallel LINQ itp)
    • Globalizacja 
    • Drag & Drop
    • Bezpieczeństwo (SRP, CAS policy, UAC)
    • Zarządzanie ustawieniami użytkownika
    • Dependency propeteries
  5. Wydanie produktu
    • Testowanie aplikacji WPF
    • Debugowanie XAML przy użyciu WPF Visualizer
    • Wyszukiwanie błędów przy użyciu PresentationTraceSources
    • Konfiguracja wydania ClickOnce  
    • Tworzenie instalatora do aplikacji
    • Konfiguracja ustawień bezpieczeństwa wydania. 

Tak wygląda w skrócie plan nauki do egzamin 70-511 / 72-511. Materiały z których będę czerpać wiedzę, to przede wszystkim Training Kit 70-511, MSDN oraz inne książki przeznaczone do nauki WPF. 

sobota, 21 kwietnia 2012

LINQ Join Extension method

Zobaczmy na prosty model EF:






Standardowo chcąc wyciągnąć imię, nazwisko i miasto w którym mieszka klient, używamy SQL-o podobnej składni LINQ:


Code:
                var q = from c in contex.Customers
                        join ca in contex.CustomerAddresses on c.CustomerID equals ca.CustomerID
                        join a in contex.Addresses on ca.AddressID equals a.AddressID
                        select new{Customer = c, Address = a};

Takie zapytanie możemy przerobić do zapytania z wykorzystaniem Extension methods:


Code:
                var q2 = contex.Customers.Join(contex.CustomerAddresses, c => c.CustomerID, ca => ca.CustomerID,
                                               (c, ca) => new{Customer = c, CustomerAddress = ca})
                    .Join(contex.Addresses, ca => ca.CustomerAddress.AddressID, a => a.AddressID,
                          (ca, a) => new{ca.Customer, Address = a});

O ile pierwszy sposób jest bardzo czytelny, o tyle drugi może na pierwszy rzut oka wydawać się skomplikowany. Zwłaszcza metoda Join jest tutaj rozbudowana. Zobaczmy na jej budowę:


Code:
public static IQueryable<TResult> Join<TOuter,TInner,TKey,TResult>
(this IQueryable<TOuter> outer, 
IEnumerable<TInner> inner, 
Expression<Func<TOuter,TKey>> outerKeySelector, 
Expression<Func<TInner,TKey>> innerKeySelector, 
Expression<Func<TOuter,TInner,TResult>> resultSelector)

elementy które podajemy w tej metodzie:
  • outer - Extension method więc typ który jest przekazywany niejawnie (on nas nie interesuje na ten moment)
  • inner - źródło z którym nastąpi złączenie (w naszym przypadku była to tabela CustomerAddresses)
  • outerKeySelector oraz innerKeySelektor - dwa wyrażenia lambda, które definiują warunek złączenia (np. my łączymy po polu AddressId)
  • resultSelector - typ zwracany czyli odpowiednik select dla składni SQL-o podobnej


Code:
.Join
(źródło z którym łączymy, 
właściwość na podstawie której łaczymy zewnętrzna tabela, 
właściwość na podstawie której łaczymy wewnętrzna tabela, 
rezultat)

czwartek, 12 kwietnia 2012

Chroniony konstruktor w klasie abstrakcyjnej

Problem typu "the best design". Mamy klasę abstrakcyjną:

Code:
    public abstract class Connection
    {
        protected DbConnection DbConnection;

        public Connection(DbConnection dbConnection)
        {
            DbConnection = dbConnection;
        }
    }

Czy jest to poprawny kod? Tak jest z punktu widzenia kompilatora jest to całkowicie poprawny kod. Obiektu klasy abstrakcyjnej nie da się utworzyć. Tak więc w wielu źródłach można znaleźć informację, aby konstruktor uczynić chroniony - w końcu i tak bezpośrednio z niego nie skorzystamy.
Lepsza definicja klasy będzie mieć więc następującą postać:

Code:
    public abstract class Connection
    {
        protected DbConnection DbConnection;

        protected Connection(DbConnection dbConnection)
        {
            DbConnection = dbConnection;
        }
    }

poniedziałek, 9 kwietnia 2012

Sprawdzenie czy login został już zajęty - atrybut Remote

W ASP.MVC 3 wykorzystano tzw. Unobtrusive JavaScript. W skrócie jest zbiór zasad, jak budować strony z wykorzystaniem JavaScript.

Mój scenariusz przewiduje tworzenie konta użytkownika. W bazie danych login jest polem unikalnym, co znaczy że nie może być dwóch użytkowników o takim samym loginie, czy też takim samym mailem. Standardowo walidacja przeprowadzana jest na serwerze, chciałem także, aby użytkownik został od razu poinformowany podczas wprowadzania loginu że jest on zajęty.
W MVC 3 jest to niezwykle proste.
Na początek w modelu, dodaję do właściwości UserName dodaję atrybut Remote:

Code:
        [Required]
        [Display(Name = "Nazwa użytkownika")]
        [Remote("IsUserExistInDatabase", "Account", HttpMethod = "POST", ErrorMessage = "Taki użytkownik już istnieje. Proszę wybrać inny login.")]
        public string UserName { get; set; }

Elementami składowymi są kolejno:
  • IsUserExistInDatabase - nazwa metody (akcji) która zostanie wywołana podczas walidacji
  • Account - nazwa kontrolera w którym akcja walidacji się znajduje
  • HttpMethod - w jaki sposób ma zostać wysłane zapytanie do kontrolera
  • ErrorMessage - komunikat błędu który ma zostać wyświetlony użytkownikowi w przypadku gdy walidacja nie przejdzie pomyślnie
Następnie w widoku dodajemy odnośniki do skryptów z walidacją:

Code:
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>

Teraz pozostaje jeszcze ustalić w którym miejscu widoku ma zostać wyświetlony komunikat z błędem odnośnie loginu:

Code:
@Html.ValidationMessageFor(x => x.UserName)

Ostatnia czynność to napisanie metody w wskazanym kontrolerze, która jako parametr przyjmuje wartość naszego pola, a jako rezultat zwraca Json:

Code:
        [HttpPost]
        public JsonResult IsUserExistInDatabase(string username)
        {
            var user = Membership.GetUser(username);

            return Json(user == null);
        }

Ot tyle, aby uzyskać walidację istnienia loginu.

Ściąga z Fluent API i atrybutów

Przygotowałem małą fiszkę, która może być pomocna podczas modelowania klas wykorzystujących notację atrybutową i odpowiedniki w Fluent API:


W kolejnej postaram się umieścić mapowania.

niedziela, 8 kwietnia 2012

Custom RoleProvider ASP.NET

Zabrałem się za projekt w ASP.NET MVC 3. W planach jest napisanie nowego, rewolucyjnego systemu :) można więc spodziewać się wpisów związanych z tą technologią.
Problem: w projekcie będzie możliwość zakładania kont użytkowników. Będzie także podział na role. Początkowo myślałem o wykorzystaniu gotowego rozwiązania (ASP.NET providers), jednak nie do końca mi się ono podoba. MembershipProvider i RoleProvider to świetne mechanizmy, ale tworzą w bazie standardowo niepotrzebny narzut zbędnych kolumn. Dlatego wziąłem się za pisanie ich pod mój system od początku.
O tworzeniu MembershipProvidera pisałem już we wcześniejszym poście, w tym trochę porad jak napisać RoleProvidera.

Sprawa jest łatwiejsza niż w przypadku MembershipProvidera, ze względu na to iż ilość metod które należy zaimplementować jest dużo mniejsza.

Startując od początku:

W bazie potrzebujemy odpowiednich tabel do przetrzymywania danych o użytkownikach i ich rolach:


Standardowa realizacja wiele do wiele (użytkownik może być przypisany do więcej niż jednej roli).

Wystartujemy od szablonu:

    public class MyRoleProvider : RoleProvider
    {
        public override bool IsUserInRole(string username, string roleName)
        {
            throw new NotImplementedException();
        }

        public override string[] GetRolesForUser(string username)
        {
            throw new NotImplementedException();
        }

        public override void CreateRole(string roleName)
        {
            throw new NotImplementedException();
        }

        public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
        {
            throw new NotImplementedException();
        }

        public override bool RoleExists(string roleName)
        {
            throw new NotImplementedException();
        }

        public override void AddUsersToRoles(string[] usernames, string[] roleNames)
        {
            throw new NotImplementedException();
        }

        public override void RemoveUsersFromRoles(string[] usernames, string[] roleNames)
        {
            throw new NotImplementedException();
        }

        public override string[] GetUsersInRole(string roleName)
        {
            throw new NotImplementedException();
        }

        public override string[] GetAllRoles()
        {
            throw new NotImplementedException();
        }

        public override string[] FindUsersInRole(string roleName, string usernameToMatch)
        {
            throw new NotImplementedException();
        }

        public override string ApplicationName
        {
            get { throw new NotImplementedException(); }
            set { throw new NotImplementedException(); }
        }
    }

Zaczniemy od potrzebnych repozytoriów:

      using System.Linq;
using BusinessObjects.Entities;
using DataAccessLayer.Repositories.Contract;

namespace DataAccessLayer.Repositories.Implementation
{
    public class RoleRepository : BaseRepository, IRoleRepository
    {
        public void AddRole(Role role)
        {
            AddRole(role.Rolename);
        }

        public void AddRole(string roleName)
        {
            ChemistContext.Roles.Add(new Role{Rolename = roleName});
        }

        public IQueryable GetAllRoles()
        {
            return ChemistContext.Roles.AsQueryable();
        }

        public Role GetRoleByName(string roleName)
        {
            return ChemistContext.Roles.SingleOrDefault(x => x.Rolename == roleName);
        }

        public Role GetRoleById(int id)
        {
            return ChemistContext.Roles.SingleOrDefault(x => x.Id == id);
        }

        public IQueryable GetRolesForUser(string userName)
        {
            var userRoles = from user in ChemistContext.Users
                            join userInRoles in ChemistContext.UserInRoles on user.Id equals userInRoles.UserId
                            join roles in ChemistContext.Roles on userInRoles.RoleId equals roles.Id
                            where user.Login == userName
                            select roles;

            return userRoles;
        }

        public void DeleteRole(string roleName)
        {
            var role = ChemistContext.Roles.SingleOrDefault(x => x.Rolename == roleName);
            if (role != null)
            {
                ChemistContext.Roles.Remove(role);
                ChemistContext.SaveChanges();
            }
        }
    }
}

using System.Linq;
using BusinessObjects.Entities;
using DataAccessLayer.Repositories.Contract;

namespace DataAccessLayer.Repositories.Implementation
{
    public class UserInRoleRepository : BaseRepository, IUserInRoleRepository
    {
        public void AddUserToRole(UserInRole userInRole)
        {
            AddUserToRole(userInRole.UserId, userInRole.RoleId);
        }

        public void AddUserToRole(int idUser, int idRole)
        {
            ChemistContext.UserInRoles.Add(new UserInRole{UserId = idUser, RoleId = idRole});
            ChemistContext.SaveChanges();
        }

        public void AddUserToRole(string userName, string roleName)
        {
            var idUser = ChemistContext.Users.Single(x => x.Login == userName).Id;
            var idRole = ChemistContext.Roles.Single(x => x.Rolename == roleName).Id;
            AddUserToRole(idUser, idRole);
        }

        public IQueryable GetAll()
        {
            return ChemistContext.UserInRoles.AsQueryable();
        }

        public UserInRole GetByIdRoleIdUser(int idRole, int idUser)
        {
            return ChemistContext.UserInRoles.SingleOrDefault(x => x.RoleId == idRole && x.UserId == idUser);
        }

        public UserInRole GetByUserNameRoleName(string userName, string roleName)
        {
            var userInRole = from users in ChemistContext.Users
                             join userInRoles in ChemistContext.UserInRoles on users.Id equals userInRoles.UserId
                             join roles in ChemistContext.Roles on userInRoles.RoleId equals roles.Id
                             where users.Login == userName && roles.Rolename == roleName
                             select userInRoles;

            return userInRole.SingleOrDefault();
        }

        public void DeleteRoleAndUsers(string roleName)
        {
            var userRolesToDelete = from roles in ChemistContext.Roles
                                    join userInRoles in ChemistContext.UserInRoles on roles.Id equals userInRoles.RoleId
                                    where roles.Rolename == roleName
                                    select userInRoles;
            foreach (var userInRole in userRolesToDelete)
            {
                ChemistContext.UserInRoles.Remove(userInRole);
            }

            ChemistContext.SaveChanges();
        }

        public void DeleteUserFromRole(string userName, string roleName)
        {
            var usersInRole = from users in ChemistContext.Users
                              join userInRoles in ChemistContext.UserInRoles on users.Id equals userInRoles.UserId
                              join roles in ChemistContext.Roles on userInRoles.RoleId equals roles.Id
                              where roles.Rolename == roleName && users.Login == userName
                              select userInRoles;

            foreach (var user in usersInRole)
            {
                ChemistContext.UserInRoles.Remove(user);
            }

            ChemistContext.SaveChanges();
        }

        public string[] GetByRoleName(string roleName)
        {
            var usersInRole = from users in ChemistContext.Users
                              join userInRoles in ChemistContext.UserInRoles on users.Id equals userInRoles.RoleId
                              join roles in ChemistContext.Roles on userInRoles.RoleId equals  roles.Id
                              where roles.Rolename == roleName
                              select users.Login;

            return usersInRole.ToArray();
        }

        public string[] GetByRoleNameContains(string roleName)
        {
            var usersInRole = from users in ChemistContext.Users
                              join userInRoles in ChemistContext.UserInRoles on users.Id equals userInRoles.RoleId
                              join roles in ChemistContext.Roles on userInRoles.RoleId equals roles.Id
                              where roles.Rolename.Contains(roleName)
                              select users.Login;

            return usersInRole.ToArray();
        }
    }
}

Nie pozostaje nic innego jak zaimplementowanie samego RoleProvidera:

using System;
using System.Configuration.Provider;
using System.Transactions;
using System.Web.Security;
using BusinessObjects.Enums;
using DataAccessLayer.Repositories.Contract;
using DataAccessLayer.Repositories.Implementation;
using System.Linq;
using Services.Contract;
using Services.Implementation;

namespace Chemist.Infrastructure
{
    public class ChemistRoleProvider : RoleProvider
    {
        private readonly IRoleRepository roleRepository;
        private readonly IUserInRoleRepository userInRoleRepository;
        private readonly IExceptionService exceptionService = new ExceptionService(new EventLogRepository());
        private string applicationName = "Chemistry";

        public ChemistRoleProvider()
        {
            roleRepository = new RoleRepository();
            userInRoleRepository = new UserInRoleRepository();
        }

        public override bool IsUserInRole(string userName, string roleName)
        {
            var isUserInRole = userInRoleRepository.GetByUserNameRoleName(userName, roleName) != null;

            return isUserInRole;
        }

        public override string[] GetRolesForUser(string userName)
        {
            return roleRepository.GetRolesForUser(userName).Select(x => x.Rolename).ToArray();
        }

        public override void CreateRole(string roleName)
        {
            roleRepository.AddRole(roleName);
        }

        public override bool DeleteRole(string roleName, bool throwOnPopulatedRole)
        {
            if (!RoleExists(roleName))
            {
                throw new ProviderException("Role does not exist.");
            }
            if (throwOnPopulatedRole && GetUsersInRole(roleName).Length > 0)
            {
                throw new ProviderException("Cannot delete a populated role.");
            }

            try
            {
                using (
                    var transaction = new TransactionScope(TransactionScopeOption.Required,
                                                           new TransactionOptions
                                                           {IsolationLevel = IsolationLevel.ReadCommitted}))
                {
                    userInRoleRepository.DeleteRoleAndUsers(roleName);
                    roleRepository.DeleteRole(roleName);
                    transaction.Complete();
                    return true;
                }
            }
            catch (Exception ex)
            {
                exceptionService.Write(ex, EventSource.RoleProvider);
                return false;
            }
        }

        public override bool RoleExists(string roleName)
        {
            var roleExist = roleRepository.GetRoleByName(roleName);

            return roleExist != null;
        }

        public override void AddUsersToRoles(string[] userNames, string[] roleNames)
        {
            foreach (var rolename in roleNames)
            {
                if (!RoleExists(rolename))
                {
                    throw new ProviderException("Role name not found.");
                }
            }

            foreach (var username in userNames)
            {
                if (username.Contains(","))
                {
                    throw new ArgumentException("User names cannot contain commas.");
                }

                foreach (var rolename in roleNames)
                {
                    if (IsUserInRole(username, rolename))
                    {
                        throw new ProviderException("User is already in role.");
                    }
                }
            }

            using (var transaction = new TransactionScope())
            {
                foreach (var username in userNames)
                {
                    foreach (var rolename in roleNames)
                    {
                        userInRoleRepository.AddUserToRole(username, rolename);
                    }
                }

                transaction.Complete();
            }
        }

        public override void RemoveUsersFromRoles(string[] userNames, string[] roleNames)
        {
            foreach (var rolename in roleNames)
            {
                if (!RoleExists(rolename))
                {
                    throw new ProviderException("Role name not found.");
                }
            }

            foreach (var username in userNames)
            {
                foreach (var rolename in roleNames)
                {
                    if (!IsUserInRole(username, rolename))
                    {
                        throw new ProviderException("User is not in role.");
                    }
                }
            }

            foreach (string username in userNames)
            {
                foreach (string rolename in roleNames)
                {
                    userInRoleRepository.DeleteUserFromRole(username, rolename);
                }
            }
        }

        public override string[] GetUsersInRole(string roleName)
        {
            return userInRoleRepository.GetByRoleName(roleName);
        }

        public override string[] GetAllRoles()
        {
            return roleRepository.GetAllRoles().Select(x => x.Rolename).ToArray();
        }

        public override string[] FindUsersInRole(string roleName, string usernameToMatch)
        {
            return userInRoleRepository.GetByRoleNameContains(roleName);
        }

        public override string ApplicationName
        {
            get { return applicationName; }
            set { applicationName = value; }
        }
    }
}

Ostatnim krokiem, który należy wykonać, to dodanie w Web.configu informacji o tym, aby został użyty nasz RoleProvider:


Code:
    <roleManager enabled="true" defaultProvider="ChemistRoleProvider">
      <providers>
        <add name="ChemistRoleProvider" type="Chemist.Infrastructure.ChemistRoleProvider"/>
      </providers>
    </roleManager>



Ilość kodu może wydać się duża, jednak raz napisany RoleProvider jest przeważnie wykorzystywany w wielu późniejszych projektach.

sobota, 7 kwietnia 2012

Entity Framework 5.0

Wraz z nowym Visual Studio 11 otrzymamy możliwość skorzystać z Entity Framework 5.0.
Co nowego znajdziemy w tej wersji?
  • .NET 4.5
  • Wsparcie dla typów wyliczeniowych
  • Typy przestrzenne 
  • Zwiększenie wydajności
Database First:
  • Obsługa procedur z wieloma rezultatami
  • Obsługa funkcji zwracających jako wynik działania tabele
Ulepszenia w designerze:
  • Wiele modelów dla diagramów
  • Możliwość kolorowania encji
  • Batchowy import procedur składowanych
W tym momencie dostępny jest VS 11 w wersji Beta. Można w nim zainstalować EF 5.0 Beta 2 dzięki czemu można sprawdzić nowości, które w nim się znajdą.

Entity Framework Code First dla istniejącej bazy

Ostatni już możliwy scenariusz stworzenia modelu Entity Framework to Code First dla istniejącej bazy danych.

Aby skorzystać z tej opcji, na początek musimy zainstalować Entity Framework Power Tools.
Po zainstalowaniu w meni kontekstowym projektu otrzymamy nową pozycję: Entity Framework:


Wybieramy opcję Reverse Engineer Code First i konfigurujemy połączenie dla bazy którą chcemy obłsużyć:


Po kliknięciu na Ok zostaną wygenerowane klasy z modelem, mapowaniem oraz Context-em:


Wydaje mi się że jest to dużo lepsze rozwiązanie niż korzystanie z Database First gdyż otrzymujemy zbiór bardzo czystych klas, które następnie łatwo możemy rozbudowywać o nowe pola, mapowania itd.

Entity Framework Database First

Trzecią możliwością wykorzystania EF, jest tzw. Database First. Jak sama nazwa mówi - baza istnieje, chcemy do niej się odwoływać za pomocą EF.

Dodanie do projektu EF Database First jest podobne do tego z Model Fist.
Dodajemy nowy item do projektu i podobnie jak poprzednio wybieramy ADO.NET Entity Data Model:


W wizardzie, tym razem wybieramy opcję Generate From Database:


Ustawiamy połączenie do istniejącej już bazy danych:


Następnie mamy możliwość wyboru, które tabele, widoki,  procedury i funkcje zostaną zmapowane:


Dodatkowo możemy zaznaczyć opcje:
  • Pluralize or singularize generated object names - czyli odpowiednio dodaje -es w nazwach kolekcji (ma to sens w przypadku używania angielskich nazw, w polskojęzycznych nazwach nie wygląda to dobrze)
  • Include foreign key columns in the model - generuje w encjach zawierających asocjację, także jawną kolumnę, która łączy dane encje (ujmując prościej mamy dostęp do kolumny będącej FK dla danej encji)
Po kliknięciu w Finish zostaje wygenerowany model:


Po zbudowaniu aplikacji możemy korzystać z standardowego Context-u, bądź też jak opisywałem to post wcześniej możemy dodać generator prostszego Contextu, znanego z Code First.

Entity Framework Model First - lepszy Context

W ostatnim poście pisałem o tym w jaki sposób stworzyć aplikację korzystającą z Entity Framework Model First. Tym razem chciałbym się skupić nad możliwością wykorzystania DbContex-ta w Model First.
Jeżeli spojrzymy na projekt który ostatnio tworzyliśmy i spróbujemy wyświetlić IntelliSense na Contexcie zobaczy:





Jak widać wiele metod, właściwości. Code First zawierał dużo bardziej czytelny schemat.
Spróbujemy wykorzystać DbContext zawarty w EF > 4.1. W tym celu instalujemy EF (np. za pomocą NuGet-a).

Po zainstalowaniu przechodzimy do Modelu w EF i wybieramy opcję Add Code Generation Item... :


Wybieramy ADO.NET DbContext Generator:


Po kliknięciu na Add do naszej solucji zostanie dodanych kilka plików, jednym z nich jest nowy Context widoczny na poniższym screenie:


Jeżeli teraz zobaczymy na zawartość IntelliSense, możemy zobaczyć iż wykorzystuje ono tylko i wyłącznie nowy DbContext:

Entity Framework Model First

Entity Framework umożliwia budowanie bazy danych w sposób wizualny - za pomocą designera. Ostatnie artykuły były poświęcone budowaniu bazy z poziomu kodu - Code First. Ten post zostanie poświęcony kolejnej możliwości a mianowicie Model First.

Aby stworzyć model do naszego projektu dodajemy nowy element typu ADO.NET Entity Data Model:


Po kliknięciu w przycisk Add, otrzymamy pierwszy krok wizarda, w którym wybieramy opcję Empty Model:


Klikamy na Finish.

Zostaje stworzona powierzchnia designera, na której możemy w sposób graficzny tworzyć encje i połączenia pomiędzy nimi:


Po zakończeniu tworzenia modelu klikamy lewym przyciskiem myszy na powierzchni designera i wybieramy opcję Generate Database from Model...


Ukazuje się nam okienko służące do definiowania gdzie ma zostać stworzona baza danych:


Nie chce nadpisać istniejącej bazy, a stworzyć nową - wybieram opcje New Connection... i konfiguruję połączenie do bazy danych:


W kolejnym kroku otrzymamy okienko z podglądem wygenerowanego skryptu SQL:


Po kliknięciu w Finish zostaniemy przeniesieni do VS skąd możemy uruchomić powstały skrypt:


Po stworzeniu bazy danych możemy już korzystać z bazy:
Code:
            using (var context = new TestDatabaseContainer())
            {
                context.Users.AddObject(new User {FirstName = "Jacek", LastName = "Kowalski"});
                context.SaveChanges();
            }

Entity Framework - strategie inicjalizacji

Entity Framework posiada w sobie mechanizm inicjalizacji - w jaki sposób zostanie obsłużona baza danych podczas tworzenia Contextu do niej.

Do dyspozycji z pudełka mamy cztery strategie:
  • CreateDatabaseIfNotExists - tworzy bazę danych jeżeli jeszcze nie została stworzona
  • DropCreateDatabaseAlways - przed stworzeniem Contextu usuwa i tworzy bazę na nowo
  • DropCreateDatabaseIfModelChanges - usuwa i tworzy bazę na nowo w  przypadku zmiany w modelu (np. dodanie kolumny)
  • MigrateDatabaseToLatestVersion - używając Code Migrations (pisałem o tym we wcześniejszych postach) uaktualnia bazę danych do najnowszej migracji
 Wykorzystanie wygląda następująco:

Code:
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<DbContext>());

Gdzie umieścić powyższą linijkę?
Powinna być ona umieszczona przed pierwszym utworzeniem DbContext-u w naszej aplikacji.
Przykładowo dla aplikacji ASP.NET będzie to Application_Start() umieszczone w Global.ascx.cs

Możemy także stworzyć własną inicjalizację, poprzez zaimplementowanie interfejsu IDatabaseInitializer


Code:
    public class MyInitializationStrategy : IDatabaseInitializer<MySampleContext>
    {
        public void InitializeDatabase(MySampleContext context)
        {
            //My initialization code
        }
    }

piątek, 6 kwietnia 2012

LEFT JOIN w LINQ

W SQLu oprócz złączeń wewnętrznych (INNER) mamy także złączenia zewnętrzne (OUTER). Złączenia zewnętrzne rozszerzają złączenia wewnętrzne, uzupełniając wynik złączenia wewnętrznego o rekordy które nie zostałyby włączone do wyniku z powodu warunku złączenia.

Na warsztat weźmiemy bazę danych AdventureWorksLT2008R2 (dostępna do zainstalowania jako sample do SQL Server 2008 R2) i proste zapytanie, którego celem jest wykazanie kategorii dla których nie ma żadnych produktów:

SELECT pc.Name
FROM SalesLT.ProductCategory pc LEFT JOIN SalesLT.Product p ON pc.ProductCategoryID = p.ProductCategoryID
WHERE p.ProductID IS NULL

Jak przełożyć to zapytanie na LINQ?
Code:
            using(var context = new AdventureWorks())
            {
                var categoriesWithoutProducts = from productCategories in context.ProductCategory
                                                join product in context.Product on
                                                    productCategories.ProductCategoryID equals
                                                    product.ProductCategoryID into prodCat
                                                from product in prodCat.DefaultIfEmpty()
                                                where product == null
                                                select productCategories.Name;

                var sql = ((ObjectQuery) categoriesWithoutProducts).ToTraceString();

                foreach (var categoriesWithoutProduct in categoriesWithoutProducts)
                {
                    Console.WriteLine(categoriesWithoutProduct);
                }

            }

Wykorzystujemy tutaj tzw. Grouped Join.
Zgodnie z tym co można znaleźć na MSDN, Grouped Join łączy wszystkie elementy kolekcji pierwszej z pasującymi w drugiej. Oznacza to, że jeżeli w drugiej kolekcji nie będzie elementów pasujących do elementów z pierwszej kolekcji, w wyniku otrzymamy element pierwszej kolekcji bez z pustym zbiorem elementów z drugiej kolekcji.

środa, 4 kwietnia 2012

Type.GetType(string) - czyli pobranie informacji o typie

Dzisiaj, krótko o problemie który natrafiłem przypadkiem i spalił trochę czasu.

Jak wiadomo za pomocą kodu:

Code:
var typeInfo = typeof (string);

można pobrać informacje o interesującym nas typie. Co jednak jeżeli chcemy dynamicznie pobrać informacje np. poprzez wysłanie nazwy typu jako ciągu znaków (string)?
Otóż o ile w przypadku kiedy nasza klasa znajduje się w tej samej solucji co kod pobierający informacje o typie - jest proste:


to w przypadku jeżeli klasa jest w innej bibliotece, nie od razu jest takie trywialne:


Aby poprawnie zainicjować klasę Type korzystając z tej metody należy podać wszystkie informacje o dll-ce z której będziemy korzystać, a mianowicie należy podać informacje zawarte w polu AssemblyQualifiedName:


niedziela, 1 kwietnia 2012

EXIST w LINQ

Niedawno szukałem w jaki sposób generować za pomocą LINQ klauzulę EXIST.
Otóż istnieje prosty wzorzec na generowanie tej klauzuli:


Code:
            using (var context = new AdventureWorksLT2008R2Entities())
            {
                var q = from p in context.Product
                        where context.Product.Any(x => x.ListPrice > 1490)
                        select p;
                var sql = ((ObjectQuery) q).ToTraceString();
            }

Kluczem jest tutaj metoda Any. Właśnie ta metoda tworzy klauzulę EXIST, jeżeli chcemy uzyskać NOT EXIST wystarczy zaprzeczyć.

Wygenerowany SQL ma następującą postać:

SELECT
[Extent1].[ProductID] AS [ProductID],
[Extent1].[Name] AS [Name],
[Extent1].[ProductNumber] AS [ProductNumber],
[Extent1].[Color] AS [Color],
[Extent1].[StandardCost] AS [StandardCost],
[Extent1].[ListPrice] AS [ListPrice],
[Extent1].[Size] AS [Size],
[Extent1].[Weight] AS [Weight],
[Extent1].[ProductCategoryID] AS [ProductCategoryID],
[Extent1].[ProductModelID] AS [ProductModelID],
[Extent1].[SellStartDate] AS [SellStartDate],
[Extent1].[SellEndDate] AS [SellEndDate],
[Extent1].[DiscontinuedDate] AS [DiscontinuedDate],
[Extent1].[ThumbNailPhoto] AS [ThumbNailPhoto],
[Extent1].[ThumbnailPhotoFileName] AS [ThumbnailPhotoFileName],
[Extent1].[rowguid] AS [rowguid],
[Extent1].[ModifiedDate] AS [ModifiedDate]
FROM [SalesLT].[Product] AS [Extent1]
WHERE  EXISTS (SELECT
    1 AS [C1]
    FROM [SalesLT].[Product] AS [Extent2]
    WHERE [Extent2].[ListPrice] > cast(1490 as decimal(18))
)

IN vs EXIST

Czym różni się IN od EXIST? Kiedy zastosować IN a kiedy EXIST?

Zobaczmy na proste zapytanie z IN:
SELECT [lista_kolumn] FROM [table1] WHERE [column1] IN (SELECT [column2] FROM [table2])

oraz to samo z użyciem EXIST
SELECT [lista_kolumn] FROM [table1] WHERE EXIST (SELECT 1 FROM [table2] WHERE [table1].[column1] = [table2].[column2])

Przetwarzanie klauzuli z IN będzie mieć postać:
  • Przetworzenie zapytania wewnętrznego
  • Rezultat zapytania zewnętrznego zostanie obcięty o powtarzające się rekordy oraz zostanie nałożony indeks
  • Rezultat powyższych operacji zostanie złączony (JOIN) z tabelą zewnętrzną
Przetwarzanie w przypadku EXIST:
  • Dla każdej wartości w tabeli pierwszej (table1) znajdź pasującą wartość w tabeli drugiej (table2)
  • Jeżeli znaleziono wartość, pobierz ją i przejdź do kolejnego rekordu.
Kiedy więc użyć IN a kiedy EXIST?
W przypadku kiedy tabela pierwsza (table1) jest mała, a tabela druga (table2) jest duża skorzystamy z EXIST. Dlaczego? Jeżeli na kolumnie [table2].[column2] będzie nałożony indeks operacja: WHERE [table1].[column1] = [table2].[column2] wykonuje się bardzo szybko.

W przypadku kiedy tabela1 będzie dużych rozmiarów, a wynik wewnętrznego zapytania mały (table2) warto zastosować klauzulę IN.

Jeżeli wyniki obu zapytań (wewnętrznego i zewnętrznego) będą dużych rozmiarów, zarówno IN jak i EXIST będą mieć podobną wydajność.

Podsumowując:
  • zapytanie zewnętrzne zwraca mało wyników, wewnętrzne dużo EXIST
  • zapytanie zewnętrzne dużo wyników, wewnętrzne mało IN
  • oba zapytania zwracają dużą ilość rekordów - nie ma znaczenia (warto zbadać wtedy plan zapytania aby rozstrzygnąć ostateczne rozwiązanie)

Entity Framework Code First Fluent Api - definiowanie poza DbContextem

Tworząc model z wykorzystaniem Fluent API lepiej umieścić kod definiujący strukturę poza Contextem.
Zobaczmy na przykład.

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

    public class Address
    {
        public int Id { get; set; }
        public string Street { get; set; }
        public string City { get; set; }
        public string BuildingNumber { get; set; }
    }

    class TestContext : DbContext
    {
        public DbSet Persons { get; set; }
        public DbSet
Addresses { get; set; } public TestContext(string connectionString) : base(connectionString) { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity().ToTable("Person"); modelBuilder.Entity().HasKey(x => x.Id); modelBuilder.Entity().Property(x => x.Id).HasColumnOrder(1); modelBuilder.Entity().Property(x => x.FirstName).HasMaxLength(50).IsRequired().HasColumnOrder(2); modelBuilder.Entity().Property(x => x.LastName).HasMaxLength(50).IsRequired().HasColumnOrder(3); modelBuilder.Entity
().ToTable("Address"); modelBuilder.Entity
().HasKey(x => x.Id); modelBuilder.Entity
().Property(x => x.Id).HasColumnOrder(1); modelBuilder.Entity
().Property(x => x.City).HasMaxLength(50).IsRequired().HasColumnOrder(2); modelBuilder.Entity
().Property(x => x.Street).HasMaxLength(100).IsRequired().HasColumnOrder(3); modelBuilder.Entity
().Property(x => x.BuildingNumber).HasMaxLength(10).IsRequired().HasColumnOrder(4); modelBuilder.Entity().HasOptional(x => x.Address).WithMany().HasForeignKey(x => x.IdAddress).WillCascadeOnDelete(true); } }

Widać że kodu mapującego jest już 11 linijek. Każda aplikacja (poza demonstracyjnymi :) ), składa się z większej ilości tabel = więcej kodu mapującego.
Można sobie zadać pytanie ile linii będzie mieć metoda OnModelCreating kiedy dodamy kolejne tabele z ich mapowaniem.
Aby tego uniknąć można skorzystać z klas konfiguracyjnych, które przenoszą informacje o mapowaniu poza metodę OnModelCreating:

    class TestContext : DbContext
    {
        public DbSet Persons { get; set; }
        public DbSet
Addresses { get; set; } public TestContext(string connectionString) : base(connectionString) { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new PersonConfiguration()); modelBuilder.Configurations.Add(new AddressConfiguration()); } } public class PersonConfiguration : EntityTypeConfiguration { public PersonConfiguration() { ToTable("Person"); HasKey(x => x.Id); Property(x => x.Id).HasColumnOrder(1); Property(x => x.FirstName).HasMaxLength(50).IsRequired().HasColumnOrder(2); Property(x => x.LastName).HasMaxLength(50).IsRequired().HasColumnOrder(3); HasOptional(x => x.Address).WithMany().HasForeignKey(x => x.IdAddress).WillCascadeOnDelete(true); } } public class AddressConfiguration : EntityTypeConfiguration
{ public AddressConfiguration() { ToTable("Address"); HasKey(x => x.Id); Property(x => x.Id).HasColumnOrder(1); Property(x => x.City).HasMaxLength(50).IsRequired().HasColumnOrder(2); Property(x => x.Street).HasMaxLength(100).IsRequired().HasColumnOrder(3); Property(x => x.BuildingNumber).HasMaxLength(10).IsRequired().HasColumnOrder(4); } }

Kod staje się bardziej czytelny, a samo dodawania następnych konfiguracji stanowczo się upraszcza.

Relacje w EntityFramework Code First Fluent API

Tym razem bardziej "ćwiczebnie", czyli o definiowaniu relacji w EF code first, przy użyciu Fluent API.
Często się zdarza iż są z tym problemy, w tym artykule przedstawię mini ściągę jak sobie z tym problemem radzić - na kilku przykładach.

Przykłady transformacji zostały zaczerpnięte ze strony http://wazniak.mimuw.edu.pl (kurs baz danych)

1. Relacja 1:1:


Kod C#:

    public class Pracownik
    {
        public int IdPracownika { get; set; }
        public string Nazwisko { get; set; }
        public string Etat { get; set; }
        public decimal Pensja { get; set; }
    }

    public class Samochod
    {
        public string NrRejestracyjny { get; set; }
        public string Marka { get; set; }
        public string Model { get; set; }
        public int IdPracownika { get; set; }
        public Pracownik Pracownik { get; set; }
    }

    class TestContext : DbContext
    {
        public DbSet Pracownicy { get; set; }
        public DbSet Samochody { get; set; }

        public TestContext(string connectionString) : base(connectionString)
        {
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity().HasKey(x => x.IdPracownika);
            modelBuilder.Entity().HasKey(x => x.NrRejestracyjny);
            modelBuilder.Entity().Property(x => x.NrRejestracyjny).HasMaxLength(10);

            modelBuilder.Entity().HasRequired(x => x.Pracownik).WithMany().HasForeignKey(x => x.IdPracownika);
        }
    }

Wynik uruchomienia i powstałe struktury w bazie:




2. Relacja 1:M:


Kod C#:

    public class Pracownik
    {
        public int IdPracownika { get; set; }
        public string Nazwisko { get; set; }
        public string Etat { get; set; }
        public decimal Pensja { get; set; }
        public Dzial Dzial { get; set; }
        public int IdDzial { get; set; }
    }

    public class Dzial
    {
        public int IdDzial { get; set; }
        public string Nazwa { get; set; }
        public string Siedziba { get; set; }
        public IList Pracownicy { get; set; }
    }

    class TestContext : DbContext
    {
        public DbSet Pracownicy { get; set; }
        public DbSet Dzialy { get; set; }

        public TestContext(string connectionString) : base(connectionString)
        {
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity().HasKey(x => x.IdDzial);
            modelBuilder.Entity().HasKey(x => x.IdPracownika);

            modelBuilder.Entity().HasRequired(x => x.Dzial).WithMany(x => x.Pracownicy).HasForeignKey(x => x.IdDzial);
        }
    }
1
W bazie będzie się przedstawiać w następujący sposób:



3. Relacja typu M:N:


W tym przypadku EF CF bardzo dobrze generuje schemat już z samego kodu, a mianowicie:

    public class Pracownik
    {
        public int IdPracownika { get; set; }
        public string Nazwisko { get; set; }
        public string Etat { get; set; }
        public decimal Pensja { get; set; }
        public virtual IList Projekty { get; set; } 
    }

    public class Projekt
    {
        public int NrProjektu { get; set; }
        public string Nazwa { get; set; }
        public string Sponsor { get; set; }
        public virtual IList Pracownicy { get; set; }
    }

Takie rozwiązanie jest dobre jednak ma jedną wadę - zawsze lepiej mieć tabelę pośrednią zdefiniowaną w kodzie. Ułatwia to późniejsze konstruowanie zapytań. Dlatego skorzystamy i tutaj z Fluent Api:

    public class Pracownik
    {
        public int IdPracownika { get; set; }
        public string Nazwisko { get; set; }
        public string Etat { get; set; }
        public decimal Pensja { get; set; }
        public int IdProjekt { get; set; }
    }

    public class Projekt
    {
        public int NrProjektu { get; set; }
        public string Nazwa { get; set; }
        public string Sponsor { get; set; }
        public int IdPracownik { get; set; }
    }

    public class PracownicyProjekty
    {
        public int IdPracownik { get; set; }
        public int IdProjekt { get; set; }

        public Pracownik Pracownik { get; set; }
        public Projekt Projekt { get; set; }
    }

    class TestContext : DbContext
    {
        public DbSet Pracownicy { get; set; }
        public DbSet Projekty { get; set; }
        public DbSet PracownicyProjekty { get; set; }

        public TestContext(string connectionString) : base(connectionString)
        {
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity().HasKey(x => x.NrProjektu);
            modelBuilder.Entity().HasKey(x => x.IdPracownika);
            modelBuilder.Entity().HasKey(x => new { x.IdPracownik, x.IdProjekt });
            modelBuilder.Entity().HasRequired(x => x.Pracownik).WithMany().HasForeignKey(x => x.IdPracownik).WillCascadeOnDelete(false);
            modelBuilder.Entity().HasRequired(x => x.Projekt).WithMany().HasForeignKey(x => x.IdProjekt);
        }
    }

Oczywiście jest to propozycja, która może ułatwić w przyszłości pisanie zapytań LINQ.