خودکار کردن تعاریف DbSetها در EF Code first
افزودن خودکار کلاسهای تنظیمات نگاشتها در EF Code first
<edmx:Edmx xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx" Version="4.0"> <edmx:DataServices> <Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="OwinAspNetCore.Models"> <EntityType Name="Product"> <Key> <PropertyRef Name="Id"/> </Key> <Property Name="Id" Type="Edm.Int32" Nullable="false"/> <Property Name="Name" Type="Edm.String"/> <Property Name="Price" Type="Edm.Decimal" Nullable="false"/> </EntityType> </Schema> <Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="Default"> <Function Name="TestFunction" IsBound="true"> <Parameter Name="bindingParameter" Type="Collection(OwinAspNetCore.Models.Product)"/> <Parameter Name="Val" Type="Edm.Int32" Nullable="false"/> <Parameter Name="Name" Type="Edm.String"/> <ReturnType Type="Edm.Int32" Nullable="false"/> </Function> <EntityContainer Name="Container"> <EntitySet Name="Products" EntityType="OwinAspNetCore.Models.Product"/> </EntityContainer> </Schema> </edmx:DataServices> </edmx:Edmx>
string postUrl = "http://localhost:port/...."; HttpClient client = new HttpClient(); var response = client.PostAsync(postUrl, new StringContent(JsonConvert.SerializeObject(new { Rating = 5 }), Encoding.UTF8, "application/json")).Result;
آن را نصب نموده و بعد از تکمیل شدن، visual studio را restart کنید.
پروژهی console خود را باز کرده و از طریق Add -> new item، آیتم OData client را جستجو کرده و با نام ProductClient.tt آن را تولید نمایید (نام آن اختیاری است):
فایل ProductClient.tt را که یک T4 code generator میباشد، باز کرده و مقدار ثابت MetadataDocumentUri را به آدرس سرویس odata خود تغییر دهید:
public const string MetadataDocumentUri = "http://localhost:port/odata/";
روی این آیتم کلیک راست و گزینهی Run Custom tool را انتخاب نمایید. این تمام کاری است که نیاز به انجام دادن دارید.
حال فایل Program.cs را باز کرده و آنرا اینگونه تغییر دهید:
using ConsoleApplication1.OwinAspNetCore.Models; using System; using System.Linq; namespace ConsoleApplication1 { public class Program { static void Main(string[] args) { Uri uri = new Uri("http://localhost:24977/odata"); //var context = new Default.Container(uri); var context = new TestNameSpace.TestNameSpace(uri); //get var products = context.Products.Where(pr => pr.Name.Contains("a")) .Take(1).Select(pr => new { Firstname = pr.Name, PriceValue = pr.Price }).ToList(); //add context.AddToProducts(new Product() { Name = "Name1", Price = 123 }); //update Product p = context.Products.First(); p.Name = "changed"; context.UpdateObject(p); //delete context.DeleteObject(context.Products.Last()); //commit context.SaveChanges(); } } }
مشاهده میفرمایید که همهی عملیاتهای لازم برای CRUD، به شرط اینکه در سمت سرور طراحی شده باشند، به راحتی از سمت کلاینت قابل فراخوانی خواهند بود.
از این ویژگی فوق العاده میتوان حتی در کلاینتها جاوااسکریپتی نیز استفاده کرد. فرض کنید نرم افزار تحت وبی را با استفاده از jquery یا angularjs طراحی کردهاید. قاعدتا فراخوانی درخواستهای شما به سمت سرور، چیزی شبیه به این خواهد بود:
//angularjs $http.get("/products/get", {Name: "Test", Company: "Test"}) .then(function(response) { console.log(response.data); }); //jquery $.get("/products/get", {Name: "Test", Company: "Test"}, function(data, status){ console.log("Data: " + data); });
با استفاده از odata و typescript و یک library مربوط به odata client در سمت کلاینت، نرم افزار شما بجای موارد، بالا چیزی شبیه به مثال زیر خواهد بود (با همراه داشتن strongly typed و intellisense کامل)
let product1 = await context.products.filter(c => c.Name.contains("Ali")).toArray(); let product2 = await context.products.getSomeFunction(1, 'Test'); context.product.add({Name: 'Test'} as Product); await context.saveChanges()
در مقالههای آتی به ویژگیهای بیشتری از Odata خواهیم پرداخت.
public class HomeViewModel { public string Id { get; set; } public string Message { get; set; } public DateTime DateTime { get; set; } }
اکنون به پوشهی Views بروید و فایل Index.cshtml را به این صورت تغییر دهید:
@model AspNetCoreDependencyInjection.Models.HomeViewModel @{ ViewData["Title"] = "Home"; } <div> <div> <div> <p> <b>Id : </b><span>@Model.Id</span> <br /> <b>Date And Time : </b><span> @Model.DateTime </span> <br/> <b>Message : </b><span>@Model.Message</span> </p> </div> </div> </div>
using AspNetCoreDependencyInjection.Services; namespace AspNetCoreDependencyInjection.ServicesImplentaions { public class MessageServiceAA { public string Message() { return "A message from MessageServiceAA"; } } }
namespace AspNetCoreDependencyInjection.Helpers { public class GuidProvider { private readonly Guid _serviceGuid; public GuidProvider() { _serviceGuid = Guid.NewGuid(); } public Guid GetNewGuid() => Guid.NewGuid(); public string GetGuidAsFormatedString(string prefix = "") => getFormatedGuid(_serviceGuid, prefix); private string getFormatedGuid(Guid guid, string prefix = "") { var guidString = guid.GetHashCode().ToString("x"); if (string.IsNullOrEmpty(prefix) == false) guidString = new StringBuilder($"{prefix}-").Append(guidString).ToString(); return guidString; } } }
حالا درون کنترل HomeController، این تغییرات را انجام میدهیم:
private readonly ILogger<HomeController> _logger; private readonly MessageServiceAA _messageService; private readonly GuidProvider _ guidProvider; public HomeController(ILogger<HomeController> logger) { _logger = logger; _messageService = new MessageServiceAA(); _guidProvider = new GuidProvider(); } public IActionResult Index() { var model = new HomeViewModel() { Id = _ guidProvider.GetGuidAsFormatedString(), Message = _messageService.Message(), DateTime = DateTime.Now, }; return View(model); }
همانطور که میبینید، در کد بالا، کنترلر HomeController، به دو شیء از کلاسها و یا سرویسهای GuidProvider و MessageServiceAA به صورت مستقیم وابسته شدهاست و با هر تغییری در هر کدام از این سرویسها، باید دوباره کامپایل شود. علاوه بر این اگر بخواهیم پیاده سازیهای مختلفی را برای هر کدام از این موارد، ارائه دهیم، به مشکل بر میخوریم. خب بیاید تغییراتی را در کد بالا بدهیم تا مشکلات ذکر شده را حل کنیم.
برای این منظور پوشهای را به نام Services میسازیم و اینترفیسی را
به نام IMessageBrokerA ایجاد میکنیم و سپس کاری میکنیم که MessageServiceAA از این
اینترفیس ارث بری کند:
namespace AspNetCoreDependencyInjection.Services { public interface IMessageServiceA { string Message(); } }
و حالا میخواهیم با
استفاده از تزریق وابستگی، وابستگی کنترلر HomeController را از کلاس MessageBrokerAA لغو کرده و آن را به اینترفیس IMessageBrokerA (انتزاع) وابسته کنیم. در
اینجا ما از تکنیک تزریق درون سازنده یا Constructor Injection استفاده میکنیم.
تزریق درون سازنده
در این تکنیک، ما لیستی از وابستگیهای مورد نیاز را به عنوان پارامترهای ورودی سازندهی کلاس، تعریف میکنیم:private readonly ILogger<HomeController> _logger; private readonly IMessageServiceA _messageService; private readonly GuidProvider _guidHelper; public HomeController(ILogger<HomeController> logger , IMessageServiceA messageService) { _logger = logger; _messageService = messageService; _messageService = new MessageServiceAA(); _guidHelper = new GuidProvider(); }
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // کدها جهت خوانایی بیشتر مخفی شده اند } }
همانطورکه میبینید، متد ConfigureService پارامتر IServiceCollection را میگیرد که به وسیلهی WebHost در زمان اجرای برنامه، مقدار دهی میشود.
تعداد زیادی Extension method برای IServiceCollection وجود دارند که برای پشتیبانی از ثبت کردن سرویسهای مختلف در سناریوهای گوناگون به کار میروند. در اینجا ما از نسخهی 3.1 چارچوب ASP.NET Core استفاده میکنیم. برای همین هم برای ثبت سرویسهای پیش فرض فریمورک MVC از متد توسعهی services.AddControllersWithViews() استفاده میکنیم. متد توسعهی AddControllersWithViews() سرویسهایی را که معمولا در فریم ورک MVC استفاده میشوند، درون IServiceCollection ثبت میکند. در نسخههای قبلی چارچوب ASP.NET Core، مانند نسخههای 2.1 و 2.2 برای این کار از متد توسعهی AddMvc() استفاده میشد.
در Microsoft Dependency Injection Container ، معمولا ترتیب ثبت سرویسها مهم نیست.
خب، اولین سرویس اختصاصی برنامهی خودمان را با چرخهی حیات Transient و زیر سرویس پیشین، به شکل زیر ثبت میکنیم :
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); services.AddTransient<IMessageServiceA, MessageServiceAA>(); }
public static IServiceCollection AddTransient<TService, TImplementation>(this IServiceCollection services)
در اینجا وقتی ما برای IMessageServiceA ، پیاده سازی MessageServiceA را ثبت میکنیم، از این به بعد DI Container، هر زمانیکه در لیست پارامترهای سازندهی یک کلاس، IMessageServiceA را مشاهده کند، بررسی میکند که چه کلاسی به عنوانی پیاده سازی این اینترفیس ثبت شدهاست، سپس از آن نمونه سازی میکند و درون سازندهی مورد نظر تزریق میکند. خب، حالا برنامه را دوباره اجرا کنید؛ میبینید که برنامه اجرا میشود.
در این تصویر اطلاعات کاملی از مکانیزم ذخیره سازی دادهها در پلتفرم اندروید مشاهده میکنید.
ما میتوانیم دادههای خود را در یک بانک از نوع SQLite در نظر بگیریم. پایگاهداده به این دلیل، ما را از تصمیم غیرضروری برای اجرای ساختار بی نظم دادهها بینیاز میکند. بیایید به یک مثال از نحوه ذخیره و بازیابی دادهها با استفاده از یکی از این مکانیزمها نگاه کنیم.
Shared Preferences یا اولویتهای اشتراکی
اولویتهای مشترک عمدتا برای ذخیرهسازی تنظیمات برنامهها مفید هستند و تا زمانی که راهاندازی مجدد توسط دستگاه انجام شود، معتبر خواهد بود. بیایید بگوییم که باید اطلاعات مربوط به یک سرور ایمیل را ذخیره کنیم که برنامه ما برای بازیابی دادهها به آن نیاز دارد.
ما باید نام میزبان ایمیل (hostname) , درگاه (port) و کارگزاری که از SSL استفاده میکند را ذخیره کنیم. کلاس زیر این کار را برای ما انجام میدهد به قطعه کد زیر توجه نمایید:
package net.zenconsult.android; import java.util.Hashtable; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.preference.PreferenceManager; public class StoreData { public static boolean storeData(Hashtable data, Context ctx) { SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(ctx); String hostname = (String) data.get("hostname"); int port = (Integer) data.get("port"); boolean useSSL = (Boolean) data.get("ssl"); Editor ed = prefs.edit(); ed.putString("hostname", hostname); ed.putInt("port", port); ed.putBoolean("ssl", useSSL); return ed.commit(); } }
package net.zenconsult.android; import java.util.Hashtable; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; public class RetrieveData { public static Hashtable get(Context ctx) { String hostname = "hostname"; String port = "port"; String ssl = "ssl"; Hashtable data = new Hashtable(); SharedPreferences prefs = PreferenceManager .getDefaultSharedPreferences(ctx); data.put(hostname, prefs.getString(hostname, null)); data.put(port, prefs.getInt(port, 0)); data.put(ssl, prefs.getBoolean(ssl, true)); return data; } }
package net.zenconsult.android; import java.util.Hashtable; import android.app.Activity; import android.content.Context; import android.os.Bundle; import android.util.Log; import android.widget.EditText; public class StorageExample1Activity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); Context cntxt = getApplicationContext(); Hashtable data = new Hashtable(); data.put("hostname", "smtp.gmail.com"); data.put("port", 587); data.put("ssl", true); if (StoreData.storeData(data, cntxt)) Log.i("SE", "Successfully wrote data"); else Log.e("SE", "Failed to write data to Shared Prefs"); EditText ed = (EditText) findViewById(R.id.editText1); ed.setText(RetrieveData.get(cntxt).toString()); } }
public class HomeController : Controller { public IActionResult GetFile() { return PhysicalFile(@"C:\path\file.pdf", "application/pdf"); }
var result = new FileStreamResult(readStream, contentType) { LastModified = lastModified, EntityTag = entityTag, EnableRangeProcessing = true, }; return PhysicalFile(path, "text/plain", "downloadName.txt", lastModified, entityTag, true); return File(data, "text/plain", "downloadName.txt", lastModified: null, entityTag: entityTag, enableRangeProcessing: true);
PM> Install-Package Twitter.Bootstrap.Less
.loud { color: red; } // Make all H1 elements loud h1 { .loud; }
<div class="container"> <div class="row"> <div class="col-md-8"> Content - Main </div> <div class="col-md-4"> Content - Secondary </div> </div> </div>
<div class="wrapper"> <div class="content-main"> Content - Main </div> <div class="content-secondary"> Content - Secondary </div> </div>
// Core variables and mixins @import "variables.less"; @import "mixins.less"; .wrapper { .make-row(); } .content-main { .make-lg-column(8); } .content-secondary { .make-lg-column(3); .make-lg-column-offset(1); }
.wrapper { margin-left: -15px; margin-right: -15px; } .content-main { position: relative; min-height: 1px; padding-left: 15px; padding-right: 15px; } @media (min-width: 1200px) { .content-main { float: left; width: 66.66666666666666%; } } .content-secondary { position: relative; min-height: 1px; padding-left: 15px; padding-right: 15px; } @media (min-width: 1200px) { .content-secondary { float: left; width: 25%; } } @media (min-width: 1200px) { .content-secondary { margin-left: 8.333333333333332%; } }
<!-- Before --> <a href="#" class="btn danger large">Click me!</a> <!-- After --> <a href="#" class="annoying">Click me!</a> a.annoying { .btn; .btn-danger; .btn-large; }
عموما برای درج فایلهای ثابت اسکریپتها و شیوهنامههای سایت، از روش متداول زیر استفاده میشود:
<link rel="stylesheet" href="/css/site.css" /> <script src="/js/site.js"></script>
مشکلی که به همراه این روش وجود دارد، مطلع سازی کاربران و مرورگر، از تغییرات آنهاست؛ چون این فایلهای ثابت، توسط مرورگرها کش شده و با فشردن دکمههایی مانند Ctrl+F5 و بهروز شدن کش مرورگر، به نگارش جدید، ارتقاء پیدا میکنند. برای رفع این مشکل حداقل دو روش وجود دارد:
الف) هربار نام این فایلها را تغییر دهیم. برای مثال بجای نام قدیمی site.css، از نام جدید site.v.1.1.css استفاده کنیم.
ب) یک کوئری استرینگ متغیر را به نام ثابت این فایلها، اضافه کنیم.
که در این بین، روش دوم متداولتر و معقولتر است. برای این منظور، ASP.NET Core به همراه ویژگی توکاری است به نام asp-append-version که اگر آنرا به تگهای اسکریپت و link اضافه کنیم:
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> <script src="~/js/site.js" asp-append-version="true"></script>
این کوئری استرینگ را به صورت خودکار محاسبه کرده و به آدرس فایل درج شده اضافه میکند؛ با خروجیهایی شبیه به مثال زیر:
<link rel="stylesheet" href="/css/site.css?v=AAs5qCYR2ja7e8QIduN1jQ8eMcls-cPxNYUozN3TJE0" /> <script src="/js/site.js?v=NO2z9yI9csNxHrDHIeTBBfyARw3PX_xnFa0bz3RgnE4"></script>
ASP.NET Core در اینجا هش فایلهای یافت شده را با استفاده از الگوریتم SHA256 محاسبه و url encode کرده و به صورت یک کوئری استرینگ، به انتهای آدرس فایلها اضافه میکند. به این ترتیب با تغییر محتوای این فایلها، این هش نیز تغییر میکند و مرورگر بر این اساس، همواره آخرین نگارش ارائه شده را از سرور دریافت خواهد کرد. نتیجهی این محاسبات نیز به صورت خودکار کش میشود و همچنین با استفاده از یک File Watcher در پشت صحنه، تغییرات این فایلها هم بررسی میشوند. یعنی اگر فایلی تغییر کرد، نیازی به ریاستارت برنامه نیست و محاسبات جدید و کش شدن مجدد آنها، به صورت خودکار انجام میشود.
البته این ویژگی هنوز به Blazor اضافه نشدهاست؛ اما امکان استفادهی از زیر ساخت ویژگی asp-append-version با کدنویسی مهیا است که در ادامه با استفاده از آن، کامپوننتی را مخصوص Blazor SSR، تهیه میکنیم.
دسترسی به زیر ساخت محاسباتی ویژگی asp-append-version با کدنویسی
زیرساخت محاسباتی ویژگی asp-append-version، با استفاده از سرویس توکار IFileVersionProvider به صورت زیر قابل دسترسی است:
public static class FileVersionHashProvider { private static readonly string ProcessExecutableModuleVersionId = Assembly.GetEntryAssembly()!.ManifestModule.ModuleVersionId.ToString("N"); public static string GetFileVersionedPath(this HttpContext httpContext, string filePath, string? defaultHash = null) { ArgumentNullException.ThrowIfNull(httpContext); var fileVersionedPath = httpContext.RequestServices.GetRequiredService<IFileVersionProvider>() .AddFileVersionToPath(httpContext.Request.PathBase, filePath); return IsEmbeddedOrNotFound(fileVersionedPath, filePath) ? QueryHelpers.AddQueryString(filePath, new Dictionary<string, string?>(StringComparer.Ordinal) { { "v", defaultHash ?? ProcessExecutableModuleVersionId } }) : fileVersionedPath; } private static bool IsEmbeddedOrNotFound(string fileVersionedPath, string filePath) => string.Equals(fileVersionedPath, filePath, StringComparison.Ordinal); }
در برنامههای Blazor SSR، دسترسی کاملی به HttpContext وجود دارد و همانطور که مشاهده میکنید، این سرویس نیز به اطلاعات آن جهت محاسبهی هش فایل معرفی شدهی به آن، نیاز دارد. در اینجا اگر هش قابل محاسبه نبود، از هش فایل اسمبلی جاری استفاده خواهد شد.
ساخت کامپوننتهایی برای درج خودکار هش فایلهای اسکریپتها
یک نمونه روش استفادهی از متد الحاقی GetFileVersionedPath فوق را در کامپوننت DntFileVersionedJavaScriptSource.razor زیر میتوانید مشاهده کنید:
@if (!string.IsNullOrWhiteSpace(JsFilePath)) { <script src="@HttpContext.GetFileVersionedPath(JsFilePath)" type="text/javascript"></script> } @code{ [CascadingParameter] public HttpContext HttpContext { set; get; } = null!; [Parameter] [EditorRequired] public required string JsFilePath { set; get; } }
با استفاده از HttpContext مهیای در برنامههای Blazor SSR، متد الحاقی GetFileVersionedPath به همراه مسیر فایل js. مدنظر، در صفحه درج میشود.
برای مثال یک نمونه از استفادهی آن، به صورت زیر است:
<DntFileVersionedJavaScriptSource JsFilePath="/lib/quill/dist/quill.js"/>
در نهایت با اینکار، یک چنین خروجی در صفحه درج خواهد شد که با تغییر محتوای فایل quill.js، هش متناظر با آن به صورت خودکار بهروز خواهد شد:
<scriptsrc="/lib/quill/dist/quill.js?v=5q7uUOOlr88Io5YhQk3lgYcoB_P3-5Awq1lf0rRa7-Y" type="text/javascript"></script>
شبیه به همین کار را برای شیوهنامهها هم میتوان تکرار کرد و کدهای آن، تفاوت آنچنانی با کامپوننت فوق ندارند.
string destinationFilePath = ("d:\\temp\\1.jpg"); GhostscriptWrapper.GenerateOutput("d:\\temp\\1.pdf", destinationFilePath, new GhostscriptSettings { Device = GhostscriptDevices.jpeg, Page = new GhostscriptPages { Start = 1, End = 1, AllPages = true, }, Resolution = new Size { Height = 150, Width = 150 }, Size = new GhostscriptPageSize { Native = GhostscriptPageSizes.a4 } });
@Html.ActionLink("About us", "Index", "About")
<a href="/About">About us</a>
@using MvcApplication5.Models
@model MvcApplication5.Models.Products
@{
ViewBag.Title = "Index";
}
@helper GetProductsList(List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li>@Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber })</li>
}
</ul>
}
<h2>Index</h2>
@GetProductsList(@Model)
http://localhost/Home/Details/D123
using System.Linq;
using System.Web.Mvc;
using MvcApplication5.Models;
namespace MvcApplication5.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
var products = new Products();
return View(products);
}
public ActionResult Details(string id)
{
var product = new Products().FirstOrDefault(x => x.ProductNumber == id);
if (product == null)
return View("Error");
return View(product);
}
}
}
@model MvcApplication5.Models.Product
@{
ViewBag.Title = "Details";
}
<h2>Details</h2>
<fieldset>
<legend>Product</legend>
<div class="display-label">ProductNumber</div>
<div class="display-field">@Model.ProductNumber</div>
<div class="display-label">Name</div>
<div class="display-field">@Model.Name</div>
<div class="display-label">Price</div>
<div class="display-field">@String.Format("{0:F}", Model.Price)</div>
</fieldset>
<p>
@Html.ActionLink("Edit", "Edit", new { /* id=Model.PrimaryKey */ }) |
@Html.ActionLink("Back to List", "Index")
</p>
'System.Web.WebPages.Html.HtmlHelper' does not contain a definition for 'ActionLink'
@using System.Web.Mvc
@using System.Web.Mvc.Html
@using MvcApplication5.Models
@helper GetProductsList(WebViewPage page, List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li> @page.Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber })</li>
}
</ul>
}
@MyHelpers.GetProductsList(this, @Model)
http://localhost/Home/Details/%D9%85%D9%82%D8%AF%D8%A7%D8%B1%20%D9%8A%D9%83
<li><a href="/Home/Details/مقدار یک">Super Fast Bike</a></li>
@helper EmitCleanUnicodeUrl(MvcHtmlString data)
{
@Html.Raw(HttpUtility.UrlDecode(data.ToString()))
}
@helper GetProductsList(List<Product> products)
{
<ul>
@foreach (var item in products)
{
<li>@EmitCleanUnicodeUrl(@Html.ActionLink(item.Name, "Details", new { id = item.ProductNumber }))</li>
}
</ul>
}