<PropertyGroup> <BlazorLinkOnBuild Condition="'$(Configuration)'!='Release'">false</BlazorLinkOnBuild> </PropertyGroup>
تمرین 1 - ایجاد فرم ثبت نام
میخواهیم به برنامه، فرم ثبت نام را که حاوی سه فیلد نام کاربری، کلمهی عبور و نام است، اضافه کنیم. نام کاربری باید از نوع ایمیل باشد. بنابراین اعتبارسنجی مرتبطی نیز باید برای این فیلد تعریف شود. کلمهی عبور وارد شده باید حداقل 5 حرف باشد. همچنین تا زمانیکه اعتبارسنجی فرم تکمیل نشدهاست، باید دکمهی submit فرم، غیرفعال باقی بماند. لینک ورود به این فرم نیز باید به منوی راهبری سایت اضافه شود.
برای حل این تمرین، فایل جدید registerForm.jsx را در پوشهی components ایجاد میکنیم و سپس توسط میانبرهای imrc و cc در VSCode، ساختار ابتدایی کامپوننت RegisterForm را ایجاد کرده و سپس آنرا به صورت زیر تکمیل میکنیم:
- ابتدا در فایل app.js، پس از import ماژول آن:
import RegisterForm from "./components/registerForm";
<Route path="/register" component={RegisterForm} />
<NavLink className="nav-item nav-link" to="/register"> Register </NavLink>
import Joi from "@hapi/joi"; import React from "react"; import Form from "./common/form"; class RegisterForm extends Form { state = { data: { username: "", password: "", name: "" }, errors: {} }; schema = { username: Joi.string() .required() .email({ minDomainSegments: 2, tlds: { allow: ["com", "net"] } }) .label("Username"), password: Joi.string() .required() .min(5) .label("Password"), name: Joi.string() .required() .label("Name") }; doSubmit = () => { // Call the server console.log("Submitted"); }; render() { return ( <div> <h1>Register</h1> <form onSubmit={this.handleSubmit}> {this.renderInput("username", "Username")} {this.renderInput("password", "Password", "password")} {this.renderInput("name", "Name")} {this.renderButton("Register")} </form> </div> ); } } export default RegisterForm;
- سپس state این کامپوننت را با شیءای حاوی دو خاصیت data و error، مقدار دهی اولیه میکنیم. خواص متناظر با المانهای فرم را نیز به صورت یک شیء، به خاصیت data انتساب دادهایم.
- پس از آن، خاصیت schema تعریف شدهاست؛ تا قواعد اعتبارسنجی تک تک فیلدهای فرم را به کمک کتابخانهی Joi، مطابق نیازمندیهایی که در ابتدای تعریف این تمرین مشخص کردیم، ایجاد کند.
- در ادامه، متد doSubmit را ملاحظه میکنید. این متد پس از کلیک بر روی دکمهی Register و پس از اعتبارسنجی موفقیت آمیز فرم، به صورت خودکار فراخوانی میشود.
- در آخر، تعریف فرم ثبتنام را مشاهده میکنید که نکات آنرا در قسمت قبل، با معرفی کامپوننت Form و افزودن متدهای کمکی رندر input و button به آن، بررسی کردیم و در کل با نکات بررسی شدهی در فرم لاگینی که تا به اینجا ایجاد کردیم، تفاوتی ندارد.
تمرین 2- ایجاد فرم ثبت و یا ویرایش یک فیلم
فرم جدید ثبت و ویرایش یک فیلم، نکات بیشتری را به همراه دارد. در اینجا میخواهیم در بالای لیست نمایش فیلمها، یک دکمهی new movie را اضافه کنیم تا با کلیک بر روی آن، به فرم ثبت و ویرایش فیلمها هدایت شویم. این فرم، از فیلدهای یک عنوان متنی، انتخاب ژانر از یک drop down list، تعداد موجود (بین 1 و 100) و امتیاز (بین صفر تا 10) تشکیل شدهاست. همچنین تا زمانیکه اعتبارسنجی فرم تکمیل نشدهاست، دکمهی submit فرم باید غیرفعال باقی بماند. پس از ذخیره شدن این فیلم (در لیست درون حافظهای برنامه)، با مراجعهی به لیست فیلمها و انتخاب آن از لیست (با کلیک بر روی لینک آن)، باید مجددا به همین فرم، در حالت ویرایش این رکورد هدایت شویم. به علاوه اگر در بالای صفحه یک id اشتباه وارد شد، باید صفحهی «پیدا نشد» نمایش داده شود.
کامپوننت MovieForm و مسیریابی آنرا در قسمت 17، تعریف و اضافه کردیم. برای تعریف لینکی به آن، به کامپوننت movies مراجعه کرده و بالای متنی که تعداد کل آیتمهای موجود در بانک اطلاعاتی را نمایش میدهد، المان زیر را اضافه میکنیم:
import { Link } from "react-router-dom"; // ... <div className="col"> <Link to="/movies/new" className="btn btn-primary" style={{ marginBottom: 20 }} > New Movie </Link> <p>Showing {totalCount} movies in the database.</p>
سپس به کامپوننت src\components\movieForm.jsx که پیشتر آنرا اضافه کرده بودیم، مراجعه کرده و به صورت زیر آنرا تکمیل میکنیم:
import Joi from "@hapi/joi"; import React from "react"; import { getGenres } from "../services/fakeGenreService"; import { getMovie, saveMovie } from "../services/fakeMovieService"; import Form from "./common/form"; class MovieForm extends Form { state = { data: { title: "", genreId: "", numberInStock: "", dailyRentalRate: "" }, genres: [], errors: {} };
- سپس این کامپوننت نیز از کامپوننت Form ارث بری میکند تا به امکانات ویژهی آن دسترسی پیدا کند.
- در ادامه در خاصیت state، طبق روالی که در کامپوننت فرم درنظر گرفتهایم، دو خاصیت data و errors باید حضور داشته باشند. در خاصیت data، شیءای که نام خاصیتهای آن با فیلدهای فرم تطابق دارد، ذکر شدهاند. در اینجا برای ذخیره سازی اطلاعات انتخاب شدهی از drop down list مرتبط با ژانرهای سینمایی، از خاصیت genreId استفاده میشود؛ این تنها اطلاعاتی است که از کل آیتمهای یک drop down list نیاز داریم. آرایهی genres که آیتمهای این drop down list را مقدار دهی میکند، در روال componentDidMount، از سرویس مرتبطی دریافت و مقدار دهی خواهد شد.
در ادامهی کدهای کامپوننت MovieForm، کدهای schema اعتبارسنجی شیء data را ملاحظه میکنید:
schema = { _id: Joi.string(), title: Joi.string() .required() .label("Title"), genreId: Joi.string() .required() .label("Genre"), numberInStock: Joi.number() .required() .min(0) .max(100) .label("Number in Stock"), dailyRentalRate: Joi.number() .required() .min(0) .max(10) .label("Daily Rental Rate") };
اکنون به مرحلهی componentDidMount میرسیم:
componentDidMount() { const genres = getGenres(); this.setState({ genres }); const movieId = this.props.match.params.id; if (movieId === "new") return; const movie = getMovie(movieId); if (!movie) return this.props.history.replace("/not-found"); this.setState({ data: this.mapToViewModel(movie) }); }
- پس از آن، نحوهی دریافت پارامتر id مسیریابی رسیده را ملاحظه میکنید. این id اگر به "new" تنظیم شده بود، یعنی قرار است، اطلاعات جدیدی ثبت شوند. بنابراین متد جاری را خاتمه میدهیم (چون کار ادامهی این متد، مقدار دهی اولیهی تمام فیلدهای فرم، بر اساس اطلاعات شیء دریافت شدهی از سرویس فیلمها است). در غیراینصورت (و با مشخص بودن id)، با استفاده از این id و متد getMovie سرویس src\services\fakeMovieService.js، سعی خواهیم کرد تا اطلاعات شیء movie متناظری را دریافت کنیم. اگر خروجی این متد null بود، یعنی id وارد شده معتبر نیست. به همین جهت کاربر را به صفحهی not-found هدایت میکنیم. اگر دقت کنید در اینجا بجای متد push، از متد replace استفاده کردهایم. چون اگر از متد push استفاده میکردیم و کاربر بر روی دکمهی back مرورگر کلیک میکرد، دوباره به همین صفحه، با id غیرمعتبر قبلی وارد میشد و یک حلقهی بیپایان رخ میداد. همچنین به return ای هم که به همراه متد replace استفاده شده، دقت کنید. کار redirect به یک صفحهی دیگر، به معنای عدم اجرای کدهای پس از آن نیست. بنابراین اگر میخواهیم کار این متد با redirect، به پایان برسد، ذکر return الزامی است.
- در پایان این متد، خاصیت data موجود در state را به روز رسانی میکنیم؛ تا سبب رندر فرم، با اطلاعات شیء movie یافت شده گردد و چون ساختار شیء movie دریافت شدهی از سرویس، با ساختار data تعریف شدهی در state یکی نیست، نیاز به نگاشت این دو به هم، توسط متد سفارشی mapToViewModel زیر است:
mapToViewModel(movie) { return { _id: movie._id, title: movie.title, genreId: movie.genre._id, numberInStock: movie.numberInStock, dailyRentalRate: movie.dailyRentalRate }; }
در ادامهی کدهای کامپوننت فرم فیلمها، به متد doSubmit میرسیم:
doSubmit = () => { saveMovie(this.state.data); this.props.history.push("/movies"); };
در انتهای این کامپوننت نیز به متد رندر آن میرسیم:
render() { return ( <div> <h1>Movie Form</h1> <form onSubmit={this.handleSubmit}> {this.renderInput("title", "Title")} {this.renderSelect("genreId", "Genre", this.state.genres)} {this.renderInput("numberInStock", "Number in Stock", "number")} {this.renderInput("dailyRentalRate", "Rate")} {this.renderButton("Save")} </form> </div> ); }
برای تعریف متد جدید renderSelect به این صورت عمل میکنیم:
- ابتدا فایل جدید src\components\common\select.jsx را ایجاد کرده و سپس آنرا جهت نمایش یک drop down list، ویرایش میکنیم:
import React from "react"; const Select = ({ name, label, options, error, ...rest }) => { return ( <div className="form-group"> <label htmlFor={name}>{label}</label> <select name={name} id={name} {...rest} className="form-control"> <option value="" /> {options.map(option => ( <option key={option._id} value={option._id}> {option.name} </option> ))} </select> {error && <div className="alert alert-danger">{error}</div>} </div> ); }; export default Select;
- پس از آن به کامپوننت src\components\common\form.jsx مراجعه کرده و متد رندر آنرا اضافه میکنیم:
import Select from "./select"; // ... class Form extends Component { // ... renderSelect(name, label, options) { const { data, errors } = this.state; return ( <Select name={name} value={data[name]} label={label} options={options} onChange={this.handleChange} error={errors[name]} /> ); } }
تمرین 3- جستجوی در لیست فیلمها
میخواهیم در بالای لیست نمایش فیلمها، یک search box را قرار دهیم تا توسط آن بتوان بر اساس عنوان وارد شده، در فیلمهای موجود جستجو کرد. همچنین این جستجو قرار است کلی بوده و حتی در صورت انتخاب ژانر خاصی از منوی کنار صفحه، باید در کل اطلاعات موجود جستجو کند. به علاوه اگر کاربر ژانری را انتخاب کرد، این text box باید خالی شود.
برای اینکار ابتدا فایل جدید src\components\searchBox.jsx را ایجاد کرده و به صورت زیر آنرا تکمیل میکنیم:
import React from "react"; const SearchBox = ({ value, onChange }) => { return ( <input type="text" name="query" className="form-control my-3" placeholder="Search..." value={value} onChange={e => onChange(e.currentTarget.value)} /> ); }; export default SearchBox;
سپس به کامپوننت movies مراجعه کرده و آنرا ذیل متن نمایش تعداد رکوردها، درج میکنیم:
<p>Showing {totalCount} movies in the database.</p> <SearchBox value={searchQuery} onChange={this.handleSearch} />
import SearchBox from "./searchBox"; //... class Movies extends Component { state = { //... selectedGenre: {}, searchQuery: "" }; handleSearch = query => { this.setState({ searchQuery: query, selectedGenre: null, currentPage: 1 }); }; handleGenreSelect = genre => { console.log("handleGenreSelect", genre); this.setState({ selectedGenre: genre, searchQuery: "", currentPage: 1 }); };
متد handleGenreSelect را نیز اندکی تغییر دادهایم تا اگر گروهی انتخاب شد، مقدار searchQuery را خالی کند. اگر در اینجا searchQuery را به نال تنظیم میکردیم، controlled component جعبهی جستجو، تبدیل به کامپوننت کنترل نشدهای میشد و در این حالت، React، اخطار تبدیل بین این دو را صادر میکرد.
در آخر، ابتدای متد getPageData هم جهت اعمال searchQuery، به صورت زیر تغییر میکند:
getPagedData() { const { pageSize, currentPage, selectedGenre, movies: allMovies, sortColumn, searchQuery } = this.state; let filteredMovies = allMovies; if (searchQuery) { filteredMovies = allMovies.filter(m => m.title.toLowerCase().startsWith(searchQuery.toLowerCase()) ); } else if (selectedGenre && selectedGenre._id) { filteredMovies = allMovies.filter(m => m.genre._id === selectedGenre._id); }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-21.zip
فرض کنید تعیین اعتبار یکی از فیلدهای فرم نیاز به انجام محاسباتی در سمت سرور دارد و اینکار را میخواهیم با استفاده از jQuery Ajax انجام دهیم. مشکلی که در اینجا وجود دارد، این است که A در Ajax به معنای asynchronous است. یعنی زمانیکه کاربر دکمه submit را فشرد، دیگر برنامه منتظر این نخواهد شد که پاسخ کامل دریافت شود ، سایر پردازشها صورت گیرد و سپس فرم را به سرور ارسال نماید (شبیه به ایجاد یک ترد جدید در برنامههای ویندوزی). مثال زیر را در نظر بگیرید:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="TestCustomValidation.aspx.cs"
Inherits="TestJQueryAjax.TestCustomValodation" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<script src="js/jquery.js" type="text/javascript"></script>
<script type="text/javascript">
function validate() {
var number1 = $("#<%=txtNumber1.ClientID %>").val();
var number2 = $("#<%=txtNumber2.ClientID %>").val();
var result = false;
$.ajax({
type: "POST",
url: 'AjaxSrv.asmx/ValidateIt',
data: '{"number1":' + number1 + ',"number2":' + number2 + '}',
contentType: "application/json; charset=utf-8",
dataType: "json",
success:
function(msg) {
if (msg.d) {
result = true;
alert('بسیار خوب');
}
else {
result = false;
alert('دوباره سعی کنید');
}
},
error:
function(XMLHttpRequest, textStatus, errorThrown) {
result = false;
alert("خطایی رخ داده است");
}
});
//debugger;
return result;
}
</script>
</head>
<body>
<form id="form1" runat="server">
<div>
number 1 :
<asp:TextBox runat="server" ID="txtNumber1" />
<br />
number 2 :
<asp:TextBox runat="server" ID="txtNumber2" />
<br />
<asp:Button ID="btnSubmit" Text="Submit" UseSubmitBehavior="false" runat="server"
OnClientClick="if(!validate()){ return false;}" OnClick="btnSubmitClick" />
</div>
</form>
</body>
</html>
این مثال یک نوع اعتبار سنجی سفارشی را در حین submit با استفاده از وب سرویس زیر انجام میدهد (حاصلضرب دو عدد دریافتی را بررسی میکند که باید مساوی 10 باشند. البته هدف از این مثال ساده، آشنایی با نحوهی انجام این نوع عملیات است که میتواند شامل کار با دیتابیس و غیره هم باشد. و گرنه بدیهی است این بررسی را با دو سطر کد جاوا اسکریپتی نیز میشد انجام داد):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Services;
using System.Web.Script.Services;
namespace TestJQueryAjax
{
/// <summary>
/// Summary description for AjaxSrv
/// </summary>
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
[ScriptService]
public class AjaxSrv : System.Web.Services.WebService
{
[WebMethod(EnableSession = true)]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public bool ValidateIt(int number1, int number2)
{
return number1 * number2 == 10;
}
}
}
راه حل چیست؟
راه حلهای فضایی بسیاری را در وب در این مورد میتوان پیدا کرد؛ اما راه حل استاندارد آن در این حالت ویژه، استفاده از Ajax در حالت غیرهمزمان است. یعنی این فریم ورک Ajax را وادار کنیم که تا پایان عملیات مورد نظر، منتظر بماند و سپس فرم را ارسال کند. برای این منظور تنها کافی است یک سطر زیر را پیش از فراخوانی تابع Ajax ، اضافه و فراخوانی نمائیم:
$.ajaxSetup({async: false}) ;
UseSubmitBehavior دکمه ما را به شکل زیر رندر میکند (دکمه به یک button معمولی (بجای حالت submit) تبدیل شده و سپس یک doPostBack را اضافه خواهد کرد):
<input id="btnSubmit" type="button" onclick="if(!validate()){ return false;};__doPostBack('btnSubmit','')" value="Submit" name="btnSubmit"/>
نگهداری سرور RavenDB
ایجاد و حذف بانکهای اطلاعاتی جدید
برای این منظور به آدرس http://localhost:8080 مراجعه و از طریق کنسول مدیریتی تحت وب RavenDB بر روی دکمه New database کلیک کنید.
در صفحه باز شده میتوان نام دیتابیس را مشخص کرد و همچنین در صورت نیاز افزونههایی مانند فشرده سازی یا رمزنگاری اطلاعات را نیز فعال نمود.
پس از ایجاد دیتابیس، برای حذف آن، بر روی نام دیتابیس کلیک راست کرده و گزینه Delete را انتخاب کنید
روش دیگر حذف اطلاعات یک بانک اطلاعاتی، مراجعه به سندهای آن و سپس کلیک راست بر روی گروهی از آنها برای حذف میباشد:
و یا سرور RavenDB را خاموش یا stop کنید. سپس به پوشه Database کنار فایل Raven.Server.exe مراجعه کرده، بانک اطلاعاتی خود را یافته و سپس کل پوشه آنرا Delete کنید.
سؤال: چگونه با دیتابیسهای ایجاد شده کار کنیم؟
تاکنون تمام مثالهای برنامه با بانک اطلاعاتی پیش فرض RavenDB کار کردند (چیزی شبیه به master database در اس کیوال سرور) و هیچگاه ابتدا یک دیتابیس جدید و مستقل را برای انجام آزمایشات خود، ایجاد نکردیم. بدیهی است این روش برای محیطهای کاری توصیه نمیشود.
برای نمونه در اینجا به System database این مجموعه وارد شدهایم که تعریف جزئیات بانک اطلاعاتی جدید ایجاد شده را در خود دارد.
جهت استفاده از بانک اطلاعاتی جدید ایجاد شده در کدهای خود، فقط کافی است خاصیت DefaultDatabase یک DocumentStore مقدار دهی شود:
using (var store = new DocumentStore { Url = "http://localhost:8080", DefaultDatabase = "Test2" }.Initialize()) { //... }
تهیه پشتیبان از بانکهای اطلاعاتی و بازیابی آنها
ابتدا نیاز است تمام بستههای مورد نیاز را یکجا از آدرس http://ravendb.net/download تهیه کنید. سپس به پوشه backup آن مراجعه کرده و از فایل اجرایی Raven.Backup.exe آن میتوان جهت تهیه پشتیبان از بانک اطلاعاتی خاصی استفاده نمود. لازم به ذکر است که این برنامه باید با سطح دسترسی ادمین اجرا شود.
Raven.Backup.exe --url==http://localhost:8080 --dest=d:\backup
برای بازیابی و Restore یک بانک اطلاعاتی ابتدا دستور Raven.Server.exe /help را صادر کنید تا کلیه سوئیچهای این برنامه مشخص شوند. یکی از آنها Restore نام دارد که پارامترهای dest و src را دریافت میکند (کجا بازیابی شود و از کجا اطلاعات را بخواند).
همچنین بجای backup و restore، امکان export و import نیز وجود دارند و برای انجام آن از برنامه Raven.Smuggler.exe که کنار Raven.Server.exe قرار دارد، میتوان استفاده کرد.
برای تهیه خروجی (که در حقیقت تهیه یک dump فشرده شده از اسناد JSON موجود است):
Raven.Smuggler.exe out http://localhost:8080/ dump.raven
Raven.Smuggler.exe in http://localhost:8080/ dump.raven
- Convert.Net
- Regular Expression Tester : که جهت تست Regexهای نوشته شده استفاده میشود.
- Encoding & Decoding : جهت تبدیل انواع رشتههای Encoded ویا Decoded به یکدیگر استفاده میشود و از html - url - EScape-js - Base64 - string و ... پشتیبانی میکند.
- Encryption & Decryption : جهت Encrypt و Decrypt انواع رشته استفاده میشود که از انکریپتورهای معروفی همچون AES - Rijndael - DES - SHA - TripleDES پشتیبانی میکند.
- Language Translation : یک دیکشنری Multi Language آنلاین در اختیار شما برای ترجمه متون قرار میدهد.
- C# & VB.Net Convertor : برای تبدیل کدهای C# به Vb و برعکس استفاده میشود و طبق تست هایی که روش به شخصه انجام دادم در اکثر موارد بدون خطا تا حدود 90 درصد تبدیلات رو به صورت موفق انجام میده ولی مانند سایر Convertorها با Lambda Expression کمی مشکل دارد.
- Xml & Json Browser : برای مشاهده و تبدیل Xml به Json و برعکس بسیار مفید است ..
- Linq Tester : برای تست کوئریهای Linq استفاده میشود . (برای استفاده از این امکان باید Roslyne روی سیستم شما نصب باشد)
- Database.Net
SQL Server 2000/2005/2008/2012/2014/Express/LocalDB SQL Server Compact 3.1/3.5/4.0 (*.sdf;*.*) SQL Azure 10/11 MS Access 97/2000/2002/2003 (*.mdb;*.mde;*.*), 2007/2010/2013 (*.accdb;*.accde;*.*) MS Excel 97/2000/2002/2003(*.xls;*.*), 2007/2010/2013 (*.xlsx;*.xlsm;*.xlsb;*.*) Firebird SuperServer/Embedded 1.5/2.0/2.1/2.5 (*.gdb;*.fdb;*.*) SQLite 3.x (*.db;*.db3;*.sqlite;*.*) MySQL 5.x, MariaDB 5.x/10.x PostgreSQL 8.x/9.x Oracle 10g/11g/12c IBM DB2 9.x/10.x IBM Informix 11.x/12.x Sybase ASE 15.x dBASE IV (*.dbf) Visual FoxPro (*.dbc) Data Sources (OleDB)(*.udl;*.*) ODBC DSN (Data Source Name)(*.dsn;*.*) OData (Open Data Protocol) v1/v2/v3/v4
- Resource.Net
سایر نرم افزارهای این تیم هم مانند نرم افزارهای معرفی شده بین کاربران محبوبیت زیادی کسب کرده اند که میتوانید برای کسب اطلاعات بیشتر و دانلود این نرم افزارها به وب سایت این تیم مراجعه فرمائید.
Unobtrusive Ajax چیست؟
در حالت معمولی، با استفاده از متد ajax جیکوئری، کار ارسال غیرهمزمان اطلاعات، به سمت سرور صورت میگیرد. چون در این روش کدهای جیکوئری داخل صفحات برنامههای ما قرار میگیرند، به این روش، «روش چسبنده» میگویند. اما با استفاده از افزونهی «jquery.unobtrusive-ajax.min.js» مایکروسافت، میتوان این کدهای چسبنده را تبدیل به کدهای غیرچسنبده یا Unobtrusive کرد. در این حالت، پارامترهای متد ajax، به صورت ویژگیها (attributes) به شکل data-ajax به المانهای مختلف صفحه اضافه میشوند و به این ترتیب، افزونهی یاد شده به صورت خودکار با یافتن مقادیر ویژگیهای data-ajax، این المانها را تبدیل به المانهای ایجکسی میکند. در این حالت به کدهایی تمیزتر و عاری از متدهای چسبندهی ajax قرار گرفتهی در داخل صفحات وب خواهیم رسید.
روش طراحی Unobtrusive را در کتابخانههای معروفی مانند بوت استرپ هم میتوان مشاهده کرد.
پیشنیازهای فعال سازی Unobtrusive Ajax در ASP.NET Core 1.0
توزیع افزونهی «jquery.unobtrusive-ajax.min.js» مایکروسافت، از طریق bower صورت میگیرد که پیشتر در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 14 - فعال سازی اعتبارسنجی ورودیهای کاربران» با آن آشنا شدیم. در اینجا نیز برای دریافت آن، تنها کافی است فایل bower.json را به نحو ذیل تکمیل کرد:
{ "name": "asp.net", "private": true, "dependencies": { "bootstrap": "3.3.6", "jquery": "2.2.0", "jquery-validation": "1.14.0", "jquery-validation-unobtrusive": "3.2.6", "jquery-ajax-unobtrusive": "3.2.4" } }
[ { "outputFileName": "wwwroot/css/site.min.css", "inputFiles": [ "bower_components/bootstrap/dist/css/bootstrap.min.css", "content/site.css" ] }, { "outputFileName": "wwwroot/js/site.min.js", "inputFiles": [ "bower_components/jquery/dist/jquery.min.js", "bower_components/jquery-validation/dist/jquery.validate.min.js", "bower_components/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js", "bower_components/jquery-ajax-unobtrusive/jquery.unobtrusive-ajax.min.js", "bower_components/bootstrap/dist/js/bootstrap.min.js" ], "minify": { "enabled": true, "renameLocals": true }, "sourceMap": false } ]
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>@ViewBag.Title - My ASP.NET Application</title> <link href="~/css/site.min.css" rel="stylesheet" /> </head> <body> <div> <div> @RenderBody() </div> </div> <script src="~/js/site.min.js" type="text/javascript" asp-append-version="true"></script> @RenderSection("Scripts", required: false) </body> </html>
استفاده از معادلهای واقعی Unobtrusive Ajax در ASP.NET Core 1.0
واقعیت این است که HTML Helper ایجکسی حذف شدهی از ASP.NET Core 1.0، کاری بجز افزودن ویژگیهای data-ajax را که توسط افزونهی jquery.unobtrusive-ajax.min.js پردازش میشوند، انجام نمیدهد و این افزونه مستقل است از مباحث سمت سرور و به نگارش خاصی از ASP.NET گره نخورده است. بنابراین در اینجا تنها کاری را که باید انجام داد، استفاده از همان ویژگیهای اصلی است که این افزونه قادر به شناسایی آنها است.
خلاصهی آنها را جهت انتقال کدهای قدیمی و یا تهیهی کدهای جدید، در جدول ذیل میتوانید مشاهده کنید:
HTML attribute | AjaxOptions |
data-ajax-confirm | Confirm |
data-ajax-method | HttpMethod |
data-ajax-mode | InsertionMode |
data-ajax-loading-duration | LoadingElementDuration |
data-ajax-loading | LoadingElementId |
data-ajax-begin | OnBegin |
data-ajax-complete | OnComplete |
data-ajax-failure | OnFailure |
data-ajax-success | OnSuccess |
data-ajax-update | UpdateTargetId |
data-ajax-url | Url |
در ASP.NET Core 1.0، به علت حذف متدهای کمکی Ajax دیگر خبری از AjaxOptions نیست. اما اگر علاقمند به انتقال کدهای قدیمی به ASP.NET Core 1.0 هستید، معادلهای اصلی این پارامترها را میتوانید در ستون HTML attribute مشاهده کنید.
چند نکته:
- اگر قصد استفادهی از این ویژگیها را دارید، باید ویژگی "data-ajax="true را نیز حتما قید کنید تا سیستم Unobtrusive Ajax فعال شود.
- ویژگی data-ajax-mode تنها با ذکر data-ajax-update (و یا همان UpdateTargetId پیشین) معنا پیدا میکند.
- ویژگی data-ajax-loading-duration نیاز به ذکر data-ajax-loading (و یا همان LoadingElementId پیشین) را دارد.
- ویژگی data-ajax-mode مقادیر before، after و replace-with را میپذیرد. اگر قید نشود، کل المان با data دریافتی جایگزین میشود.
- سه callback قابل تعریف data-ajax-complete، data-ajax-failure و data-ajax-success، یک چنین پارامترهایی را از سمت سرور در اختیار کلاینت قرار میدهند:
parameters | Callback |
xhr, status | data-ajax-complete |
data, status, xhr | data-ajax-success |
xhr, status, error | data-ajax-failure |
برای مثال میتوان ویژگی data-ajax-success را به نحو ذیل در سمت کلاینت مقدار دهی کرد:
data-ajax-success = "myJsMethod"
function myJsMethod(data, status, xhr) { }
return Json(new { param1 = 1, param2 = 2, ... });
مثالهایی از افزودن ویژگیهای data-ajax به المانهای مختلف
در حالت استفاده از Form Tag Helpers که در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 12 - معرفی Tag Helpers» بررسی شدند، یک فرم ایجکسی، چنین تعاریفی را پیدا خواهد کرد:
با این ViewModel فرضی
using System.ComponentModel.DataAnnotations; namespace Core1RtmEmptyTest.ViewModels.Account { public class RegisterViewModel { [Required] [EmailAddress] [Display(Name = "Email")] public string Email { get; set; } } }
@using Core1RtmEmptyTest.ViewModels.Account @model RegisterViewModel @{ } <form method="post" asp-controller="TestAjax" asp-action="Index" asp-route-returnurl="@ViewBag.ReturnUrl" class="form-horizontal" role="form" data-ajax="true" data-ajax-loading="#Progress" data-ajax-success="myJsMethod"> <input asp-for="Email" class="form-control" /> <span asp-validation-for="Email" class="text-danger"></span> <button type="submit">ارسال</button> <div id="Progress" style="display: none"> <img src="images/loading.gif" alt="loading..." /> </div> </form> @section scripts{ <script type="text/javascript"> function myJsMethod(data, status, xhr) { alert(data.param1); } </script> }
این View از کنترلر ذیل استفاده میکند:
using Core1RtmEmptyTest.ViewModels.Account; using Microsoft.AspNetCore.Mvc; namespace Core1RtmEmptyTest.Controllers { public class TestAjaxController : Controller { public IActionResult Index() { return View(); } [HttpPost] public IActionResult Index([FromForm]RegisterViewModel vm) { var ajax = isAjax(); if (ajax) { // it's an ajax post } if (ModelState.IsValid) { //todo: save data return Json(new { param1 = 1, param2 = 2 }); } return View(); } private bool isAjax() { return Request?.Headers != null && Request.Headers["X-Requested-With"] == "XMLHttpRequest"; } } }
و یا Action Link ایجکسی نیز به صورت خلاصه به این نحو قابل تعریف است:
<div id="EmployeeInfo"> <a asp-controller="MyController" asp-action="MyAction" data-ajax="true" data-ajax-loading="#Progress" data-ajax-method="POST" data-ajax-mode="replace" data-ajax-update="#EmployeeInfo"> Get Employee-1 info </a> <div id="Progress" style="display: none"> <img src="images/loading.gif" alt="loading..." /> </div> </div>
نکتهای در مورد اکشن متدهای ایجکسی در ASP.NET Core 1.0
همانطور که در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 18 - کار با ASP.NET Web API»، قسمت «تغییرات Model binding پیش فرض، برای پشتیبانی از ASP.NET MVC و ASP.NET Web API» نیز ذکر شد:
public IActionResult Index([FromBody] MyViewModel vm) { return View(); }
کدام روش مدیریت پروژه مناسبتر است؟
هر کسی در حوزه کاری خود تمرکز کند و کاملا تخصصی در حوزه کاری خود انجام وظیفه نماید. (مثلا متخصص Back-End در حوزه خود، متخصص Front-End در حوزه خود فعالیت کنند و کارها تفکیک شده باشند).
فواید استفاده از فریمورک Seneca
- فریمورک Seneca کدنویسی برای ایجاد درخواستها، ارسال پاسخ به درخواستهای رسیده و تبدیل دادهها را که از روتینهای هر سرویسی میتوانند باشند، سادهتر میکند.
- فریمورک Seneca با معرفی ایده Actionها و Pluginها که جلوتر توضیح داده خواهند شد، روند تبدیل کامپوننتهای یک برنامه Monolithic را به نوع سرویسی، تسهیل میکند. روندی که به صورت عادی میتواند کاری طاقت فرسا باشد و نیاز به Refactoring زیادی دارد.
- سرویسهای نوشته شده با زبانهای برنامهنویسی مختلف یا فریمورکهای متفاوت میتوانند با سرویسهای نوشته شده توسط فریمورک Seneca در ارتباط و تبادل باشند.
نصب فریمورک Seneca
{ "name": "seneca-example", "dependencies": { "seneca": "0.6.5", "express": "latest" } }
var seneca = require("seneca")();
Actionها
seneca.add({role: "accountManagement", cmd: "login"}, function(args, respond){ }); seneca.add({role: "accountManagement", cmd: "register"}, function(args, respond){ });
seneca.act({role: "accountManagement", cmd: "register", username: "parham", password: "12345!"}, function(error, response){ }); seneca.act({role: "accountManagement", cmd: "login", username: "parham", password: "12345!"}, function(error, response){ });
Pluginها
function account(options){ this.add({init: "account"}, function(pluginInfo, respond){ console.log(options.message); respond(); }) this.add({role: "accountManagement", cmd: "login"}, function(args, respond){ }); this.add({role: "accountManagement", cmd: "register"}, function(args, respond){ }); } seneca.use(account, {message: "Plugin Added"});
module.exports = function(options){ this.add({init: "account"}, function(pluginInfo, respond){ console.log(options.message); respond(); }) this.add({role: "accountManagement", cmd: "login"}, function(args, respond){ }); this.add({role: "accountManagement", cmd: "register"},function(args, respond){ }); return "account"; } seneca.use("./account.js", {message: "Plugin Added"});
Serviceها
var seneca = require("seneca")(); seneca.use("./account.js", {message: "Plugin Added"}); seneca.listen({port: "9090", pin: {role: "accountManagement"}});
seneca.client({port: "9090", pin: {role: "accountManagement"}});
در اینجا هم موارد port و pin اختیاری هستند. اگر سرویسی که میخواهیم ثبت کنیم در سرور دیگری باشد، بایستی خاصیت host و با مقدار آدرس IP سرور مورد نظر در زمان ثبت، اعمال شود. سوالی که باقی میماند این است که یک سرویس چطور Actionی از یک سرویس دیگر را فراخوانی میکند؟
در بخش Actionها آمد که برای فراخوانی یک Action از متد act، از نمونهی Seneca استفاده میکنیم. رفتار Seneca به این صورت است که ابتدا بر اساس امضای Action درخواست شده، Actionهای محلی را که به سرویس جاری اضافه شدهاند، جستجو میکند. اگر تطبیقی نیافت به سراغ Actionهای ثبت شده خارجی که دارای خاصیت pin هستند، خواهد رفت و در نهایت اگر آنجا هم موردی نیافت، برای تک تک سرویسهایی که آنها را ثبت کرده، اما خاصیت pin را ندارند، درخواستی را ارسال میکند.
برای اطلاعات بیشتر به بخش مستندات فریمورک Seneca رجوع کنید.
پیش فرضهای تعیین کلید اصلی در EF Core
به صورت پیش فرض هر خاصیتی که به نام Id و یا type name>Id> باشد، به عنوان primary key تفسیر خواهد شد؛ مانند:
public class Car { public string Id { get; set; }
public class Car { public string CarId { get; set; }
نحوهی تعیین کلید اصلی به صورت صریح
اگر یکی از دو حالت فوق برقرار نباشند، باید کلید اصلی را به نحو صریحی مشخص کرد.
الف) از طریق ویژگیها
public class Car { [Key] public string LicensePlate { get; set; }
ب) با استفاده از روش Fluent API
public class MyContext : DbContext { public DbSet<Car> Cars { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Car>() .HasKey(c => c.LicensePlate); } }
پیشنیاز کار با ویژگیها در EF Core
در اسمبلی که مدلهای موجودیتها شما قرار دارند، نیاز است وابستگی System.ComponentModel.Annotations به فایل project.json پروژه اضافه شود، تا ویژگیهایی مانند Key، شناسایی و قابل استفاده شوند:
{ "dependencies": { "System.ComponentModel.Annotations": "4.1.0" } }
تعیین کلید ترکیبی و یا Composite key
اگر نیاز است چندین خاصیت را به صورت کلید اصلی معرفی کرد که به آن composite key هم میگویند، تنها روش ممکن، استفاده از Fluent API و به صورت زیر است:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Car>() .HasKey(c => new { c.State, c.LicensePlate }); }
روشهای مختلف تولید خودکار مقادیر خواص
حالت پیش فرض تولید مقدار فیلدهای Id عددی، همان حالت خود افزایندهای است که توسط بانک اطلاعاتی کنترل میشود و یا کلید اصلی که از نوع Guid تعیین شود نیز به صورت خودکار توسط بانک اطلاعاتی در حین عملیات Add، مقدار دهی میشود (با استفاده از الگوریتم Guid سری در SQL Server).
اگر این حالات مطلوب شما نیست، حالتهای سه گانهی ذیل را میتوان استفاده کرد:
الف) هیچ دادهی خودکاری تولید نشود
برای اینکار میتوان با استفاده از ویژگی DatabaseGenerated و تنظیم مقدار آن به None، جلوی تولید خودکار کلید اصلی را گرفت. در این حالت باید هم در حین عملیات Add و هم در حین عملیات Update، مقادیر را خودتان مقدار دهی کنید:
public class Blog { [DatabaseGenerated(DatabaseGeneratedOption.None)] public int BlogId { get; set; } public string Url { get; set; } }
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .Property(b => b.BlogId) .ValueGeneratedNever(); }
ب) تولید دادههای خودکار فقط در حالت Add
حالت Add به این معنا است که دادههای خواص مشخصی، برای موجودیتهای «جدید»، به صورت خودکار تولید خواهند شد. اینکه آیا واقعا این مقادیر به صورت خودکار تولید میشوند یا خیر، صرفا وابستهاست به بانک اطلاعاتی در حال استفاده. برای مثال SQL Server برای نوعهای Guid، به صورت خودکار با کمک الگوریتم SQL Server sequential GUID، کار مقدار دهی یک چنین فیلدهایی را انجام میدهد.
این فیلدها باید توسط ویژگی DatabaseGenerated و با مقدار Identity مشخص شوند. در اینجا Identity به معنای فیلدهایی است که به صورت خودکار توسط بانک اطلاعاتی مقدار دهی میشوند و الزاما به کلید اصلی اشاره نمیکنند. برای مثال در موجودیت ذیل، خاصیت تاریخ ثبت رکورد، از نوع Identity مشخص شدهاست. به این معنا که در حین ثبت اولیهی رکورد آن، نیازی نیست تا خاصیت Inserted را مقدار دهی کرد. اما اینکه آیا SQL Server یک چنین کاری را به صورت خودکار انجام میدهد، پاسخ آن خیر است. SQL server فقط برای فیلدهای عددی و Guid ایی که با DatabaseGeneratedOption.Identity مزین شده باشند، مقادیر متناظری را به صورت خودکار تولید میکند. برای حالت DateTime نیاز است، مقدار پیش فرض فیلد را صریحا مشخص کرد که توسط ویژگیها میسر نیست و فقط fluent API از آن پشتیبانی میکند.
public class Blog { public int BlogId { get; set; } public string Url { get; set; } [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public DateTime Inserted { get; set; } }
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .Property(b => b.Inserted) .ValueGeneratedOnAdd(); }
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .Property(b => b.Inserted) .HasDefaultValueSql("getdate()"); }
یک نکته: در حالت DatabaseGeneratedOption.Identity و یا ValueGeneratedOnAdd فوق، اگر مقداری به این نوع فیلدها انتساب داده شده باشد که با مقدار پیش فرض آنها (property.ClrType.GetDefaultValue) متفاوت باشد، از این مقدار جدید، بجای تولید مقداری خودکار، استفاده خواهد شد. برای مثال مقدار پیش فرض رشتهها، نال، مقادیر عددی، صفر و برای Guid مقدار Guid.Empty است. اگر هر مقدار دیگری بجای اینها به فیلدهای فوق انتساب داده شوند، از آنها استفاده میشود.
ج) تولید دادههای خودکار در هر دو حالت Add و Update
تولید دادهها در حالتهای Add و Update به این معنا است که یک چنین خواصی، همواره با فراخوانی متد SaveChanges، دارای مقدار خودکار جدیدی خواهند شد و نیازی نیست در کدها مقدار دهی شوند. برای مشخص سازی این نوع خواص، از ویژگی DatabaseGenerated با مقدار Computed و یا متد ValueGeneratedOnAddOrUpdate در حالت Fluent API میتوان استفاده کرد:
public class Blog { public int BlogId { get; set; } public string Url { get; set; } [DatabaseGenerated(DatabaseGeneratedOption.Computed)] public DateTime LastUpdated { get; set; } }
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .Property(b => b.LastUpdated) .ValueGeneratedOnAddOrUpdate(); }
تذکر: در اینجا نیز همانند حالت ValueGeneratedOnAdd، اگر این خواص مشخص شده، دارای مقدار متفاوتی با مقدار پیش فرض آنها باشند، از این مقادیر جدید بجای تولید خودکار مقادیر استفاده خواهد شد.
خواص محاسباتی (Computed Columns) و تفاوت آنها با DatabaseGeneratedOption.Computed
خواص محاسباتی (Computed Columns)، خواصی هستند که مقادیر آنها در بانک اطلاعاتی محاسبه میشوند و کاملا متفاوت هستند با DatabaseGeneratedOption.Computed که مفهوم دیگری دارد. DatabaseGeneratedOption.Computed به این معنا است که این فیلد خاص، با هر بار فراخوانی SaveChanges باید مقدار محاسبه شدهی جدیدی را داشته باشد و روش تولید این مقدار خودکار، یا بر اساس Guidهای سری است، یا توسط فیلدهای خود افزایندهی عددی و یا از طریق مقادیر پیش فرضی مانند getdate در حین ثبت یا به روز رسانی، مقدار دهی میشوند. اما خواص محاسباتی، یکی از امکانات «گزارشگیری سریع» SQL Server هستند و به نحو ذیل، تنها توسط Fluent API قابل تنظیم میباشند:
public class Person { public int PersonId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string DisplayName { get; set; } } public class MyContext : DbContext { public DbSet<Person> People { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Person>() .Property(p => p.DisplayName) .HasComputedColumnSql("[LastName] + ', ' + [FirstName]"); } }
امکان تعریف Sequence در EF Core 1.0
Sequence قابلیتی است که به SQL Server 2012 اضافه شدهاست و توضیحات بیشتر آنرا در مطلب «نحوه ایجاد Sequence و استفاده آن در Sql Server 2012» میتوانید مطالعه کنید.
در EF Core، امکان مدلسازی Sequence نیز پیش بینی شدهاست. آنها به صورت پیش فرض در مدلها ذکر نمیشوند و همچنین وابستگی به جدول خاصی ندارند. به همین جهت امکان تعریف آنها صرفا توسط Fluent API وجود دارد:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasSequence<int>("OrderNumbers", schema: "shared") .StartsAt(1000).IncrementsBy(5); modelBuilder.Entity<Order>() .Property(o => o.OrderNo) .HasDefaultValueSql("NEXT VALUE FOR shared.OrderNumbers"); }
در مثال فوق، ابتدا یک Sequence نمونه به نام OrderNumbers تعریف شدهاست که از عدد 1000 شروع شده و واحد افزایش آن 5 است. سپس از این نام در قسمت مقدار پیش فرض ستون OrderNo استفاده شدهاست.
و یا از Sequence ها میتوان برای تعیین مقدار پیش فرض Primary key بجای حالت identity خود افزایش یابنده استفاده کرد:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.HasSequence<int>("PrimaryKeyWithSequenceSequence"); modelBuilder.Entity<PrimaryKeyWithSequence>(entity => { entity.Property(e => e.PrimaryKeyWithSequenceId).HasDefaultValueSql("NEXT VALUE FOR [PrimaryKeyWithSequenceSequence]"); }); }
[PrimaryKeyWithSequenceSequence] دریافت و سپس بجای فیلد id درج میشود.
به این روش الگوریتم Hi-Low هم میگویند که یکی از مهمترین اهداف آن داشتن یک سری Id منحصربفرد، جهت بالابردن سرعت insertها در یک batch است. در حالت عادی insertها، ابتدا یک insert انجام میشود، سپس کوئری گرفته شده و آخرین Id درج شده به کلاینت بازگشت داده میشود. این روش، برای انجام تنها یک insert، سریع است. اما برای batch insert، به شدت کارآیی پایینی دارد. به همین جهت دسترسی به بازهای از اعداد منحصربفرد، پیش از شروع به insert تعداد زیادی رکورد، سرعت نهایی کار را بالا میبرد.
نحوهی تعریف ایندکسها در EF Core 1.0
برای افزودن ایندکسها به EF Core 1.0، تنها روش میسر، استفاده از Fluent API است (و برخلاف EF 6.x از روش data annotations فعلا پشتیبانی نمیکند؛ هرچند API جدید آن نسبت به EF 6.x بسیار واضحتر است و با ابهامات کمتر).
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .HasIndex(b => b.Url) .HasName("Index_Url");
modelBuilder.Entity<Blog>().HasIndex(b => b.Url).HasName("Index_Url").IsUnique();
modelBuilder.Entity<Person>() .HasIndex(idx => new { idx.FirstName, idx.LastName }) .IsUnique();
یک نکته: اگر از پروایدر SQL Server استفاده میکنید، میتوان متد الحاقی ویژهای را به نام ForSqlServerIsClustered نیز برای تعریف clustered indexes، در این زنجیره ذکر کرد.
امکان تعریف Alternate Keys در EF Core 1.0
به Unique Constraints در EF Core، نام Alternate Keys را دادهاند و این مورد نیز تنها از طریق Fluent API قابل تنظیم است:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Car>() .HasAlternateKey(c => c.LicensePlate) .HasName("AlteranteKey_LicensePlate"); }
اگر متد HasName در اینجا ذکر نشود، نام پیش فرض آن <type name>_<property name> خواهد بود و اگر همانند composite keys و یا ایندکسهای ترکیبی، چند خاصیت ذکر شوند، قسمت property name به جمع نام تمام خواص ذکر شده و جدا شدهی با _ تنظیم میشود.
برای نمونه اگر یک Alternate Key ترکیبی را به صورت ذیل تعریف کنیم:
modelBuilder.Entity<Person>() .HasAlternateKey(x => new { x.FirstName, x.LastName });
table.UniqueConstraint("AK_Persons_FirstName_LastName", x => new { x.FirstName, x.LastName });
سؤال: یک Unique Constraint با Unique Index چه تفاوتی دارد؟
در پشت صحنه، پیاده سازی یک Unique Constraint با Unique Index تفاوتی ندارند. فقط از دیدگاه روشنتر شدن مقصود، استفادهی از Unique Constraint ترجیح داده میشود.
البته از دیدگاه بانک اطلاعاتی پیاده سازی کننده نیز برای نمونه SQL Server، این تفاوتها وجود دارند:
الف) یک Unique Constraint را نمیتوان غیرفعال کرد؛ برخلاف Unique Indexها.
ب) Unique Constraintها موارد اضافهتری را مانند FILLFACTOR و IGNORE_DUP_KEY نیز میتوانند تنظیم کنند.
ج) امکان تعریف فیلترها برای Unique Indexها وجود دارد؛ برخلاف Unique Constraint ها.
که البته از دیدگاه EF، این سه مورد اهمیتی ندارند و بیشتر روشنتر شدن مقصود، هدف اصلی آنها است.
<system.webServer> <httpProtocol> <customHeaders> <!-- SECURITY HEADERS - https://securityheaders.io/? --> <!-- Protects against Clickjacking attacks. ref.: http://stackoverflow.com/a/22105445/1233379 --> <add name="X-Frame-Options" value="SAMEORIGIN" /> <!-- Protects against Clickjacking attacks. ref.: https://www.owasp.org/index.php/HTTP_Strict_Transport_Security_Cheat_Sheet --> <add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains"/> <!-- Protects against XSS injections. ref.: https://www.veracode.com/blog/2014/03/guidelines-for-setting-security-headers/ --> <add name="X-XSS-Protection" value="1; mode=block" /> <!-- Protects against MIME-type confusion attack. ref.: https://www.veracode.com/blog/2014/03/guidelines-for-setting-security-headers/ --> <add name="X-Content-Type-Options" value="nosniff" /> <!-- CSP modern XSS directive-based defence, used since 2014. ref.: http://content-security-policy.com/ --> <add name="Content-Security-Policy" value="default-src 'self'; font-src *;img-src * data:; script-src *; style-src *;" /> <!-- Prevents from leaking referrer data over insecure connections. ref.: https://scotthelme.co.uk/a-new-security-header-referrer-policy/ --> <add name="Referrer-Policy" value="strict-origin" /> </customHeaders> </httpProtocol> </system.webServer>