(MVC) MVC (2018)

Entity Framework missing FAQ (Part 1 and Part 2)

Entity Framework missing FAQ (Part 3).

Я, нажаль, не встиг закінчити другу частину у минулому році, вибачаюсь, бо не мав достатньо вільного часу, тому закінчую лише зараз.





3.9 Склад стартового проєкту.

Стартовий проєкт цієї сторінки знаходиться ось тут, він нормально компілюється. Цю сторінку я робив трохи пізніше попередньої частини, тому проапгрейдів проєкт до NET 4.7.2.



Нище ви бачите опис файлів стартового проєкту цієї сторінки:

Сурогат вьюшек SQL-серверу, визначений на рівні кода, а не у самому SQL-сервері. Як я казав у першій частині, MS SQL база має крім табличок ще приблизно сто специфічних об'єктів, які Entity Framework ніяк їх не підтримує, але має ось такий засіб хоча б зібрати дані у такий сурогат вьюшек:

Ну я вже не кажу про службовий код, безпосередньо пов'язаний з Entity Framework:

Якщо б ми користувалися звичайним Linq-to-SQL увесь вище перерахований код був би утворений автоматично, коли ми мишкою б перетягли іконку з базою на фейс сторінки DBML. Але у EF Code First нам необхідно ручками надзвичайно ретельно самому написати цей код без жодної помилки. Це й є ускладнення Code First порівняно з існуючим вже 20-30 років засобом Database First. Інші файли проєкту були б присутні, якщо б ми не користувалися Entity Framework, але б користувалися MVC-роутінгом сторінок (якщо б ми не користувалися MVC, то потрібно б були лише Global.asax та Web.config).

Далі йде декілька важливих файлів самої студії, які визначають середовище компіляції та роботи проєкту.

Ще декілька загальних файлів проєкту.

Усе це вище - лише деякі службові файли (частину яких можна б було взагалі утворити автоматично, наприклад по DatabaseFirst або по Linq-to-SQL), а частина взагалі була б непотрібна, якщо б ми не користувалися модним MVC-роутінгом. Поки що безпосередньо про код сайта ми ще не згадували взагалі. Єдиний спеціфічний для нашого проєкту файл, крім пов'язаних з EntityFramework, згаданий вище - це файл Site.CSS. I ось прийшла черга перерахувати саме файли з кодом VB та HTML нашого проєкту - у нашому проєкті 71-файл, але лише три з них безпосередньо пов'язані з нашим сайтом, якщо не рахувати визначення даних у базі та CSS-стіля.


3.10 Deploy Database to SQL server.



3.10.1 Manually Deploy.


Але пам'ятаємо про загальний сенс існування технології CodeFirst - постійне розгортання класів у базу з кожною модифікацією класів. Тому для початку спробуємо розгорнути поточну версію класів у базу за допомогою Package Console. Як бачите, класи розгорнулися у базу, навіть з необхідними тестовими даними.




3.10.2 Визначення зв'язків даних 1:1, 1:M, M:M у базі та EF CodeFirst.


Подивимося тепер на базу, у тому вигляді як це робилося останні 30 років та порівняємо з визначенням маркованих буферів даних у Code First.




Тепер теж саме, але за допомогою EF.



3.10.3 Visual Designer.


Цікаво, що якщо додати EF Power pack, та клацнути View Model, то у web.config буде додан конект до розгорнутої бази і можна буде візуально подивитися на структуру бази у студії, тобто нібито перейти у режим Model First, тобто нібито повернутися на 30 років тому у SQL Server 7, де й існувала ця діаграма взаємовідносин табл, але тепер вона незрозуміло для чого перенесена з MS SMS безпосередньо у Visual Studio. Схоже на якісь іграшки Мікрософта, з величезними зусиллями за 20 років зробили дубль існуючого софта, але працюючий у мільйон разів повільніше. Не підтримуються базові можливості MS SQL, але з'явився мапінг внутрішніх класів-об'єктів на табли бази.



