استفاده از Flash Uploader در ASP.NET MVC
انتخاب چندین فایل با هم
تعیین پسوند فایلهای مورد نظر
امکان نمایش درصد پیشرفت آپلود فایلها
همچنین حذف کلی لیست
حذف یک آیتم از لیست
و غیره را دارد استفاده فرمایید و نیازی هم به استفاده از فلش نمیباشد البته jquery.plupload بر حسب اینکه Browser کلاینتها عمل میکند و به صورت خودکار از نوعی که خود Browser کلاینتها میتوانند استفاده کنند استفاده مینماید برای توضیحات بیشتر لینک را ببینید.
1) بهروز رسانی شماره نگارش داتنت
اولین قدم در جهت ارتقاء پروژههای قدیمی، تغییر شماره نگارش TargetFramework موجود در فایل csproj. به net8.0 است. پس از اینکار نیاز است تمام بستههای نیوگت موجود را نیز به نگارشهای جدیدتر آنها ارتقاء دهید.
2) فعالسازی حالت SSR تعاملی سمت سرور
پایهی تمام تغییرات انجام شدهی در Blazor 8x، قابلیت SSR است و تمام امکانات دیگر برفراز آن اجرا میشوند. به همین جهت پس از ارتقاء شماره نگارش داتنت، نیاز است SSR را فعال کنیم و برای اینکار باید به هاست ASP.NET Core بگوئیم که درخواستهای رسیده را به کامپوننتهای Razor هدایت کند. بنابراین، به فایل Program.cs مراجعه کرده و دو تغییر زیر را به آن اعمال کنید:
// ... builder.Services.AddRazorComponents().AddInteractiveServerComponents(); // ... app.MapRazorComponents<App>().AddInteractiveServerRenderMode();
در اینجا ترکیب کامپوننتهای تعاملی سمت سرور (AddInteractiveServerComponents) و رندر تعاملی سمت سرور (AddInteractiveServerRenderMode)، دقیقا همان Blazor Server قدیمی است که ما با آن آشنا هستیم.
یک نکته: اگر از قالب جدید dotnet new blazor --interactivity None استفاده کنیم، یعنی حالت تعاملی بودن آنرا به None تنظیم کنیم، کلیات ساختار پروژهای را که مشاهده خواهیم کرد، با حالت تعاملی Server آن یکی است؛ فقط در تنظیمات Program.cs آن، گزینههای فوق را نداریم و به صورت زیر ساده شدهاست:
// ... builder.Services.AddRazorComponents(); // ... app.MapRazorComponents<App>();
3) ایجاد فایل جدید App.razor
در دات نت 8، دیگر خبری از فایل آغازین Host.cshtml_ پروژههای Blazor Server قدیمی نیست و کدهای آن با تغییراتی، به فایل جدید App.razor منتقل شدهاند. در این قسمت، کار هدایت درخواستهای رسیده به کامپوننتهای برنامه رخ میدهد و از این پس، صفحهی ریشهی برنامه خواهد بود.
در این تصویر، مقایسهای را بین جریان پردازش یک درخواست رسیده در دات نت 8، با نگارش قبلی Blazor Server مشاهده میکنید. در دات نت 8، فایل Host.cshtml_ (یک Razor Page آغازین برنامه) با یک کامپوننت Razor به نام App.razor جایگزین شدهاست و فایل قدیمی App.razor این پروژهها به Routes.razor، تغییر نام یافتهاست.
نمونهای از فایل App.razor جدید را که در قسمت قبل نیز معرفی کردیم، در اینجا با جزئیات بیشتری بررسی میکنیم:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <base href="/" /> <link rel="stylesheet" href="bootstrap/bootstrap.min.css" /> <link rel="stylesheet" href="app.css" /> <link rel="stylesheet" href="MyApp.styles.css" /> <link rel="icon" type="image/png" href="favicon.png" /> <HeadOutlet /> </head> <body> <Routes /> <script src="_framework/blazor.web.js"></script> </body> </html>
- تمام دایرکتیوهای تعریف شده مانند page ،@addTagHelper@ و غیره حذف شدهاند.
- base href تعریف شده اینبار فقط با یک / شروع میشود و نه با /~. این مورد خیلی مهم است! اگر به آن دقت نکنید، هیچکدام از فایلهای استاتیک برنامه مانند فایلهای css. و js.، بارگذاری نخواهند شد!
- پیشتر برای رندر HeadOutlet، از یک تگهلپر استفاده میشد. این مورد در نگارش جدید با یک کامپوننت ساده جایگزین شدهاست.
- تمام component tag helperهای پیشین حذف شدهاند و نیازی به آنها نیست.
- ارجاع پیشین فایل blazor.server.js با فایل جدید blazor.web.js جایگزین شدهاست.
یک نکته: همانطور که مشاهده میکنید، فایل App.razor یک کامپوننت است و اینبار به همراه تگ <script> نیز شدهاست. یعنی در این نگارش از Blazor میتوان اسکریپتها را در کامپوننتها نیز ذکر کرد؛ فقط با یک شرط! این کامپوننت حتما باید SSR باشد. اگر این تگ اسکریپتی را در یک کامپوننت تعاملی ذکر کنید، همانند قابل (و نگارشهای پیشین Blazor) با خطا مواجه خواهید شد.
4) ایجاد فایل جدید Routes.razor و مدیریت سراسری خطاها و صفحات یافت نشده
همانطور که عنوان شد، فایل قدیمی App.razor این پروژهها به Routes.razor تغییر نام یافتهاست که درج آنرا در قسمت body مشاهده میکنید. محتوای این فایل نیز به صورت زیر است:
<Router AppAssembly="@typeof(Program).Assembly"> <Found Context="routeData"> <RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" /> <FocusOnNavigate RouteData="@routeData" Selector="h1" /> </Found> </Router>
app.UseStatusCodePagesWithRedirects("/StatusCode/{0}");
@page "/StatusCode/{responseCode}" <h3>StatusCode @ResponseCode</h3> @code { [Parameter] public string? ResponseCode { get; set; } }
یک نکته: اگر پروژهای را بر اساس قالب dotnet new blazor --interactivity Server ایجاد کنیم، در فایل Program.cs آن، چنین تنظیمی اضافه شدهاست:
if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error", createScopeForErrors: true); }
سؤال: در اینجا (برنامههای Blazor Server) چه تفاوتی بین UseExceptionHandler و UseStatusCodePagesWithRedirects وجود دارد؟
میانافزار UseExceptionHandler برای مدیریت استثناءهای آغازین برنامه، پیش از تشکیل اتصال دائم SignalR وارد عمل میشود. پس از آن و تشکیل اتصال وبسوکت مورد نیاز، فقط از میانافزار UseStatusCodePagesWithRedirects استفاده میکند.
اگر علاقمند نیستید تا تمام خطاهای رسیده را همانند مثال فوق در یک صفحه مدیریت کنید، میتوانید حداقل سه فایل زیر را به برنامه اضافه کنید تا خطاهای متداول یافت نشدن آدرسی، بروز خطایی و یا عدم دسترسی را مدیریت کنند:
404.razor
@page "/StatusCode/404" <PageTitle>Not found</PageTitle> <h1>Not found</h1> <p role="alert">Sorry, there's nothing at this address.</p>
500.razor
@page "/StatusCode/500" <PageTitle>Unexpected error</PageTitle> <h1>Unexpected error</h1> <p role="alert">There was an unexpected error.</p>
401.razor
@page "/StatusCode/401" <PageTitle>Not Authorized</PageTitle> <h1>Not Authorized</h1> <p role="alert">Sorry, you are not authorized to access this page.</p>
5) تعاملی کردن سراسری برنامه
پس از این تغییرات اگر برنامه را اجرا کنید، بر اساس روش جدید static server-side rendering کار میکند و تعاملی نیست. یعنی تمام کامپوننتهای آن به صورت پیشفرض، یکبار بر روی سرور رندر شده و خروجی آنها به مرورگر کاربر ارسال میشوند و هیچ اتصال دائم SignalR ای برقرار نخواهد شد. برای فعالسازی سراسری قابلیتهای تعاملی برنامه و بازگشت به حالت Blazor Server قبلی، به فایل App.razor مراجعه کرده و دو تغییر زیر را اعمال کنید تا به صورت خودکار به تمام زیرکامپوننتها، یعنی کل برنامه، اعمال شود:
<HeadOutlet @rendermode="@InteractiveServer" /> ... <Routes @rendermode="@InteractiveServer" />
نکته 1: اجرای دستور زیر در داتنت 8، قالب پروژهای را ایجاد میکند که رفتار آن همانند پروژههای Blazor Server نگارشهای قبلی داتنت است (این مورد را در قسمت قبل بررسی کردیم)؛ یعنی همهجای آن به صورت پیشفرض، تعاملی است:
dotnet new blazor --interactivity Server --all-interactive
نکته 2: البته ... InteractiveServer، دقیقا همان حالت پیشفرض برنامههای Blazor Server قبلی نیست! این حالت رندر، به صورت پیشفرض به همراه پیشرندر (pre-rendering) هم هست. یعنی در این حالت، روال رویدادگردان OnInitializedAsync یک کامپوننت، دوبار فراخوانی میشود (که باید به آن دقت داشت و عدم توجه به آن میتواند سبب انجام دوبارهی کارهای سنگین آغازین یک کامپوننت شود)؛ یکبار برای پیشرندر صفحه به صورت یک HTML استاتیک (بدون فعال سازی هیچ قابلیت تعاملی) که برای موتورهای جستجو و بهبود SEO مفید است و بار دیگر برای فعالسازی قسمتهای تعاملی آن، درست پس از زمانیکه اتصال SignalR صفحه، برقرار شد (البته امکان فعالسازی حالت پیشرندر در Blazor Server قبلی هم وجود داشت؛ ولی مانند Blazor 8x، به صورت پیشفرض فعال نبود). در صورت نیاز، برای سفارشی سازی و لغو آن میتوان به صورت زیر عمل کرد:
@rendermode InteractiveServerRenderModeWithoutPrerendering @code{ static readonly IComponentRenderMode InteractiveServerRenderModeWithoutPrerendering = new InteractiveServerRenderMode(false); }
در مورد پیشرندر و روش مدیریت دوبار فراخوانی شدن روال رویدادگردان OnInitializedAsync یک کامپوننت در این حالت، در قسمتهای بعدی این سری بیشتر بحث خواهد شد.
خلاصه مهاجرت داده پروفایل ها
- کلاس جدیدی بسازید که دارای خواصی برای ذخیره اطلاعات پروفایل است.
- کلاس جدیدی بسازید که از 'ProfileBase' ارث بری میکند و متدهای لازم برای دریافت پروفایل کاربران را پیاده سازی میکند.
- استفاده از تامین کنندههای پیش فرض را، در فایل web.config فعال کنید. و کلاسی که در مرحله 2 ساختید را بعنوان کلاس پیش فرض برای خواندن اطلاعات پروفایل معرفی کنید.
شروع به کار
پوشه جدیدی با نام 'Models' بسازید تا اطلاعات پروفایل را در آن قرار دهیم.
بعنوان یک مثال، بگذارید تا تاریخ تولد کاربر، شهر سکونت، قد و وزن او را در پروفایلش ذخیره کنیم. قد و وزن بصورت یک کلاس سفارشی (custom class) بنام 'PersonalStats' ذخیره میشوند. برای ذخیره و بازیابی پروفایل ها، به کلاسی احتیاج داریم که 'ProfileBase' را ارث بری میکند. پس کلاس جدیدی با نام 'AppProfile' بسازید.
public class ProfileInfo { public ProfileInfo() { UserStats = new PersonalStats(); } public DateTime? DateOfBirth { get; set; } public PersonalStats UserStats { get; set; } public string City { get; set; } } public class PersonalStats { public int? Weight { get; set; } public int? Height { get; set; } } public class AppProfile : ProfileBase { public ProfileInfo ProfileInfo { get { return (ProfileInfo)GetPropertyValue("ProfileInfo"); } } public static AppProfile GetProfile() { return (AppProfile)HttpContext.Current.Profile; } public static AppProfile GetProfile(string userName) { return (AppProfile)Create(userName); } }
پروفایل را در فایل web.config خود فعال کنید. نام کلاسی را که در مرحله قبل ساختید، بعنوان کلاس پیش فرض برای ذخیره و بازیابی پروفایلها معرفی کنید.
<profile defaultProvider="DefaultProfileProvider" enabled="true" inherits="UniversalProviders_ProfileMigrations.Models.AppProfile"> <providers> ..... </providers> </profile>
برای دریافت اطلاعات پروفایل از کاربر، فرم وب جدیدی در پوشه Account بسازید و آنرا 'AddProfileData.aspx' نامگذاری کنید.
<h2> Add Profile Data for <%# User.Identity.Name %></h2> <asp:Label Text="" ID="Result" runat="server" /> <div> Date of Birth: <asp:TextBox runat="server" ID="DateOfBirth"/> </div> <div> Weight: <asp:TextBox runat="server" ID="Weight"/> </div> <div> Height: <asp:TextBox runat="server" ID="Height"/> </div> <div> City: <asp:TextBox runat="server" ID="City"/> </div> <div> <asp:Button Text="Add Profile" ID="Add" OnClick="Add_Click" runat="server" /> </div>
کد زیر را هم به فایل code-behind اضافه کنید.
protected void Add_Click(object sender, EventArgs e) { AppProfile profile = AppProfile.GetProfile(User.Identity.Name); profile.ProfileInfo.DateOfBirth = DateTime.Parse(DateOfBirth.Text); profile.ProfileInfo.UserStats.Weight = Int32.Parse(Weight.Text); profile.ProfileInfo.UserStats.Height = Int32.Parse(Height.Text); profile.ProfileInfo.City = City.Text; profile.Save(); }
دقت کنید که فضای نامی که کلاس AppProfile در آن قرار دارد را وارد کرده باشید.
اپلیکیشن را اجرا کنید و کاربر جدیدی با نام 'olduser' بسازید. به صفحه جدید 'AddProfileData' بروید و اطلاعات پروفایل کاربر را وارد کنید.
با استفاده از پنجره Server Explorer میتوانید تایید کنید که اطلاعات پروفایل با فرمت xml در جدول 'Profiles' ذخیره میشوند.
مهاجرت الگوی دیتابیس
اسکریپت مورد نیاز را از آدرس https://raw.github.com/suhasj/UniversalProviders-Identity-Migrations/master/Migration.txt دریافت کرده و آن را اجرا کنید. اگر اتصال خود به دیتابیس را تازه کنید خواهید دید که جداول جدیدی اضافه شده اند. میتوانید دادههای این جداول را بررسی کنید تا ببینید چگونه اطلاعات منتقل شده اند.
مهاجرت اپلیکیشن برای استفاده از ASP.NET Identity
- Microsoft.AspNet.Identity.EntityFramework
- Microsoft.AspNet.Identity.Owin
- Microsoft.Owin.Host.SystemWeb
- Microsoft.Owin.Security.Facebook
- Microsoft.Owin.Security.Google
- Microsoft.Owin.Security.MicrosoftAccount
- Microsoft.Owin.Security.Twitter
using Microsoft.AspNet.Identity.EntityFramework; using System; using System.Collections.Generic; using System.Linq; using System.Web; using UniversalProviders_ProfileMigrations.Models; namespace UniversalProviders_Identity_Migrations { public class User : IdentityUser { public User() { CreateDate = DateTime.UtcNow; IsApproved = false; LastLoginDate = DateTime.UtcNow; LastActivityDate = DateTime.UtcNow; LastPasswordChangedDate = DateTime.UtcNow; Profile = new ProfileInfo(); } public System.Guid ApplicationId { get; set; } public bool IsAnonymous { get; set; } public System.DateTime? LastActivityDate { get; set; } public string Email { get; set; } public string PasswordQuestion { get; set; } public string PasswordAnswer { get; set; } public bool IsApproved { get; set; } public bool IsLockedOut { get; set; } public System.DateTime? CreateDate { get; set; } public System.DateTime? LastLoginDate { get; set; } public System.DateTime? LastPasswordChangedDate { get; set; } public System.DateTime? LastLockoutDate { get; set; } public int FailedPasswordAttemptCount { get; set; } public System.DateTime? FailedPasswordAttemptWindowStart { get; set; } public int FailedPasswordAnswerAttemptCount { get; set; } public System.DateTime? FailedPasswordAnswerAttemptWindowStart { get; set; } public string Comment { get; set; } public ProfileInfo Profile { get; set; } } }
انتقال داده پروفایلها به جداول جدید
آخرین نسخه پکیج Entity Framework را نصب کنید. همچنین یک رفرنس به اپلیکیشن وب پروژه بدهید (کلیک راست روی پروژه و گزینه 'Add Reference').
کد زیر را در کلاس Program.cs وارد کنید. این قطعه کد پروفایل تک تک کاربران را میخواند و در قالب 'ProfileInfo' آنها را serialize میکند و در دیتابیس ذخیره میکند.
public class Program { var dbContext = new ApplicationDbContext(); foreach (var profile in dbContext.Profiles) { var stringId = profile.UserId.ToString(); var user = dbContext.Users.Where(x => x.Id == stringId).FirstOrDefault(); Console.WriteLine("Adding Profile for user:" + user.UserName); var serializer = new XmlSerializer(typeof(ProfileInfo)); var stringReader = new StringReader(profile.PropertyValueStrings); var profileData = serializer.Deserialize(stringReader) as ProfileInfo; if (profileData == null) { Console.WriteLine("Profile data deserialization error for user:" + user.UserName); } else { user.Profile = profileData; } } dbContext.SaveChanges(); }
برخی از مدلهای استفاده شده در پوشه 'IdentityModels' تعریف شده اند که در پروژه اپلیکیشن وبمان قرار دارند، بنابراین افزودن فضاهای نام مورد نیاز فراموش نشود.
کد بالا روی دیتابیسی که در پوشه App_Data وجود دارد کار میکند، این دیتابیس در مراحل قبلی در اپلیکیشن وب پروژه ایجاد شد. برای اینکه این دیتابیس را رفرنس کنیم باید رشته اتصال فایل app.config اپلیکیشن کنسول را بروز رسانی کنید. از همان رشته اتصال web.config در اپلیکیشن وب پروژه استفاده کنید. همچنین آدرس فیزیکی کامل را در خاصیت 'AttachDbFilename' وارد کنید.
یک Command Prompt باز کنید و به پوشه bin اپلیکیشن کنسول بالا بروید. فایل اجرایی را اجرا کنید و نتیجه را مانند تصویر زیر بررسی کنید.
در پنجره Server Explorer جدول 'AspNetUsers' را باز کنید. حال ستونهای این جدول باید خواص کلاس مدل را منعکس کنند.
کارایی سیستم را تایید کنید
- استفاده از WCF Proxy
- استفاده از ChannelFactory
قصد دارم طی یک مقایسه کوتاه این دو روش را بررسی کنیم:
WCF Proxy:
Proxy در واقع یک کلاس CLR است که به عنوان نماینده یک اینترفیس که از نوع Service Contract است مورد استفاده قرار میگیرد. یا به زبان ساده تر، یک Proxy در واقع نماینده Service Contract ای که سمت سرور پیاده سازی شده است در سمت کلاینت خواهد بود. Proxy تمام متد یا Operation Contractهای سمت سرور را داراست به همراه یک سری متدها و خواص دیگر برای مدیریت چرخه طول عمر سرویس، هم چنین اطلاعات مربوط به وضعیت سرویس و نحوه اتصال آن به سرور. ساخت Proxy به دو روش امکان پذیر است:
- با استفاده از امکانات AddServiceReference موجود در Visual Studio. کافیست از پنجره معروف زیر با استفاده از یک URL سرویس مورد نظر را به پروژه سمت کلاینت خود اضافه نمایید
همچنین میتوانید از قسمت Advanced نیز برای تنظیمات خاص مورد نظر خود(مثل تولید کد برای متدهای Async یا تعیین نوع Collectionها در هنگام انتقال داده و ...) استفاده نمایید.
- با استفاده از SvcUtil.exe . کاربرد svcutil.exe در موارد Metadata Export، Service Validtation، XmlSerialization Type Generator و Metadata Download و ... خلاصه میشود. با استفاده از Vs.Net Command Promptو svcutil میتوان به سرویس مورد نظر دسترسی داشت.مثال
svcutil.exe /language:vb /out:generatedProxy.vb /config:app.config http://localhost:8000/ServiceModelSamples/service
ChannelFactory:
ChannelFactory یک کلاس تعبیه شده در دات نت میباشد که به وسیله یک اینترفیس که به عنوان تعاریف سرویس سمت سرور است یک نمونه از سرویس مورد نظر را برای ما خواهد ساخت. اما به خاظر داشته باشید از این روش زمانی میتوان استفاده کرد که دسترسی کامل به سمت سرور و کلاینت داشته باشید.
برای آشنایی با نحوه پیاده سازی به این روش نیز میتوانید از این مقاله کمک بگیرید.
مثال:
public static TChannel CreateChannel() { IBookService service; var endPointAddress = new EndpointAddress( "http://localhost:7000/service.svc" ); var httpBinding = new BasicHttpBinding(); ChannelFactory<TChannel> factory = new ChannelFactory<TChannel>( httpBinding, endPointAddress ); instance= factory.CreateChannel(); return instance; }
در نتیجه برای ساخت یک سرویس به روش ChannelFactory باید مراحل زیر را طی نمایید:
- یک نمونه از WCF Binding بسازید
- یک نمونه از کلاس EndPointAddress به همراه آدرس سرویس مورد نظر در سمت سرور بسازید(البته میتوان این مرحله را نادیده گرفت و آدرس سرویس را مستقیما به نمونه ChannelFactory به عنوان پارامتر پاس داد)
- یک نمونه از کلاس ChannelFactory یا استفاده از EndPointAddress بسازید
- با استفاده از ChannelFactory یک نمونه از Channel مورد نظر را فراخوانی نمایید(فراخوانی متد CreateChannel)
تفاوتهای دو روش
Proxy | ChannelFactory |
فقط نیاز به یک URL برای ساخت سرویس مورد نظر دارد. بقیه مراحل توسط ابزارهای مرتبط انجام خواهد شد | شما نیاز به دسترسی مستقیم به اسمبلی حاوی Service Contract پروژه خود دارید. |
استفاده از این روش بسیار آسان و ساده است | پیاده سازی آن پیچیدگی بیشتر دارد |
فهم مفاهیم این روش بسیار راحت است | نیاز به دانش اولیه از مفاهیم WCF برای پیاده سازی دارد |
زمانی که میزان تغییرات در کلاسهای مدل و Entityها زیاد باشد این روش بسیار موثر است.(مدیریت تغییرات در WCF) | زمانی که اطمینان دارید که مدل و entityها پروژه زیاد تغییر نخواهند کرد و از طرفی نیاز به کد نویسی کمتر در سمت کلاینت دارید، این روش موثرتر خواهد بود |
فقط به اینترفیس هایی که دارای ServiceContractAttribute هستند دسترسی خواهیم داشت. | به تمام اینترفیسهای تعریف شده در بخش Contracts دسترسی داریم. |
فقط به متدهای که دارای OperationContractAttribute هستند دسترسی خواهیم داشت. | به تمام متدهای عمومی سرویس دسترسی داریم. |
آیا میتوان از روش AddServiceReference تعبیه شده در Vs.Net، برای ساخت ChannelFactory استفاده کرد؟
بله! کافیست هنگام ساخت سرویس، در پنجره AddServiceReference از قسمت Advanced وارد برگه تنظیمات شوید. سپس تیک مربوط به قسمت های Reused Type in referenced assemblies و Reused Types in specified referenced assemblies را بزنید. بعد از لیست پایین، اسمبلیهای مربوط به Domain Model و هم چنین Contractهای سمت سرور را انتخاب نمایید. در این حالت شما از روش Channel Factory برای ساخت سرویس WCF استفاده کرده اید.
میخواهید پروژهای را انجام دهید که شامل جداول زیر است:
مقالات، اخبار، گالری تصاویر، گالری ویدیو، اسلایدشو، تبلیغات و ... و تمامی این جداول حداقل شامل یک فایل پیوست (عکس، فیلم، ...) میباشند. به طور مثال جدول مقالات دارای یک عکس نیز میباشد. قصد داریم تمام فایلها را بر روی هاست ذخیره کرده و فقط آدرس و نام فایل را در دیتابیس ذخیره نمایم.
روش اول : استفاده از یک فیلد در هر جدول برای نگه دارای اسم فایل
مثال:
public class Article { public int Id { get; set; } public string Title { get; set; } public string Body { get; set; } public string RegisterDate { get; set; } public string FileName { get; set; } }
این روش فقط در صورتی پاسخگو میباشد که هر رکورد فقط شامل یک فایل باشد. به طور مثال ممکن است برای یک مقاله، چندین عکس و فایل را ضمیمهی آن کنیم. در این حالت این روش پاسخ گو نمیباشد؛ ولی میتوانیم به صورت زیر نیز عمل کنیم:
ایجاد جدولی برای نگهداری فایلهای هر رکورد از مقاله :
public class ArticleFiles { public int Id { get; set; } public string FielName { get; set; } public string FileExtension { get; set; } public Article Article { get; set; } public int FileSize { get; set; } }
میتوانیم جدولی را به نام Attachment ایجاد کرده و هر فایلی را که آپلود میکنیم، مشخصات آن را در این جدول ذخیره کنیم و هر جدول هم که نیازی به فایل داشت، رابطهای با این جدول برقرار کند. در این حالت خواهیم داشت:
public class Attachment { public int Id { get; set; } public string Title { get; set; } public string FileName { get; set; } public string Extension { get; set; } public DateTime RegisterDate { get; set; } public int Size { get; set; } public ICollection<Article> ArticleFiles { get; set; } public ICollection<News> NewsFiles { get; set; } public int Viewed { get; set; } }
روش سوم : جدولی برای نگه داری اسم فایلها، بدون رابطه
جدول Attachment در این روش، همانند روش دوم میباشد؛ با دو تفاوت:
1- با هیچ جدولی رابطهای ندارد.
2- دو فیلد به عنوان نام جدول و Id رکورد به آن اضافه شده است.
تفاوت نسبت به روش دوم:
در روش دوم، ثبت یک رکورد، وابستهی به ثبت رکورد در جدول Attachment بود و ابتدا میبایستی فایل در Attachment ذخیره میشد و بعد از بدست آوردن Id آن، رکورد مورد نظر (مقاله) را درج میکردیم. ولی در این روش ابتدا مقاله درج شده و بعد از آن فایل را با اسم جدول و ID رکورد مورد نظر ذخیره میکنیم:
public class Attachment { public int Id { get; set; } public string Title { get; set; } public string FileName { get; set; } public string Extension { get; set; } public DateTime RegisterDate { get; set; } public int Size { get; set; } public string TableName { get; set; } public int RowId { get; set; } public int Viewed { get; set; } }
ایجاد یک کلاس پایه و ارث بری سایر کلاسها از کلاس پایه و ایجاد رابطهای بین کلاس پایه و کلاسهای مشتق شده.
نظراتی پیرامون حالتهای مختلف:
1- داشتن یک جدول الحاقات برای هر جدول
- اضافه کردن یک فیلد: بعضیها این
روش را ترجیح میدهند. به این دلیل که هر جدول، یک جدول attachment مختص به
خود دارد؛ با توجه به فیلدهایی که لازم است. به طور مثال ممکن است بعد از
گذشت مدتی، نیاز باشد تا دو فیلد برای فایلهای هر مقاله اضافه شوند که در
این حالت فقط به جدول attachment مقاله اضافه خواهند شد.
2- داشتن یک جدول پایه که کل فایلها در آن ذخیره شوند (روشهای دوم و سوم)
- متمرکز شدن کل فایلها در یک جدول: بیشتر پروژهها و یا برنامه نویسان (طبق تجربهی بنده) یک جدول پایه را برای این منظور دوست دارند. به دلیل اینکه تمام اطلاعات یکجا باشد.
- عدم آپلود چندین بارهی یک فایل: در این حالت میتوان از یک فایل چندین بار در چند جای مختلف استفاده نمود و در فضای هاست صرفه جویی میشود. این روش مدیریت سختی دارد و نیازمند کوئریهای بیشتری میباشد.
- وجود فیلدهای زیاد null در جدول: در این حالت ممکن است ردیفهایی با ستونهای مقدار null در جدول زیاد شوند. فرض کنید دو فیلد در جدول attachment وجود دارند که فقط توسط جدول مقالات مورد استفاده قرار میگیرند و در بقیهی جداول بدون استفاده میباشند.
از کدام روش استفاده کنیم؟
نمی توان پیشنهاد کرد که الزاما از کدامیک از روشهای بالا باید استفاده کنیم؛ چون نیازمندهایهای هر پروژه با هم متفات است و نمیتوان نسخهای خاص را برای همه تجویز کرد.
Gulp #3
نصب bower
- ساده کردن تعریف وابستگیهای منابع پروژه با تعریف یک فایل bower.json
- نیازی به commit کردن واستگیهای پروژه نیست.
- با ذکر ورژن مربوط به وابستگی یا محدودهی قابل قبول برای آن، به روز رسانی منابع به سادگی با یک دستور انجام میشود.
- وابستگی های وابسته به یک منبع را نیز نصب میکند. برای مثال زمانیکه بوت استرپ را به عنوان وابستگی پروژه تعریف میکنیم، وابستگی آن یعنی jquery را چون در فایل bower.json بوت استرپ تعریف شدهاست، به صورت خودکار دانلود میکند.
- در نهایت افراد هم تیمی یا توسعه دهندگان دیگر به راحتی با زدن دستور bower install تمام وابستگیهای پروژه را میتوانند نصب کنند.
sudo npm install -g bower bower init
bower install bootstrap-sass-official --save bower install fontawesome --save bower install bootstrap-rtl --save
نصب پلاگینهای مورد نیاز gulp
sudo npm install gulp gulp-ruby-sass gulp-notify gulp-bower --save-dev
نوشتن تسکها برای گالپ
var gulp = require('gulp'), sass = require('gulp-ruby-sass'), notify = require('gulp-notify'), bower = require('gulp-bower');
var config = { sassPath = './resources/sass', bowerDir = './bower_components' }
// create a task to do bower install gulp.task('bower', function() { return bower() .pipe(gulp.dest(config.bowerDir)) });
// Copy js files to public folder gulp.task('js', function() { return gulp.src([config.bowerDir + '/bootstrap-sass-official/assets/javascripts/bootstrap.min.js', config.bowerDir + '/jquery/dist/jquery.min.js' ]) .pipe(gulp.dest('./public/js')); }); // Copy fontawesome icons to public/fonts folder gulp.task('icons', function() { return gulp.src(config.bowerDir + '/fontawesome/fonts/**.*') .pipe(gulp.dest('./public/fonts')); });
gulp.task('css', function() { return sass(config.sassPath + '/style.scss', { // Our coustom sass style: 'compressed', // minify css loadPath: [ // load paths to easy use import in resources/sass './resources/sass', config.bowerDir + '/bootstrap-sass-official/assets/stylesheets', // bootstrap sass files config.bowerDir + '/fontawesome/scss' // awesome icons sass files ] }) });
gulp.task('css', function() { return sass(config.sassPath + '/style.scss', { // Our coustom sass style: 'compressed', // minify css loadPath: [ // load paths to easy use import in resources/sass './resources/sass', config.bowerDir + '/bootstrap-sass-official/assets/stylesheets', // bootstrap sass files config.bowerDir + '/fontawesome/scss' // awesome icons sass files ] }) .on('error', notify.onError(function(error) { return 'Error: ' + error.message; })) .pipe(gulp.dest('./public/css')); });
// Rerun the task when a file changes gulp.task('watch', function() { gulp.watch(config.sassPath + '/**/*.scss', ['css']); });
// Run this task with : gulp // OR gulp default gulp.task('default', ['bower', 'icons', 'css','js']);
مخزن گیت هاب
API Versioning
- URI-based versioning
- Header-based versioning
- Media type-based versioning
پیاده سازی URI-based versioning
public class ItemViewModel { public int Id { get; set; } public string Name { get; set; } public string Country { get; set; } } public class ItemV2ViewModel : ItemViewModel { public double Price { get; set; } }
public class ItemsController : ApiController { [ResponseType(typeof(ItemViewModel))] public IHttpActionResult Get(int id) { var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" }; return Ok(viewModel); } } public class ItemsV2Controller : ApiController { [ResponseType(typeof(ItemV2ViewModel))] public IHttpActionResult Get(int id) { var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 }; return Ok(viewModel); } }
config.Routes.MapHttpRoute("ItemsV2", "api/v2/items/{id}", new { controller = "ItemsV2", id = RouteParameter.Optional }); config.Routes.MapHttpRoute("Items", "api/items/{id}", new { controller = "Items", id = RouteParameter.Optional }); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } );
config.Routes.MapHttpRoute( "defaultVersioned", "api/v{version}/{controller}/{id}", new { id = RouteParameter.Optional }, new { version = @"\d+" }); config.Routes.MapHttpRoute( "DefaultApi", "api/{controller}/{id}", new { id = RouteParameter.Optional } );
با این تنظیمات فعلا به مسیریابی ورژن بندی شدهای دست نیافتهایم. زیرا فعلا به هیچ طریق به Web API اشاره نکردهایم که به چه صورت از این پارامتر version برای پیدا کردن کنترلر ورژن بندی شده استفاده کند و به همین دلیل این دو مسیریابی نوشته شده در عمل نتیجه یکسانی را خواهند داشت. برای رفع مشکل مطرح شده باید فرآیند پیش فرض انتخاب کنترلر را کمی شخصی سازی کنیم.
IHttpControllerSelector مسئول پیدا کردن کنترلر مربوطه با توجه به درخواست رسیده میباشد. شکل زیر مربوط است به مراحل ساخت کنترلر بر اساس درخواست رسیده:
به جای پیاده سازی مستقیم این اینترفیس، از پیاده سازی کننده پیش فرض موجود (DefaultHttpControllerSelector) اسفتاده کرده و HttpControllerSelector جدید ما از آن ارث بری خواهد کرد.
public class VersionFinder { private static bool NeedsUriVersioning(HttpRequestMessage request, out string version) { var routeData = request.GetRouteData(); if (routeData != null) { object versionFromRoute; if (routeData.Values.TryGetValue(nameof(version), out versionFromRoute)) { version = versionFromRoute as string; if (!string.IsNullOrWhiteSpace(version)) { return true; } } } version = null; return false; } private static int VersionToInt(string versionString) { int version; if (string.IsNullOrEmpty(versionString) || !int.TryParse(versionString, out version)) return 0; return version; } public static int GetVersionFromRequest(HttpRequestMessage request) { string version; return NeedsUriVersioning(request, out version) ? VersionToInt(version) : 0; } }
کلاس VersionFinder برای یافتن ورژن رسیده در درخواست جاری مورد استفاده قرار خواهد گرفت. با استفاده از متد NeedsUriVersioning بررسی صورت میگیرد که آیا در این درخواست پارامتری به نام version وجود دارد یا خیر که درصورت موجود بودن، مقدار آن واکشی شده و درون پارامتر out قرار میگیرد. در متد GetVersionFromRequest بررسی میشود که اگر خروجی متد NeedsUriVersioning برابر با true باشد، با استفاده از متد VersionToInt مقدار به دست آمده را به int تبدیل کند.
public class VersionAwareControllerSelector : DefaultHttpControllerSelector { public VersionAwareControllerSelector(HttpConfiguration configuration) : base(configuration) { } public override string GetControllerName(HttpRequestMessage request) { var controllerName = base.GetControllerName(request); var version = VersionFinder.GetVersionFromRequest(request); return version > 0 ? GetVersionedControllerName(request, controllerName, version) : controllerName; } private string GetVersionedControllerName(HttpRequestMessage request, string baseControllerName, int version) { var versionControllerName = $"{baseControllerName}v{version}"; HttpControllerDescriptor descriptor; if (GetControllerMapping().TryGetValue(versionControllerName, out descriptor)) { return versionControllerName; } throw new HttpResponseException(request.CreateErrorResponse( HttpStatusCode.NotFound, $"No HTTP resource was found that matches the URI {request.RequestUri} and version number {version}")); } }
متد GetControllerName وظیفه بازگشت دادن نام کنترلر را برعهده دارد. ما نیز با لغو رفتار پیش فرض این متد و تهیه نام ورژن بندی شده کنترلر و معرفی این پیاده سازی از IHttpControllerSelector به شکل زیر میتوانیم به Web API بگوییم که به چه صورت از پارامتر version موجود در درخواست استفاده کند.
config.Services.Replace(typeof(IHttpControllerSelector), new VersionAwareControllerSelector(config));
حال با اجرای برنامه به نتیجه زیر خواهیم رسید:
راه حل دوم: برای زمانیکه Attribute Routing مورد استفاده شما است میتوان به راحتی با تعریف قالبهای مناسب مسیریابی، API ورژن بندی شده را ارائه دهید.
[RoutePrefix("api/v1/Items")] public class ItemsController : ApiController { [ResponseType(typeof(ItemViewModel))] [Route("{id:int}")] public IHttpActionResult Get(int id) { var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" }; return Ok(viewModel); } } [RoutePrefix("api/V2/Items")] public class ItemsV2Controller : ApiController { [ResponseType(typeof(ItemV2ViewModel))] [Route("{id:int}")] public IHttpActionResult Get(int id) { var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 }; return Ok(viewModel); } }
اگر توجه کرده باشید در مثال ما، نامهای کنترلرها متفاوت از هم میباشند. اگر بجای در نظر گرفتن نامهای مختلف برای یک کنترلر در ورژنهای مختلف، آن را با یک نام یکسان درون namespaceهای مختلف احاطه کنیم یا حتی آنها را درون Class Libraryهای جدا نگهداری کنیم، به مشکل "یافت شدن چندین کنترلر که با درخواست جاری مطابقت دارند" برخواهیم خورد. برای حل این موضوع به راه حل سوم توجه کنید.
راه حل سوم: استفاده از یک NamespaceControllerSelector که پیاده سازی دیگری از اینترفیس IHttpControllerSelector میباشد. فرض بر این است که قالب پروژه به شکل زیر میباشد:
کار با پیاده سازی اینترفیس IHttpRouteConstraint آغاز میشود:
public class VersionConstraint : IHttpRouteConstraint { public VersionConstraint(string allowedVersion) { AllowedVersion = allowedVersion.ToLowerInvariant(); } public string AllowedVersion { get; private set; } public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection) { object value; if (values.TryGetValue(parameterName, out value) && value != null) { return AllowedVersion.Equals(value.ToString().ToLowerInvariant()); } return false; } }
کلاس بالا در واقع برای اعمال محدودیت خاصی که در ادامه توضیح داده میشود، پیاده سازی شده است.
متد Match آن وظیفه چک کردن برابری مقدار کلید parameterName موجود در درخواست را با مقدار allowedVersion ای که API از آن پشتیبانی میکند، برعهده دارد. با استفاده از این Constraint مشخص کردهایم که دقیقا چه زمانی باید Route نوشته شده انتخاب شود.
به روش استفاده از این Constraint توجه کنید:
namespace UriBasedVersioning.Namespace.Controllers.V1 { using Models.V1; RoutePrefix("api/{version:VersionConstraint(v1)}/items")] public class ItemsController : ApiController { [ResponseType(typeof(ItemViewModel))] [Route("{id}")] public IHttpActionResult Get(int id) { var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" }; return Ok(viewModel); } } } namespace UriBasedVersioning.Namespace.Controllers.V2 { using Models.V2; RoutePrefix("api/{version:VersionConstraint(v2)}/items")] public class ItemsController : ApiController { [ResponseType(typeof(ItemViewModel))] [Route("{id}")] public IHttpActionResult Get(int id) { var viewModel = new ItemViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 }; return Ok(viewModel); } } }
با توجه به کد بالا میتوان دلیل استفاده از VersionConstraint را هم درک کرد؛ از آنجایی که ما دو Route شبیه به هم داریم، لذا باید مشخص کنیم که در چه شرایطی و کدام یک از این Routeها انتخاب شود. خوب، اگر برنامه را اجرا کرده و یکی از APIهای بالا را تست کنید، با خطا مواجه خواهید شد؛ زیرا فعلا این Constraint به سیستم Web API معرفی نشده است. تنظیمات زیر را انجام دهید:
var constraintsResolver = new DefaultInlineConstraintResolver(); constraintsResolver.ConstraintMap.Add(nameof(VersionConstraint), typeof (VersionConstraint)); config.MapHttpAttributeRoutes(constraintsResolver);
مرحله بعدی کار، پیاده سازی IHttpControllerSelector میباشد:
public class NamespaceControllerSelector : IHttpControllerSelector { private readonly HttpConfiguration _configuration; private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers; public NamespaceControllerSelector(HttpConfiguration config) { _configuration = config; _controllers = new Lazy<Dictionary<string, HttpControllerDescriptor>>(InitializeControllerDictionary); } public HttpControllerDescriptor SelectController(HttpRequestMessage request) { var routeData = request.GetRouteData(); if (routeData == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } var controllerName = GetControllerName(routeData); if (controllerName == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } var version = GetVersion(routeData); if (version == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", version, controllerName); HttpControllerDescriptor controllerDescriptor; if (_controllers.Value.TryGetValue(controllerKey, out controllerDescriptor)) { return controllerDescriptor; } throw new HttpResponseException(HttpStatusCode.NotFound); } public IDictionary<string, HttpControllerDescriptor> GetControllerMapping() { return _controllers.Value; } private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary() { var dictionary = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase); var assembliesResolver = _configuration.Services.GetAssembliesResolver(); var controllersResolver = _configuration.Services.GetHttpControllerTypeResolver(); var controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver); foreach (var controllerType in controllerTypes) { var segments = controllerType.Namespace.Split(Type.Delimiter); var controllerName = controllerType.Name.Remove(controllerType.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length); var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", segments[segments.Length - 1], controllerName); if (!dictionary.Keys.Contains(controllerKey)) { dictionary[controllerKey] = new HttpControllerDescriptor(_configuration, controllerType.Name, controllerType); } } return dictionary; } private static T GetRouteVariable<T>(IHttpRouteData routeData, string name) { object result; if (routeData.Values.TryGetValue(name, out result)) { return (T)result; } return default(T); } private static string GetControllerName(IHttpRouteData routeData) { var subroute = routeData.GetSubRoutes().FirstOrDefault(); var dataTokenValue = subroute?.Route.DataTokens.First().Value; var controllerName = ((HttpActionDescriptor[])dataTokenValue)?.First().ControllerDescriptor.ControllerName.Replace("Controller", string.Empty); return controllerName; } private static string GetVersion(IHttpRouteData routeData) { var subRouteData = routeData.GetSubRoutes().FirstOrDefault(); return subRouteData == null ? null : GetRouteVariable<string>(subRouteData, "version"); } }
سورس اصلی کلاس بالا از این آدرس قابل دریافت است. در تکه کد بالا بخشی که مربوط به چک کردن تکراری بودن آدرس میباشد، برای ساده سازی کار حذف شده است. ولی نکتهی مربوط به SubRoutes که برای واکشی مقادیر پارامترهای مرتبط با Attribute Routing میباشد، اضافه شده است. روال کار به این صورت است که ابتدا RouteData موجود در درخواست را واکشی کرده و با استفاده از متدهای GetControllerName و GetVersion، پارامترهای controller و version را جستجو میکنیم. بعد با استفاده از مقادیر به دست آمده، controllerKey را تشکیل داده و درون کنترلرهای موجود در برنامه به دنبال کنترلر مورد نظر خواهیم گشت.
کارکرد متد InitializeControllerDictionary :
همانطور که میدانید به صورت پیشفرض Web API توجهی به فضای نام مرتبط با کنترلرها ندارد. از طرفی برای پیاده سازی اینترفیس IHttpControllerSelector نیاز است متدی با امضای زیر را داشته باشیم:
public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
لذا در کلاس پیاده سازی شده، خصوصیتی به نام _controllers را که از به صورت Lazy و از نوع بازگشتی متد بالا میباشد، تعریف کردهایم. متد InitializeControllerDictionary کار آماده سازی دادههای مورد نیاز خصوصیت _controllers میباشد. به این صورت که تمام کنترلرهای موجود در برنامه را واکشی کرده و این بار کلیدهای مربوط به دیکشنری را با استفاده از نام کنترلر و آخرین سگمنت فضای نام آن، تولید و درون دیکشنری مورد نظر ذخیره کردهایم.
حال تنظیمات زیر را اعمال کنید:
public static class WebApiConfig { public static void Register(HttpConfiguration config) { var constraintsResolver = new DefaultInlineConstraintResolver(); constraintsResolver.ConstraintMap.Add(nameof(VersionConstraint), typeof (VersionConstraint)); config.MapHttpAttributeRoutes(constraintsResolver); config.Services.Replace(typeof(IHttpControllerSelector), new NamespaceControllerSelector(config)); } }
این بار برنامه را اجرا کرده و APIهای مورد نظر را تست کنید؛ بله بدون مشکل کار خواهد کرد.
نکته تکمیلی: سورس مذکور در سایت کدپلکس برای حالتی که از Centralized Routes استفاده میکنید آماده شده است. روش مذکور در این مقاله هم فقط قسمت Duplicate Routes آن را کم دارد که میتوانید اضافه کنید. پیاده سازی دیگری را از این راه حل هم میتوانید داشته باشید.
پیاده سازی Header-based versioning
public class HeaderVersionConstraint : IHttpRouteConstraint { private const string VersionHeaderName = "api-version"; public HeaderVersionConstraint(int allowedVersion) { AllowedVersion = allowedVersion; } public int AllowedVersion { get; } public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection) { if (!request.Headers.Contains(VersionHeaderName)) return false; var version = request.Headers.GetValues(VersionHeaderName).FirstOrDefault(); return VersionToInt(version) == AllowedVersion; } private static int VersionToInt(string versionString) { int version; if (string.IsNullOrEmpty(versionString) || !int.TryParse(versionString, out version)) return 0; return version; } }
public sealed class HeaderVersionedRouteAttribute : RouteFactoryAttribute { public HeaderVersionedRouteAttribute(string template) : base(template) { Order = -1; } public int Version { get; set; } public override IDictionary<string, object> Constraints => new HttpRouteValueDictionary { {"", new HeaderVersionConstraint(Version)} }; }
[RoutePrefix("api/items")] public class ItemsController : ApiController { [ResponseType(typeof(ItemViewModel))] [HeaderVersionedRoute("{id}", Version = 1)] public IHttpActionResult Get(int id) { var viewModel = new ItemViewModel { Id = id, Name = "PS4", Country = "Japan" }; return Ok(viewModel); } } [RoutePrefix("api/items")] public class ItemsV2Controller : ApiController { [ResponseType(typeof(ItemV2ViewModel))] [HeaderVersionedRoute("{id}", Version = 2)] public IHttpActionResult Get(int id) { var viewModel = new ItemV2ViewModel { Id = id, Name = "Xbox One", Country = "USA", Price = 529.99 }; return Ok(viewModel); } }
پیاده سازی Media type-based versioning
GET /api/Items HTTP 1.1 Accept: application/vnd.mediatype.versioning-v1+json GET /api/Items HTTP 1.1 Accept: application/vnd.mediatype.versioning-v2+json
public class MediaTypeVersionConstraint : IHttpRouteConstraint { private const string VersionMediaType = "vnd.mediatype.versioning"; public MediaTypeVersionConstraint(int allowedVersion) { AllowedVersion = allowedVersion; } public int AllowedVersion { get; } public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection) { if (!request.Headers.Accept.Any()) return false; var acceptHeaderVersion = request.Headers.Accept.FirstOrDefault(x => x.MediaType.Contains(VersionMediaType)); //Accept: application/vnd.mediatype.versioning-v1+json if (acceptHeaderVersion == null || !acceptHeaderVersion.MediaType.Contains("-v") || !acceptHeaderVersion.MediaType.Contains("+")) return false; var version = acceptHeaderVersion.MediaType.Between("-v", "+"); return VersionToInt(version)==AllowedVersion; } }
اگر روش فوق را امتحان کنید (چه استفاده از افزونه یاد شده یا حتی روش دستی مقدماتی فوق)، هر نامی را که در ابتدای کار ایجاد Solution جدید وارد کنید، به زیر پروژههای اضافه شده اعمال نمیشود. یعنی همان نام ابتدایی خودشان را خواهند داشت که این مورد اصلا جالب نیست.
برای رفع آن نیاز است از متغیری به نام $safeprojectname$ استفاده شود (هرجایی که نام پروژه به صورت مستقیم استفاده شده، حتی نام پوشهها یا فایلها) به همراه ReplaceParameters=true. یک مثال را در این مورد در پیوست ذیل میتوانید دریافت کنید:
روش نصب دستی این قالب با کپی کردن آن در پوشه My Documents\Visual Studio xyz\Templates\ProjectTemplates است.
استفاده از bower در visual studio
- برای کار با bower در ویژوال استودیو فقط کافی است فایل استاندارد کانفیگ آنرا اضافه کنید:
سپس محتوای این فایل bower.json را به نحو ذیل تغییر داده و ذخیره کنید:
{ "name": "asp.net", "private": true, "dependencies": { "bootstrap-rtl":"" } }
- فایلهای نهایی که باید استفاده شوند، در پوشههای dist آن قرار دارند (distribution).
در این مورد خاص، نیاز خواهید داشت به فایلهای توزیع بوت استرپ اصلی و همچنین به بوت استرپ راست به چپ:
bower_components\bootstrap\dist\css\bootstrap.min.css bower_components\bootstrap-rtl\dist\css\bootstrap-rtl.min.css bower_components\jquery\dist\jquery.min.js bower_components\bootstrap\dist\js\bootstrap.min.js