لیست APIهای جدید در NET Core 3.0.
پیشنیازهای کار با EF Core Migrations
در قسمت قبل در حین بررسی «برپایی تنظیمات اولیهی EF Core 1.0 در یک برنامهی ASP.NET Core 1.0»، چهار مدخل جدید را به فایل project.json برنامه اضافه کردیم. مدخل جدید Microsoft.EntityFrameworkCore.Tools که به قسمت tools آن اضافه شد، پیشنیاز اصلی کار با EF Core Migrations است.
بررسی ابزارهای خط فرمان EF Core و تشکیل ساختار بانک اطلاعاتی بر اساس کلاسهای برنامه
پس از تکمیل پیشنیازهای کار با EF Core، از طریق خط فرمان به پوشهی جاری پروژه وارد شده و دستور dotnet ef را صادر کنید.
یک نکته: در ویندوز اگر در پوشهای، کلید shift را نگه دارید و در آن پوشه کلیک راست کنید، در منوی باز شده، گزینهی جدیدی را به نام Open command window here مشاهده خواهید کرد که میتواند به سرعت خط فرمان را از پوشهی جاری شروع کند.
خروجی صدور فرمان dotnet ef را در ذیل مشاهده میکنید:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef _/\__ ---==/ \\ ___ ___ |. \|\ | __|| __| | ) \\\ | _| | _| \_/ | //|\\ |___||_| / \\\/\\ Entity Framework .NET Core CLI Commands 1.0.0-preview2-21431 Usage: dotnet ef [options] [command] Options: -h|--help Show help information -v|--verbose Enable verbose output --version Show version information --assembly <ASSEMBLY> The assembly file to load. --startup-assembly <ASSEMBLY> The assembly file containing the startup class. --data-dir <DIR> The folder used as the data directory (defaults to current working directory). --project-dir <DIR> The folder used as the project directory (defaults to current working directory). --content-root-path <DIR> The folder used as the content root path for the application (defaults to application base directory). --root-namespace <NAMESPACE> The root namespace of the target project (defaults to the project assembly name). Commands: database Commands to manage your database dbcontext Commands to manage your DbContext types migrations Commands to manage your migrations Use "dotnet ef [command] --help" for more information about a command.
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations add InitialDatabase
نام دلخواه InitialDatabase را در انتهای نام فایل 13950526050417_InitialDatabase مشاهده میکنید.
اگر قصد حذف این مرحله را داشته باشیم، میتوان دستور dotnet ef migrations remove را مجددا صادر کرد.
فایل 13950526050417_InitialDatabase به همراه کلاسی است که در آن دو متد Up و Down قابل مشاهده هستند. متد Up نحوهی ایجاد جدول جدیدی را از کلاس Person بیان میکند و متد Down نحوهی Drop این جدول را پیاده سازی کردهاست.
فایل ApplicationDbContextModelSnapshot.cs دارای کلاسی است که خلاصهای از تعاریف موجودیتهای ذکر شدهی در DB Context برنامه را به همراه دارد و تفسیر آنها را از دیدگاه EF در اینجا میتوان مشاهده کرد.
پس از مرحلهی افزودن migrations، نوبت به اعمال آن به بانک اطلاعاتی است. تا اینجا EF تنها متدهای Up و Down مربوط به ساخت و حذف ساختار جداول را ایجاد کردهاست. اما هنوز آنها را به بانک اطلاعاتی برنامه اعمال نکردهاست. برای اینکار در پوشهی جاری دستور ذیل را صادر کنید:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef database update Applying migration '13950526050417_InitialDatabase'. Done.
اکنون اگر به لیست بانکهای اطلاعاتی مراجعه کنیم، بانک اطلاعاتی جدید TestDbCore2016 را به همراه جدول متناظر کلاس Person میتوان مشاهده کرد:
در اینجا جدول دیگری به نام __EFMigrationsHistory نیز قابل مشاهدهاست که کار آن ذخیره سازی وضعیت فعلی Migrations در بانک اطلاعاتی، جهت مقایسههای آتی است. این جدول صرفا توسط ابزارهای EF استفاده میشود و نباید به صورت مستقیم تغییری در آن ایجاد کنید.
مقدار دهی اولیهی جداول بانکهای اطلاعاتی در EF Core
در همین حالت اگر کنترلر TestDB مطرح شدهی در انتهای بحث قسمت قبل را اجرا کنیم، به این استثناء خواهیم رسید:
این تصویر بدین معنا است که کار Migrations موفقیت آمیز بودهاست و اینبار امکان اتصال و کار با بانک اطلاعاتی وجود دارد، اما این جدول حاوی اطلاعات اولیهای برای نمایش نیست.
در نگارش قبلی EF Code First، امکانات Migrations به همراه یک متد Seed نیز بود که توسط آن کار مقدار دهی اولیهی جداول را میتوان انجام داد (زمانیکه جدولی ایجاد میشود، در همان هنگام، چند رکورد خاص نیز به آن اضافه شوند. برای مثال به جدول کاربران، رکورد اولین کاربر یا همان Admin اضافه شود). این متد در EF Core 1.0 وجود ندارد.
برای این منظور کلاس جدیدی را به نام ApplicationDbContextSeedData به همان پوشهی جدید Migrations اضافه کنید؛ با این محتوا:
using System.Collections.Generic; using System.Linq; using Core1RtmEmptyTest.Entities; using Microsoft.Extensions.DependencyInjection; namespace Core1RtmEmptyTest.Migrations { public static class ApplicationDbContextSeedData { public static void SeedData(this IServiceScopeFactory scopeFactory) { using (var serviceScope = scopeFactory.CreateScope()) { var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>(); if (!context.Persons.Any()) { var persons = new List<Person> { new Person { FirstName = "Admin", LastName = "User" } }; context.AddRange(persons); context.SaveChanges(); } } } } }
public void Configure(IServiceScopeFactory scopeFactory) { scopeFactory.SeedData();
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<ApplicationDbContext>(ServiceLifetime.Scoped);
- برای پیاده سازی الگوی واحد کار، اولین قدم، مشخص سازی طول عمر Db Context برنامه است. برای اینکه تنها یک Context در طول یک درخواست وهله سازی شود، نیاز است به نحو صریحی طول عمر آنرا به حالت Scoped تنظیم کرد. متد AddDbContext دارای پارامتری است که این طول عمر را دریافت میکند. بنابراین در اینجا ServiceLifetime.Scoped ذکر شدهاست. همچنین در این مثال از نمونهای که IConfigurationRoot به سازندهی کلاس ApplicationDbContext تزریق شده (نکتهی انتهای بحث قسمت قبل)، استفاده شدهاست. به همین جهت تنظیمات options آنرا ملاحظه نمیکنید.
- مرحلهی بعد نحوهی دسترسی به این سرویس ثبت شده در یک کلاس static دارای متدی الحاقی است. در اینجا دیگر دسترسی مستقیمی به تزریق وابستگیها نداریم و باید کار را با IServiceScopeFactory شروع کنیم. در اینجا میتوانیم به صورت دستی یک Scope را ایجاد کرده، سپس توسط ServiceProvider آن، به سرویس ApplicationDbContext دسترسی پیدا کنیم و در ادامه از آن به نحو متداولی استفاده نمائیم. IServiceScopeFactory جزو سرویسهای توکار ASP.NET Core است و در صورت ذکر آن به عنوان پارامتر جدیدی در متد Configure، به صورت خودکار وهله سازی شده و در اختیار ما قرار میگیرد.
- نکتهی مهمی که در اینجا بکار رفتهاست، ایجاد Scope و dispose خودکار آن توسط عبارت using است. باید دقت داشت که ایجاد Scope و تخریب آن به صورت خودکار در ابتدا و انتهای درخواستها توسط ASP.NET Core انجام میشود. اما چون شروع کار ما از متد Configure است، در اینجا خارج از Scope قرار داریم و باید مدیریت ایجاد و تخریب آنرا به صورت دستی انجام دهیم که نمونهای از آنرا در متد SeedData کلاس ApplicationDbContextSeedData ملاحظه میکنید. در اینجا Scope ایی ایجاد شدهاست. سپس دادههای اولیهی مدنظر به بانک اطلاعاتی اضافه گردیده و در آخر این Scope تخریب شدهاست.
- اگر کار ایجاد و تخریب scope، به نحوی که مشخص شدهاست انجام نگیرد، طول عمر درخواستی خارج از Scope، همواره Singleton خواهد بود. چون خارج از طول عمر درخواست جاری قرار داریم و هنوز کار به سرویس دهی درخواستها نرسیدهاست. بنابراین مدیریت Scopeها هنوز شروع نشدهاست و باید به صورت دستی انجام شود.
در این حالت اگر برنامه را اجرا کنیم، این خروجی قابل مشاهده است:
که به معنای کار کردن متد SeedData و ثبت اطلاعات اولیهای در بانک اطلاعاتی است.
اعمال تغییرات به مدلهای برنامه و به روز رسانی ساختار بانک اطلاعاتی
فرض کنید به کلاس Person قسمت قبل، خاصیت Age را هم اضافه کردهایم:
namespace Core1RtmEmptyTest.Entities { public class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public int Age { get; set; } } }
An unhandled exception occurred while processing the request. SqlException: Invalid column name 'Age'.
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations add v2 D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef database update
با اجرا این دستورات، فایل جدید 13950526073248_v2 به پوشهی Migrations اضافه میشود. این فایل حاوی نحوهی به روز رسانی بانک اطلاعاتی، بر اساس خاصیت جدید Age است. سپس با اجرای دستور dotnet ef database update، کار به روز رسانی بانک اطلاعاتی بر اساس مرحلهی v2 انجام میشود.
بنابراین هر بار که تغییری را در مدلهای خود ایجاد میکنید، یکبار باید کلاس مهاجرت آنرا ایجاد کنید و سپس آنرا به بانک اطلاعاتی اعمال نمائید.
تهیه اسکریپت تغییرات بجای اعمال تغییرات توسط ابزارهای EF
شاید علاقمند باشید که پیش از اعمال تغییرات به بانک اطلاعاتی، یک اسکریپت SQL از آن تهیه کنید (جهت مطالعه و یا اعمال دستی آن توسط خودتان). برای اینکار میتوانید دستور ذیل را در پوشهی جاری پروژه اجرا کنید:
D:\Prog\1395\Core1RtmEmptyTest\src\Core1RtmEmptyTest>dotnet ef migrations script -o v2.sql
تغییرات ساختار جدول __EFMigrationsHistory در EF Core 1.0
در EF 6.x، ساختار اطلاعات جدول نگهداری تاریخچهی تغییرات، بسیار پیچیده بود و شامل رشتهای gzip شدهی حاوی یک snapshot از کل ساختار دیتابیس در هر مرحلهی migration بود. در این نگارش، این snapshot حذف شدهاست و بجای آن فایل ApplicationDbContextModelSnapshot.cs را مشاهده میکنید (تنها یک snapshot به ازای کل context برنامه). همچنین در اینجا کاملا مشخص است که چه مراحلی به بانک اطلاعاتی اعمال شدهاند و دیگر خبری از رشتهی gzip شدهی قبلی نیست (تصویر فوق).
در شکل زیر ساختار قبلی این جدول را در EF 6.x مشاهده میکنید. در EF 6.x حتی فضای نام کلاسهای موجودیتهای برنامه هم مهم هستند و در صورت تغییر، مشکل ایجاد میشود:
مهاجرت خودکار از EF Core حذف شدهاست
در EF 6.x در کنار کلاس Db Context یک کلاس Configuration هم وجود داشت که برای مثال امکان چنین تعریفی در آن میسر هست:
public Configuration() { AutomaticMigrationsEnabled = true; }
با حذف مهاجرت خودکار:
- دیگر نیازی نیست تا model snapshots در بانک اطلاعاتی ذخیره شوند (همان ساده شدن ساختار جدول ذخیره سازی تاریخچهی مهاجرتهای فوق).
- در حالت افزودن یک مرحلهی مهاجرت، دیگر نیازی به کوئری گرفتن از بانک اطلاعاتی نخواهد بود (سرعت بیشتر).
- میتوان چندین مرحلهی مهاجرت را افزود بدون اینکه الزاما مجبور به اعمال آنها به بانک اطلاعاتی باشیم.
- کاهش کدهای مدیریت ساختار بانک اطلاعاتی.
- تیمها برای یکی کردن تغییرات خود مشکلی نخواهند داشت چون دیگر snapshot مدلها در جدول __EFMigrationsHistory ذخیره نمیشود.
بنابراین در EF Core میتوان مهاجرت v1 را اضافه کرد. سپس تغییراتی را در کدها اعمال کرد. در ادامه مهاجرت v2 را تولید کرد و در آخر کار اعمال یکجای اینها را به بانک اطلاعاتی انجام داد.
هرچند در اینجا اگر میخواهید مرحلهی اجرای دستور dotnet ef database update را حذف کنید، میتوانید از کدهای ذیل نیز استفاده نمائید:
using Core1RtmEmptyTest.Entities; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; namespace Core1RtmEmptyTest.Migrations { public static class DbInitialization { public static void Initialize(this IServiceScopeFactory scopeFactory) { using (var serviceScope = scopeFactory.CreateScope()) { var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>(); // Applies any pending migrations for the context to the database. // Will create the database if it does not already exist. context.Database.Migrate(); } } } }
کار متد Migrate، ایجاد بانک اطلاعاتی در صورت عدم وجود و سپس اعمال تمام مراحل migration ایی است که در جدول __EFMigrationsHistory ثبت نشدهاند (دقیقا همان کار دستور dotnet ef database update را انجام میدهد).
تفاوت متد Database.EnsureCreated با متد Database.Migrate
اگر به متدهای context.Database دقت کنید، یکی از آنها EnsureCreated نام دارد. این متد نیز سبب تولید بانک اطلاعاتی بر اساس ساختار Context برنامه میشود. اما هدف آن صرفا استفادهی از آن در آزمونهای واحد سریع است. از این جهت که جدول __EFMigrationsHistory را تولید نمیکند (برخلاف متد Migrate). بنابراین بجز آزمونهای واحد، در جای دیگری از آن استفاده نکنید چون به دلیل عدم تولید جدول __EFMigrationsHistory توسط آن، قابلیت استفادهی از بانک اطلاعاتی تولید شدهی توسط آن با امکانات migrations وجود ندارد. در پایان آزمون واحد نیز میتوان از متد EnsureDeleted برای حذف این بانک اطلاعاتی موقتی استفاده کرد.
در قسمت بعد، مطالب تکمیلی مهاجرتها را بررسی خواهیم کرد. برای مثال چگونه میتوان کلاسهای موجودیتها را به اسمبلیهای دیگری منتقل کرد.
Finbuckle.MultiTenant is an open source framework for multitenant support in ASP.NET Core 2.0+ applications. Check out the GitHub repository for source code and samples or to provide feedback and suggestions.
- ASP.NET MVC Core 2.0.0 on .NET Core 2.0.0
- ASP.NET Identity Core 2.0.0
- REST services clients generation with using Microsoft AutoRest
- Liquid view engine based on DotLiquid
- LibSaasHost for processing scss stylesheets in runtime
- Multi-Store support
- Multi-Language support
- Multi-Currency support
- Multi-Themes support
- Faceted search support
- SEO friendly routing
ویژگی های جدید dotNET Core 2.1
Earlier this week, Microsoft published the roadmap for .NET Core 2.1, ASP.NET Core 2.1 and EF Core 2.1, expected to be out in the first quarter of 2018. The team also talked about several new features with this new release. This release is more of a feedback-oriented release based on .NET Core 2.0 release. The.NET Core 2.0 is a huge success and already more than half a million developers are now using .NET Core 2.0. All thanks to .NET Standard 2.0 release . In this post find out about the new features of .NET Core 2.1.
@Html.AntiForgeryToken()
//Post in Ajax services.AddAntiforgery(o => o.HeaderName = "XSRF-TOKEN");
function initDataTables() { table.destroy(); table = $("#tblJs").DataTable({ processing: true, serverSide: true, filter: true, ajax: { url: '@Url.Page("yourPage","yourHandler")', beforeSend: function (xhr) { xhr.setRequestHeader("XSRF-TOKEN", $('input:hidden[name="__RequestVerificationToken"]').val()); }, type: "POST", datatype: "json" }, language: { url: "/Persian.json" }, responsive: true, select: true, columns: scheme, select: true, }); }
processing: true, serverSide: true, filter: true, ajax: { url: '@Url.Page("yourPage","yourHandler ")', beforeSend: function (xhr) { xhr.setRequestHeader("XSRF-TOKEN", $('input:hidden[name="__RequestVerificationToken"]').val()); }, type: "POST", datatype: "json" },
public class FiltersFromRequestDataTable { public string length { get; set; } public string start { get; set; } public string sortColumn { get; set; } public string sortColumnDirection { get; set; } public string sortColumnIndex { get; set; } public string draw { get; set; } public string searchValue { get; set; } public int pageSize { get; set; } public int skip { get; set; } }
public static void GetDataFromRequest(this HttpRequest Request, out FiltersFromRequestDataTable filtersFromRequest) { //TODO: Make Strings Safe String filtersFromRequest = new(); filtersFromRequest.draw = Request.Form["draw"].FirstOrDefault(); filtersFromRequest.start = Request.Form["start"].FirstOrDefault(); filtersFromRequest.length = Request.Form["length"].FirstOrDefault(); filtersFromRequest.sortColumn = Request.Form["columns[" + Request.Form["order[0][column]"].FirstOrDefault() + "][name]"].FirstOrDefault(); filtersFromRequest.sortColumnDirection = Request.Form["order[0][dir]"].FirstOrDefault(); filtersFromRequest.searchValue = Request.Form["search[value]"].FirstOrDefault(); filtersFromRequest.pageSize = filtersFromRequest.length != null ? Convert.ToInt32(filtersFromRequest.length) : 0; filtersFromRequest.skip = filtersFromRequest.start != null ? Convert.ToInt32(filtersFromRequest.start) : 0; filtersFromRequest.sortColumnIndex = Request.Form["order[0][column]"].FirstOrDefault(); filtersFromRequest.searchValue = filtersFromRequest.searchValue?.ToLower(); }
Request.GetDataFromRequest(out FiltersFromRequestDataTable filtersFromRequest);
public static PaginationDataTableResult<T> ToDataTableJs<T>(this IEnumerable<T> source, FiltersFromRequestDataTable filtersFromRequest) { int recordsTotal = source.Count(); CofingPaging(ref filtersFromRequest, recordsTotal); var result = new PaginationDataTableResult<T>() { draw = filtersFromRequest.draw, recordsFiltered = recordsTotal, recordsTotal = recordsTotal, data = source.OrderByIndex(filtersFromRequest).Skip(filtersFromRequest.skip).Take(filtersFromRequest.pageSize).ToList() }; return result; } private static void CofingPaging(ref FiltersFromRequestDataTable filtersFromRequest, int recordsTotal) { if (filtersFromRequest.pageSize == -1) { filtersFromRequest.pageSize = recordsTotal; filtersFromRequest.skip = 0; } }
private static IEnumerable<T> OrderByIndex<T>(this IEnumerable<T> source, FiltersFromRequestDataTable filtersFromRequest) { var props = typeof(T).GetProperties(); string propertyName = ""; for (int i = 0; i < props.Length; i++) { if (i.ToString() == filtersFromRequest.sortColumnIndex) propertyName = props[i].Name; } System.Reflection.PropertyInfo propByName = typeof(T).GetProperty(propertyName); if (propByName is not null) { if (filtersFromRequest.sortColumnDirection == "desc") source = source.OrderByDescending(x => propByName.GetValue(x, null)); else source = source.OrderBy(x => propByName.GetValue(x, null)); } return source; }
var result = query.ToDataTableJs(filtersFromRequest); return new JsonResult(result);
public static IEnumerable<TSource> WhereSearchValue<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate) { return source.Where(predicate); } public static bool ContainsSearchValue(this string source, string toCheck) { return source != null && toCheck != null && source.IndexOf(toCheck, StringComparison.OrdinalIgnoreCase) >= 0; }
if (!string.IsNullOrEmpty(filtersFromRequest.searchValue)) query = query.WhereSearchValue(x => x.title.ContainsSearchValue(filtersFromRequest.searchValue) || x.id.ToString().ContainsSearchValue(filtersFromRequest.searchValue)).AsQueryable();
public JsonResult OnPostList() { Request.GetDataFromRequest(out FiltersFromRequestDataTable filtersFromRequest); var query = _Repo.GetQueryable().Select(x => new VmAdminList() { title = x.Title, } ); if (!string.IsNullOrEmpty(filtersFromRequest.searchValue)) query = query.WhereSearchValue(x => x.title.ContainsSearchValue(filtersFromRequest.searchValue) || x.id.ToString().ContainsSearchValue(filtersFromRequest.searchValue)).AsQueryable(); var result = query.ToDataTableJs(filtersFromRequest); return new JsonResult(result); }
data: function (d) { d.parentId = parentID; d.StartDateTime= StartDateTime; },
if (!int.TryParse(Request.Form["parentId"].FirstOrDefault(), out int parentId)) throw new NullReferenceException();
dataSrc: function (json) { $("#count").val(json.data.length); var sum = 0; json.data.forEach(function (item) { if (!isNullOrEmpty(item.credit)) sum += parseInt(item.credit); }) $("#sum").val(separate(sum)); return json.data; }
columns: [ { data: 'name' }, { data: 'position' }, { data: 'salary' }, { data: 'office' } ]
public class JsDataTblGeneretaor<T> { public readonly DataTableSchemaResult DataTableSchemaResult = new(); public JsDataTblGeneretaor<T> CreateTableSchema() { var props = typeof(T).GetProperties(); foreach (var prop in props) { DataTableSchemaResult.SchemaResult.Add(new() { data = prop.Name, sortable = (prop.PropertyType == typeof(int)) || (prop.PropertyType == typeof(bool)) || (prop.PropertyType == typeof(DateTime)), width = "", visible = (prop.PropertyType != typeof(DateTime)) }); } return this; } public JsDataTblGeneretaor<T> CreateTableColumns() { var props = typeof(T).GetProperties(); CustomAttributeData displayAttribute; foreach (var prop in props) { string displayName = prop.Name; displayAttribute = prop.CustomAttributes.FirstOrDefault(x => x.AttributeType.Name == "DisplayAttribute"); if (displayAttribute != null) { displayName = displayAttribute.NamedArguments.FirstOrDefault().TypedValue.Value.ToString(); } DataTableSchemaResult.Colums.Add(displayName); } return this; } public JsDataTblGeneretaor<T> AddCustomSchema(string data, bool? sortable = null, bool? visible = null, string width = null, string className = null) { if (DataTableSchemaResult.SchemaResult == null || !DataTableSchemaResult.SchemaResult.Any()) return this; foreach (var item in DataTableSchemaResult.SchemaResult.Where(x => x.data == data)) { if (sortable != null) item.sortable = sortable.Value; if (visible != null) item.visible = visible.Value; if (width != null) item.width = width; if (className != null) item.className = className; } return this; } public JsDataTblGeneretaor<T> SerializeSchema() { if (DataTableSchemaResult.SchemaResult == null || !DataTableSchemaResult.SchemaResult.Any()) return this; DataTableSchemaResult.SerializedSchemaResult = JsonSerializer.Serialize(DataTableSchemaResult.SchemaResult); return this; } } public class DataTableSchema { public string data { get; set; } public bool sortable { get; set; } public string width { get; set; } public bool visible { get; set; } public string className { get; set; } } public class DataTableSchemaResult { public readonly List<DataTableSchema> SchemaResult = new(); public readonly List<string> Colums = new(); public string SerializedSchemaResult = ""; }
public void OnGet() { //Create Data Table Js Schema and Columns Dynamicly JsDataTblGeneretaor<yourVM> tblGeneretaor = new(); DataTableSchemaResult = tblGeneretaor.CreateTableColumns().CreateTableSchema().SerializeSchema().DataTableSchemaResult; }
.AddCustomSchema("yourProperty",visible:false)
var scheme = JSON.parse('@Html.Raw(Model.DataTableSchemaResult.SerializedSchemaResult)')
@foreach (var col in Model.DataTableSchemaResult.Colums) { <th>@col</th> }
You might think, as a developer, that nothing but the best is good enough as a development database. You might be mistaken. There is a lot to be said for LocalDB, but Ed Elliott argues that every edition has its pros and cons, and you need to consider Cloud-based resources, VMs and Containerised databases too. There is a whole range of alternatives and how you choose depends on the type of database you are developing, but for Ed, LocalDB gets the five-star accolade
- Improving the development experience when using popular JavaScript libraries
- Adding support for new JavaScript ECMAScript 2015 (also known as ES2015 and formerly ES6) language and web browser APIs
- Increasing your productivity in complex JavaScript code bases