Хе-хе, не знаю як вам, але мені набагато зручніше зрозуміти структуру даних у візуальному вигляді SQL, як це й робилося останні 30 років, ніж по модному індуському EF Code First, що описує цей скрін.



3.10.4 Unexpected Deploy.


До запуску сайту нам потрібно спочатку необхідно визначити у конфігі ConnectionString до бази.



І тут з'ясувався один цікавий пунктик, спочатку я розгортав базу мастером студії 3.8.1 Manually Deploy. (тобто командою update-database -verbose) у базу .\SQLExpress, але потім, коли клацнув у Solution Explorer VS2017 пункт Entity Framework -> View Entity Data Model (read only), тобто 3.10.2 Visual Designer., мастер студії модифікував мій Web.config та додав туди Connection String, який дивився вже зовсім у інший сервер, у якому теж розгорнулася база (LocalDb)\MSSQLLocalDB. Тобто я спочатку навіть не зрозумів, що моя база розгорнулася у два місця, у одне місце я розгорнув її ручками, у друге місце її розгорнув Visual Designer з пакету EF Power Pack. Треба бути обережним з цим, бо мені здавалося, що метадані проєкта описують місце розгортання бази, але це не так. EF у метаданих проєкта не має посилання на місце розгортання бази, якщо наприклад ви доставили собі на кампутер ще один instance SQL-серверу, база несподівано може розгорнутися у ньому, навіть коли ви цього не очікуєте.





3.10.5 Save relation 1:M, M:M to DB..


Зберігання зв'язків у базу я покажу на прикладі іншого сайту, я його тільки почав розробляти, коли писав ці нотатки по ContosoUniversity, й тому він ще дуже простий та у ньому все більш зрозуміло. Я зробив там ось таку модель, тобто кожне резюме має декілька скілів, які угруповані у мета-скіл групи. Цей проєкт я робив як модел-first, це більш зручно для мене.

.

EDMX-designer зробив мені з цієї екстремально простої моделі ось такі класи.


   1:  Partial Public Class [Resume]
   2:      Public Property Id As System.Guid
   3:      Public Property Title As String
   4:      Public Property PdfFileName As String
   5:      Public Property InputAvaFile As String
   6:      Public Property Description As String
   7:      Public Property PublicContacts As String
   8:      Public Property AdminContact As String
   9:      Public Property Login As String
  10:      Public Property Pass As String
  11:      Public Property CrDate As Date
  12:   
  13:      Public Overridable Property Skills As ICollection(Of Skill) = New HashSet(Of Skill)
  14:   
  15:  End Class

   1:  Partial Public Class Skill
   2:      Public Property Id As Integer
   3:      Public Property SkillName As String
   4:      Public Property SkillTypeId As Short
   5:   
   6:      Public Overridable Property SkillType As SkillType
   7:      Public Overridable Property Resumes As ICollection(Of [Resume]) = New HashSet(Of [Resume])
   8:   
   9:  End Class

   1:  Partial Public Class SkillType
   2:      Public Property Id As Short
   3:      Public Property SkillTypeName As String
   4:   
   5:      Public Overridable Property Skills As ICollection(Of Skill) = New HashSet(Of Skill)
   6:   
   7:  End Class

При розгортанні у базу цієї екстремально простої моделі утворилася додаткова табла ResumeSkill, яка зберігає відносини M:M і та до записів якої немає безпосереднього доступа з коду, лише тільки через роботу з колекціями ось так:



  ...
  52:                      Dim NewResume As New [Resume] With {
  53:                          .Id = Guid.NewGuid,
  54:                          .CrDate = Now,
  55:                          .Title = Model.Title,
  56:                          .Description = Model.ShortDescription,
  57:                          .PublicContacts = Model.PublicContacts
  58:                       }
  ...
  73:                      Dim DB1 As New ProgrammerExpertContainer
  74:                      For Each One In Model.Skills
  75:                          Dim SkillRow = DB1.Skills.FirstOrDefault(Function(x) x.SkillName = One)
  76:                          NewResume.Skills.Add(SkillRow)
  77:                      Next
  78:                      DB1.Resumes.Add(NewResume)
  79:                      DB1.SaveChangesAsync()

