بررسی افزونه Bing Code Search
بررسی افزونه jPages
ASP.NET MVC #21
آیا استفاده از این افزونه http://awesome.codeplex.com/ رایگان است؟
معرفی افزونه CAT.NET
نه افزونه خاصی نصب نکردم !!! ؟
تعریف نوع جنریک به صورت متغیر
مطلبی را چندی قبل در مورد نحوه خودکار کردن افزودن کلاسهای EntityTypeConfiguration به modelBuilder در این سایت مطالعه کردید. در مطلب جاری به خودکار سازی تعاریف مرتبط با DbSetها خواهیم پرداخت.
ابتدا مثال کامل زیر را درنظر بگیرید:
using System; using System.Data.Entity; using System.Data.Entity.Migrations; using System.Linq; using System.Reflection; namespace MyNamespace { public abstract class BaseEntity { public int Id { set; get; } public string CreatedBy { set; get; } } public class User : BaseEntity { public string Name { get; set; } } public class MyContext : DbContext { protected override void OnModelCreating(DbModelBuilder modelBuilder) { var asm = Assembly.GetExecutingAssembly(); loadEntities(asm, modelBuilder, "MyNamespace"); } void loadEntities(Assembly asm, DbModelBuilder modelBuilder, string nameSpace) { var entityTypes = asm.GetTypes() .Where(type => type.BaseType != null && type.Namespace == nameSpace && type.BaseType.IsAbstract && type.BaseType == typeof(BaseEntity)) .ToList(); var entityMethod = typeof(DbModelBuilder).GetMethod("Entity"); entityTypes.ForEach(type => { entityMethod.MakeGenericMethod(type).Invoke(modelBuilder, new object[] { }); }); } } public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { context.Set<User>().Add(new User { Name = "name-1" }); context.Set<User>().Add(new User { Name = "name-2" }); context.Set<User>().Add(new User { Name = "name-3" }); base.Seed(context); } } public static class Test { public static void RunTests() { Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>()); using (var context = new MyContext()) { var user1 = context.Set<User>().Find(1); if (user1 != null) Console.WriteLine(user1.Name); } } } }
همانطور که ملاحظه میکنید در این مثال خبری از تعاریف DbSetها نیست. به کمک Reflection تمام مدلهای برنامه که از نوع کلاس پایه BaseEntity هستند (روشی مرسوم جهت مدیریت خواص تکراری مدلها) یافت شده (در متد loadEntities) و سپس نتیجه حاصل به صورت پویا به متد جنریک Entity ارسال میشود. حاصل، افزوده شدن خودکار کلاسهای مورد نظر به سیستم EF است.
البته در این حالت چون دیگر کلاسهای مدلها در MyContext به صورت صریح تعریف نمیشوند، نحوه استفاده از آنها را توسط متد Set، در متدهای RunTests و یا Seed، ملاحظه میکنید.
Custom type converters
همانطور که از اسمش مشخصه، زمانی کاربرد داره که نوع عضو یا اعضای یک شی در مبداء، با معادلشون در مقصد یکی نیستند. مثلا فرض کنید نوع Bool در مبداء رو میخواهیم به نوع String در مقصد نگاشت کنیم؛ همون Yes و No معروف بجای True یا False .
کلاسهای زیر رو در نظر بگیرید:
public class Source { public string Value1 { get; set; } public string Value2 { get; set; } public string Value3 { get; set; } } public class Destination { public int Value1 { get; set; } public DateTime Value2 { get; set; } public Type Value3 { get; set; } }
نکته: در تستی که من انجام دادم، AutoMapper تبدیل نوعهای ابتدایی رو خودش انجام میده؛ مثلا همین تبدیل Int به String رو!
یکی از روشهای مهیا کردن تبدیل کنندهی نوع، پیاده سازی اینترفیس ITypeConverter<TSource, TDestination> هست. تقریبا مثل کاری که در WPF و SL با پیاده سازی اینترفیس IValueConverter انجام میدادیم.
من برای تست از همون تبدیل نوع Bool به String استفاده میکنم و البته بخاطر ساده بودن دیگه Model ها رو نمینویسم.
ابتدا تعریف کلاس تبدیل کنندهی نوع:
public class BooltoStringTypeConvertor : ITypeConverter<bool, string> { public string Convert(ResolutionContext context) { return (bool)context.SourceValue ? "Yes" : "No"; } }
Mapper.CreateMap<bool,string>().ConvertUsing<BooltoStringTypeConvertor>(); Mapper.CreateMap<Product, ProductDto>(); Mapper.AssertConfigurationIsValid(); var product = new Product { Id = 1,Name ="PC" ,InStock = true }; var productDto = Mapper.Map<Product, ProductDto>(product);
نکته: TypeConvertorها میدان دیدشون سراسریه و نیازی نیست به ازای هر نگاشتی اونو به AutoMapper معرفی کنیم Global Scope.
Custom value resolvers
کلاسهای زیر رو در نظر بگیرید
public class Person { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } } public class PersonDTO { public int Id { get; set; } public string RawData { get; set; } }
یک روش برای این کار ارث بری از کلاس Abstract ی بنام ValueResolver<TSource, TDestination> هست.
public class CommaDelimetedResolver:ValueResolver<Person,string> { protected override string ResolveCore(Person source) { return string.Join(",", source.Id, source.FirstName, source.LastName); } }
Mapper.CreateMap<Person, PersonDTO>().ForMember( des => des.RawData, op => op.ResolveUsing<CommaDelimetedResolver>()); var person = new Person { Id = 1, FirstName = "Mohammad", LastName = "Saheb", }; var personDTO = Mapper.Map<Person, PersonDTO>(person);
نکته: توجه کنید این فقط یک مثال بود و این کار رو با روشهای دیگه هم میشه انجام داد مثلا MapFrom و...
نکته: میدان دید Value Resolverها سراسری نیست و باید به ازای هر نگاشتی اونو معرفی کنیم.
Custom Value Formatters
فرض کنید تاریخ رو در بانک، به صورت میلادی ذخیره کردهاید و میخواهید سمت View به صورت شمسی نمایش بدید. بنابراین در مبدا ویژگی بنام MiladiDate از نوع DateTime دارید و در مقصد ویژگی بنام ShamsiDate از نوع String. هنگام نگاشت، AutoMapper به صورت پیش فرض ToString رو فراخونی میکنه که بدرد ما نمیخوره و...
برای این کار میشه از Value Formatter استفاده کرد با پیاده سازی اینترفیس IValueFormatter.
public class ShamsiFormatter:IValueFormatter { public string FormatValue(ResolutionContext context) { return ToShamsi(context.SourceValue.ToString()); } }
Mapper.CreateMap<Person, PersonDTO>().ForMember( des => des.ShamsiDate, op => op.AddFormatter<ShamsiFormatter>());
سناریویی را در نظر بگیرید که یک برنامه وب نوشته شده، قرار است به چندین مستاجر (مشتری یا tenant) خدماتی را ارائه کند. در این حالت اطلاعات هر مشتری به صورت کاملا جدا شده از دیگر مشتریان در سیستم قرار دارد و فقط به همان قسمتها دسترسی دارد.
مثلا یک برنامه مدیریت رستوران را در نظر بگیرید که برای هر مشتری، در دامین
مخصوص به خود قرار دارد و همه آنها به یک سیستم متمرکز متصل شده و اطلاعات
خود را از آنجا دریافت میکنند.
در معماری Multi-Tenancy، چندین کاربر میتوانند از یک نمونه (Single
Instance) از اپلیکیشن نرمافزاری استفاده کنند. یعنی این نمونه روی سرور
اجرا میشود و به چندین کاربر سرویس میدهد. هر کاربر را یک Tenant
مینامیم. میتوان به Tenantها امکان تغییر و شخصیسازی بخشی از اپلیکیشن
را داد؛ مثلا امکان تغییر رنگ رابط کاربری و یا قوانین کسبوکار، اما آنها نمیتوانند
کدهای اپلیکیشن را شخصیسازی کنند.
خوشبختانه اوضاع با وجود OWIN بهتر شده و ما در این مطلب قصد استفاده از یک تولکیت را به نام SaasKit، برای پیاده سازی این معماری در ASP.NET Core داریم. هدف از این toolkit، سادهتر کردن هر چه بیشتر ساخت برنامههای SaaS (Software as a Service) هست. با استفاده از OWIN ما قادریم که بدون در نظر گرفتن فریم ورک مورد استفاده، رفتار مورد نظر خودمان را مستقیما در یک چرخه درخواست HTTP پیاده سازی کنیم و البته به لطف طراحی خاص ASP.NET Core 1.0 و استفاده از میان افزارهایی مشابه OWIN در برنامه، کار ما با SaasKit باز هم راحتتر خواهد بود.
شروع کار
یک پروژه ASP.NET Core جدید را ایجاد کنید و سپس ارجاعی را به فضای نام SaasKit.Multitenancy (موجود در Nuget) بدهید.PM> Install-Package SaasKit.Multitenancy
شناسایی مستاجر (tenant)
اولین جنبه در معماری multi-tenant، شناسایی مستاجر بر اساس اطلاعات درخواست جاری میباشد که میتواند از hostname ، کاربر جاری یا یک HTTP header باشد.ابتدا به تعریف کلاس مستاجر میپردازیم:
public class AppTenant { public string Name { get; set; } public string[] Hostnames { get; set; } }
public class AppTenantResolver : ITenantResolver<AppTenant> { IEnumerable<AppTenant> tenants = new List<AppTenant>(new[] { new AppTenant { Name = "Tenant 1", Hostnames = new[] { "localhost:6000", "localhost:6001" } }, new AppTenant { Name = "Tenant 2", Hostnames = new[] { "localhost:6002" } } }); public async Task<TenantContext<AppTenant>> ResolveAsync(HttpContext context) { TenantContext<AppTenant> tenantContext = null; var tenant = tenants.FirstOrDefault(t => t.Hostnames.Any(h => h.Equals(context.Request.Host.Value.ToLower()))); if (tenant != null) { tenantContext = new TenantContext<AppTenant>(tenant); } return tenantContext; } }
سیم کشی کردن
بعد از پیاده سازی این اینترفیس نوبت به سیم کشیهای SaasKit میرسد. من در اینجا سعی کردم که مثل الگوی برنامههای ASP.NET Core عمل کنم. ابتدا نیاز داریم که وابستگیهای SaasKit را ثبت کنیم. فایل startups.cs را باز کنید و کدهای زیر را در متد ConfigureServices اضافه نمایید:public void ConfigureServices(IServiceCollection services) { services.AddMultitenancy<AppTenant, AppTenantResolver>(); }
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { // after .UseStaticFiles() app.UseMultitenancy<AppTenant>(); // before .UseMvc() }
دریافت مستاجر جاری
حالا هر جا که نیاز به وهلهای از شیء مستاجر جاری داشتید، میتوانید به روش زیر عمل کنید:public class HomeController : Controller { private AppTenant tenant; public HomeController(AppTenant tenant) { this.tenant = tenant; } }
@inject AppTenant Tenant;
<a asp-controller="Home" asp-action="Index">@Tenant.Name</a>
اجرای نمونه مثال
فایل project.json را باز کنید و مقدار web را به شکل زیر مقدار دهی کنید: (در اینجا برای سایت خود 3 آدرس را نگاشت کردیم)"commands": { "web": "Microsoft.AspNet.Server.Kestrel --server.urls=http://localhost:6000;http://localhost:6001;http://localhost:6002", },
dotnet run
و اگر آدرس http://localhost:6002 را وارد کنیم، مستاجر 2 را مشاهده میکنیم:
قابل پیکربندی کردن مستاجر ها
از آنجائیکه نوشتن مشخصات مستاجرها در کد زیاد جالب نیست، برای همین
تصمیم داریم که این مشخصات را با استفاده از قابلیتهای ASP.NET Core از
فایل appsettings.json دریافت کنیم. تنظیمات مستاجرها را مطابق اطلاعات زیر به این فایل اضافه کنید:
"Multitenancy": { "Tenants": [ { "Name": "Tenant 1", "Hostnames": [ "localhost:6000", "localhost:6001" ] }, { "Name": "Tenant 2", "Hostnames": [ "localhost:6002" ] } ] }
public class MultitenancyOptions { public Collection<AppTenant> Tenants { get; set; } }
services.Configure<MultitenancyOptions>(Configuration.GetSection("Multitenancy"));
public class AppTenantResolver : ITenantResolver<AppTenant> { private readonly IEnumerable<AppTenant> tenants; public AppTenantResolver(IOptions<MultitenancyOptions> options) { this.tenants = options.Value.Tenants; } public async Task<TenantContext<AppTenant>> ResolveAsync(HttpContext context) { TenantContext<AppTenant> tenantContext = null; var tenant = tenants.FirstOrDefault(t => t.Hostnames.Any(h => h.Equals(context.Request.Host.Value.ToLower()))); if (tenant != null) { tenantContext = new TenantContext<AppTenant>(tenant); } return Task.FromResult(tenantContext); } }
در آخر
اولین قدم در پیاده سازی یک معماری multi-tenant، تصمیم گیری درباره این
موضوع است که شما چطور مستاجر خود را شناسایی کنید. به محض این شناسایی شما میتوانید عملیاتهای بعدی خود را مثل تفکیک بخشی از برنامه، فیلتر کردن دادهای، نمایش یک view خاص برای هر مستاجر و یا بازنویسی قسمتهای مختلف
برنامه بر اساس هر مستاجر، انجام دهید.
_ سورس مثال بالا در گیت هاب قابل دریافت میباشد.
_ منبع: اینجا