Таким чином, коли ми не маємо з коду доступу до табли зв'язків M:M ResumeSkill, яка зберігається у базі та виконує тут головну роль, утворюється ілюзія, що ми нібито працюємо лише з об'єктами та колекціями, а не с плоскими реляційними даними. Тобто EF це такий слой коду, який нам забезпечує доступ до об'ектів на верхньому рівні, а сам цей код мапірує усі об'екти на реляційні базу (та навіть на MongoDB) так, що ми цього не бачимо взагалі.




3.11 Ще раз про Scaffold template.

Але повернемось до проєкту ContosoUniversity. Я вже казав раніше пару слів про Scaffold template, давайте ще раз повернемося. Якщо ми подивимося на схему даних, то зрозуміємо, що Student, Instructor, Course, Department - це чисті довідники (справочники), тобто вони потребують лише найпростіших CRUD-операцій. Отже ми можемо спробувати зробити чотири скафорд-шаблона на цих таблах. Почнемо, наприклад з student та подивимося, що утворив мастер Scaffold:




3.11.1 Collection.Generic.List (of T), IQueryable vs IEnumerable


Хм, чудово. Все вибрали правильно, але скафолд утворив якісь дурниці замість коду. З одного боку, у вьюху передається List (of T), з іншого боку вьюха очікує IEnumerable. Більш того, ми попросили передати таблу Student, а Скафолд-мастер утворив передачу табли People. Все, як і завжди у мікрософті, навіть на найпростіших тестових прикладах ніхто цей код ніколи не перевіряв, незважаючи на тисячу мікрософтовских книг про необхідність тестів написаного програмного кода.


  11:  ....
  12:   
  13:  Namespace Controllers
  14:      Public Class StudentsController
  15:          Inherits System.Web.Mvc.Controller
  16:   
  17:          Private db As New SchoolContext
  18:   
  19:          ' GET: Students
  20:          Async Function Index() As Task(Of ActionResult)
  21:              Return View(Await People.ToListAsynk())
  22:          End Function
  23:   
  24:  ....

   1:  @ModelType IEnumerable(Of CU_VB_3.Models.Student)
   2:  @Code
   3:  ViewData("Title") = "Index"
   4:  Layout = "~/Views/Shared/_Layout.vbhtml"
   5:  End Code
   6:  ....

Ось тут повний код контролера Student, та п'ять вьюшек, що утворив мастер Scaffold (після мого приведення до IEnumerable). До речі, вьюшки непогані та корисні, у випадках настільки простих форм це реально полегшує працю програміста, погано що у реальних проєктах таких форм мабуть меньше 10%.



Додамо у плашку Layout виклики редакторів цих довідників та спробуємо викликати цю сторінку.



Так і є, працювати не буде. Тому передамо у вьюху правильну таблу та виконаємо коректне перетворення System.Collection.Generic.List (of T) до IEnumerable:


  12:   
  13:  Namespace Controllers
  14:      Public Class StudentsController
  15:          Inherits System.Web.Mvc.Controller
  16:   
  17:          Private db As New SchoolContext
  18:   
  19:          ' GET: Students
  20:          Async Function Index() As Task(Of ActionResult)
  21:              Return View(Await Task.FromResult(db.Students.ToList()))
  22:          End Function
  23   ....

Так працює все добре, зрозуміло що довідники видаляти не можна, поки на них є референси у інших таблах.



І щоб закінчити тему про засоби передачі регулярних даних, зробимо ще одну помітку, зверніть увагу що прості LINQ-запроси взагалі не видають ніяких реквестів у базу, це лише визначення інтерфейсу IQueryable(of T) - "Queryable take expression trees, which - rather than regular IL, get compiled to an object model", безпосередньо звернення у базу виконуються лише при викликанні Where(), ToList(), Select(), тобто лише тоді сформовані компілятором статичні визначення Expression Trees почнуть виконуватися. Дебагер Студії вміє виконувати визначення IQueryable(of T), але вони виконуються один раз, потім настає стан EOF, кінця даних та повторити прохід по даних IQueryable неможливо. Звичайно, що коли дані перетворені у List - то по буферу з даними System.Collection.Generic.List (of T) можна рухатися скільки завгодно у будь якому напрямку, навіть без звернення у базу.


3.11.2 Атрибут Bind


Давайте подивимось далі на код, що утворив Scaffold. Як я казав раніше - код контролера насичений атрибутами, я перерахував сотню атрибутів ось тут 3.6 Close look to controller code Але зараз я звернув увагу на стрічку :



Взагалі з моменту утворення ASP.NET ми користувалися різноманітними парсерами постбеков, наприклад ось тут лежить опис одного з них - Пакетный загрузчик файлов на сайт, ось тут ще один Перемикач мови для сайту, зазвичай я прибіндюю параметри ось так Мой первый сайт на MVC 3 Razor, Общий шаблон простейшего приложения на ASP NET MVC, Формування безопасного токена для зміни паролю, Додаток про загальний код у проектах MVC.. Я так робив у достатньо великих своїх комерційних MVC-проєктах, наприклад Проекты нового Вотпуска, Электронный магазин запчастей SHEL-AUTO.RU на web-сервисах EMEX.RU, Social network for Canada with printed version of calendar to communities та інших. Але я ніколи ще не використовував аттрібут BIND. Тому я звернув на нього увагу як на ще один засіб распарсити постбек параметри колекції Request.FormCollection.


3.11.3 Антиспам валідатор Validate Anti Forgery Code


Ще один цікавий атрибут з сотні головних атрибутів MVC та NET, якого здається не було у попередніх версіях MVC - ValidateAntiForgeryToken.



Цей атрибут додає Hidden field кожному реквесту, без якого реквест на сайт не буде прийматися ковеером IIS, який обробляє усі реквести та передає їх формам сайту. Якщо ми уважно роздимивося сформовану HTML-сторінку, то побачимо це Hidden field у кожному тегі <form.


  39:  <form action="/Students/Edit/1" method="post">
  41:          <input name="__RequestVerificationToken" type="hidden" value="Rg2XiX--2-Ylkgi3W-b7Oq0p5ZaRXAi5X_N4feQs24Y5hk0L4HjzT_gW2q_YJKbryT_XDqQpRn_Y0yxHuJgE-HVdhgj_igKidP4Kmw_bYpY1" />    
  42:   




3.11.4 jquery.validate.unobtrusive.js


Ще можна звернути увагу на сервіс unobtrusive, який дозволяє формувати більш приємний код валідації нестандартними тегами HTML.


  52:                  <input class="form-control text-box single-line" data-val="true" data-val-length="The field Last Name must be a string with a maximum length of 50." data-val-length-max="50" data-val-required="The Last Name field is required." id="LastName" name="LastName" type="text" value="Alexander1" />

Щоб unobtrusive-валідація запрацювала, треба додати необхідні параметри у конфіг та, зрозуміло, власне скрипти на сторінку (за допомогою Nuget наприклад) .




3.12. MVC paging, sorting, filtering in EF level.


Звичайна, та найбільш поширена технологія - це піднімати з SQL лише ті дані, які потрібні, сортувати та фільтрувати їх безпосередньо у SQL-Сервері, ось так SPA-page на Classic ASP.NET та jQuery., Делаем SEO SiteMap., Мой первый фото-слайдер на Flex 4, Collection and processing information about your system. - це так званий Connected Data Mode. Він автоматично вирішує проблеми будь-яких блокировок на рівні SQL та є найшвидшим. Я саме його застосую у більшості своїх проєктів, бо вважаю його найкращім. Будь які пейджери MVC-сторінок я роблю точно так же, й не тільки для MVC, але й для класичного ASP.NET - Пейджер для DataList.

Але існує протилежний погляд на речі - вичитати усю базу у пам'ять і більш не чіпати її. Це так званий Disconected Mode. У EF принципово можливо працювати і так і інакше, але якщо ви працюєте у disconnected mode то це складніше, сайт може легко заблокуватися, потрібно також розуміти що таки Optimistic/Pessimistic mode, потрібно розуміти які методи Бейсіка виробляють запроси в SQL, а які лише працюють з буферами пам'яті, також потрібно розуміти стан записів у буферах.

Тому мені було дуже цікаво, що саме пропонують мікрософтовськи індуси. А вони зробили ось такий клас:


   1:  Imports System.Data.Entity
   2:  Imports System.Threading.Tasks
   3:   
   4:  Public Class PaginatedList(Of T)
   5:      Inherits List(Of T)
   6:   
   7:      Public Property PageIndex As Integer
   8:      Public Property TotalPages As Integer
   9:   
  10:      Public Sub New(ByVal items As List(Of T), ByVal count As Integer, ByVal pageIndex_ As Integer, ByVal pageSize As Integer)
  11:          PageIndex = pageIndex_
  12:          TotalPages = CInt(Math.Ceiling(count / CDbl(pageSize)))
  13:          Me.AddRange(items)
  14:      End Sub
  15:   
  16:      Public ReadOnly Property HasPreviousPage As Boolean
  17:          Get
  18:              Return (PageIndex > 1)
  19:          End Get
  20:      End Property
  21:   
  22:      Public ReadOnly Property HasNextPage As Boolean
  23:          Get
  24:              Return (PageIndex < TotalPages)
  25:          End Get
  26:      End Property
  27:   
  28:      Public Shared Async Function CreateAsync(ByVal source As IQueryable(Of T), ByVal pageIndex As Integer, ByVal pageSize As Integer) As Task(Of PaginatedList(Of T))
  29:          Dim count = Await source.CountAsync()
  30:          Dim items = Await source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync()
  31:          Return New PaginatedList(Of T)(items, count, pageIndex, pageSize)
  32:      End Function
  33:  End Class

Потім зробили ось таку сторінку та ось такий контролер, у якому, нажаль у звичайному MVC (не CORE) нічого не працює, тільки у CORE, бо не вистачає методів TryUpdateModelAsync, ThenInclude, Update. Але для пейджера це не важливо.

Але те, що стосується пейджінга та фільтрації - працює чудово.



На сторінці це виглядає ось так:


   1:  @ModelType PaginatedList(Of CU_VB_3.Models.Student)
   ..  
  16:              Find by name: <input type="text" name="SearchString" value='@ViewData("currentFilter")' />
  17:              <input type="submit" value="Search" class="btn btn-default" /> |
   ..  
  27:                  @Html.ActionLink("Last Name", "Index", New With {.SortOrder = ViewData("NameSortParm")})
   ..  
  33:                  @Html.ActionLink("Enrollment Date", "Index", New With {.SortOrder = ViewData("DateSortParm")})
   ..  
  61:  @Code
  62:      Dim prevDisabled = If(Not Model.HasPreviousPage, "disabled", "")
  63:      Dim nextDisabled = If(Not Model.HasNextPage, "disabled", "")
  64:  End Code
   ..  
  67:  @Html.ActionLink("Previous", "Index", New With {.SortOrder = ViewData("CurrentSort"), .Page = (Model.PageIndex - 1), .currentFilter = ViewData("CurrentFilter")}, New With {.class = "btn btn-default " & prevDisabled})
  68:  @Html.ActionLink("Next", "Index", New With {.SortOrder = ViewData("CurrentSort"), .Page = (Model.PageIndex + 1), .currentFilter = ViewData("CurrentFilter")}, New With {.class = "btn btn-default " & nextDisabled})

А у контролері ось так:


 191:          Public Async Function Index(ByVal sortOrder As String, ByVal currentFilter As String, ByVal searchString As String, ByVal page As Integer?) As Task(Of ActionResult)
 192:              ViewData("CurrentSort") = sortOrder
 193:              ViewData("NameSortParm") = If(String.IsNullOrEmpty(sortOrder), "name_desc", "")
 194:              ViewData("DateSortParm") = If(sortOrder = "Date", "date_desc", "Date")
 195:   
 196:              If searchString IsNot Nothing Then
 197:                  page = 1
 198:              Else
 199:                  searchString = currentFilter
 200:              End If
 201:   
 202:              ViewData("CurrentFilter") = searchString
 203:              Dim students = From s In _context.Students Select s
 204:   
 205:              If Not String.IsNullOrEmpty(searchString) Then
 206:                  students = students.Where(Function(s) s.LastName.Contains(searchString) OrElse s.FirstMidName.Contains(searchString))
 207:              End If
 208:   
 209:              Select Case sortOrder
 210:                  Case "name_desc"
 211:                      students = students.OrderByDescending(Function(s) s.LastName)
 212:                  Case "Date"
 213:                      students = students.OrderBy(Function(s) s.EnrollmentDate)
 214:                  Case "date_desc"
 215:                      students = students.OrderByDescending(Function(s) s.EnrollmentDate)
 216:                  Case Else
 217:                      students = students.OrderBy(Function(s) s.LastName)
 218:              End Select
 219:   
 220:              Dim pageSize As Integer = 3
 221:              Return View(Await PaginatedList(Of Student).CreateAsync(students.AsNoTracking(), If(page, 1), pageSize))
 222:          End Function

Більш нічого цікавого я не побачив у цьому проєкті взагалі, за винятком того, що це взагалі не мій стиль програмування. Так пишуть тільки індуси. Наприклад, я обгортаю кожний метод контролерів у TRY та обробляю повідомлення про помилки. Я перевіряю кожний параметр на вході кожного важливого метода та видаю специфічні повідомлення про помилки, наприклад навіть ось такий найпростіший хандлер на 10 стрічок кода Proxy-handler for graphhopper.com, я обгортаю повідомленнями про помилки, індуси так не роблять взагалі.



Тому, хоча я спочатку мав бажання написати ще про дещо

на прикладі проєкту Contoso Univercity, з часом я зрозумів що копошитися у цих індуських помиях я не маю бажання. До того ж, цей сайт написан на NET CORE, а NET CORE має інші теги замість звичайних MVC-хелперів, а саме (asp-controller, asp-action, asp-route-{value}, asp-route, asp-all-route-data, asp-fragment, asp-area, asp-protocol, asp-host, asp-page, asp-page-handler, cache, distributed-cache, environment, Form, Input, Textarea, Label, Validation, Select, img, partial) - та ці теги потрібно конвертувати у звичайні хелпери ручками (ніякого сервісу індуси не передбачили), це додає складнощів до звичайної конвертації Шарпа у Бейсік, бо Щарп великі та маленькі літери розуміє як різні змінні, і коли конвертер перетворює код Шарпа на Бейсік (у якому це однакові змінні), навіть після бездоганно-працюючого конвертеру потрібно кожну стрічку кода зрозуміти за індусом та уважно перевірити, а це навіть складніше, ніж зробити самому той самий код з самого початку. Тому, на жаль, наступних частин з цим Contoso University вже не буде, можливо з іншими сайтами.

Entity Framework missing FAQ (Part 4). From EF Code First class definition to WebAPI2 on VB.NET





Comments ( )
<00>  <01>  <02>  <03>  <04>  <05>  <06>  <07>  <08>  <09>  <10>  <11>  <12>  <13>  <14>  <15>  <16>  <17>  <18>  <19
Link to this page: http://www.vb-net.com/EF-missing-FAQ/index3.htm
<SITEMAP>  <MVC>  <ASP>  <NET>  <DATA>  <KIOSK>  <FLEX>  <SQL>  <NOTES>  <LINUX>  <MONO>  <FREEWARE>  <DOCS>  <ENG>  <MAIL ME>  <ABOUT ME>  < THANKS ME>