کش خروجی HTML در ASP.NET Core با Redis

در برنامه های قبلی MVC ویژگی به نام OutputCache  در اختیار داشتیم که می توانستیم خروجی HTML تولید شده یک اکشن متد را به صورت لوکال کش کرده و کارآیی برنامه را افزایش دهیم.

در ASP.NET Core ویژگی با عنوان ResponseCache در اختیار داریم که با استفاده از آن صرفا می توانیم هدر Cache-Control را جهت کش کردن محتوا سمت پروکسی سرورهای واسط تنظیم کنیم. اما خبری از کش لوکال نیست. بنابراین باید خودمان دست به کار شده و آن را پیاده سازی کنیم.

در این مطلب ما از بانک اطلاعاتی NoSql مقیم در حافظه به نام Redis  که ساختار آن به شکل کلید/مقدار می باشد استفاده می کنیم. Redis در حال حاضر نسخه رسمی مخصوص ویندوز ندارد هرچند روش های برای نصب آن وجود دارد (redis-windows) اما شما برای شروع می توانید یک حساب رایگان در سایت RedisLab ایجاد کرده و مقدار 30Mb رم دریافت کنید که برای کار ما کافی می باشد.

راهکار ما ترکیبی از یک Middleware و به دنبال آن یک Attribute خواهد بود، برای شروع یک پروژه جدید ASP.NET Core Web Application ایجاد نمایید و بسته های زیر را نصب کنید:

PM> Install-Package Microsoft.Extensions.Caching.Memory -Version 2.0.0 
PM> Install-Package Microsoft.Extensions.Caching.Redis -Version 2.0.0

قبل از اینکه کدنویسی رو شروع کنیم ابتدا میپردازیم به اینکه راه حل ما شامل چه مراحلی خواهد بود

1- ایجاد یک ActionFilter سفارشی به نام Cache

2- این فیلتر، event های زیر را برای ما مدیریت خواهد کرد:

  • OnActionExecuting از اجرای درخواست در صورتی که پاسخ آن قبلا در کش وجود داشته باشد جلوگیری می کند و پاسخ قبلا کش شده را در خروجی درخواست جاری قرار میدهد.
  • OnResultExecuting پایان دادن به درخواست جاری در صورتی که پاسخ از کش خوانده شده باشد.
  • OnResultExecuted در صورتی که پاسخ در کش وجود نداشته آن را در کش ذخیره نماید.

3- یک میان افزار سفارشی که Stream درخواست را رهگیری کرده و یک MemoryStream را برای ما مهیا می کند که بعدا با استفاده از آن قادر خواهیم بود که در خروجی درخواست دستکاری کنیم.

پیاده سازی اینترفیس

از آنجا که قصد نداریم در کدهای خود وابستگی سخت بوجود آوریم، کار خود را با اینترفیس مرتبط با Caching شروع می کنیم که متدهای کلی مورد نیاز ما جهت کش اطلاعات را در اختیارمان قرار می دهد.

یک پوشه جدید به نام CacheToolkit ایجاد کرده و اینترفیس زیر را با نام ICacheService به آن اضافه کنید:

public interface ICacheService
{
    void Store(string key, object content);
    void Store(string key, object content, int duration);
    T Get<T>(string key) where T : class;
}

پیاده سازی این اینترفیس را بعدا انجام می دهیم.

یک پوشه جدید به نام FilterAttribute ایجاد کرده و کلاس CacheAttribute را مطابق زیر پیاده سازی کنید:

public class CacheAttribute : ResultFilterAttribute, IActionFilter
{
//...
}

پیاده سازی میان افزار سفارشی جهت کار با جریان خروجی

قبل از اینکه بتوانیم به پیاده سازی اکشن فیلتر مورد نظر خود بپردازیم نیاز به دسترسی به Stream خروجی درخواست داریم. به همین دلیل میان افزار CacheMiddleware  را به برنامه اضافه می کنیم. هر میان افزار به یک متد با امضای زیر احتیاج دارد:

public async Task Invoke(HttpContext httpContext)

در پیاده سازی میان افزار ها اینطور در نظر گرفته شده که هر میان افزار باید میان افزار بعدی را جهت تکمیل چرخه درخواست صدا بزند، به همین دلیل ما یک وابستگی به  RequestDelegate را از طریق متد سازنده به میان افزار خود اضافه می کنیم و سیستم تزریق وابستگی توکار Core کار تزریق را برای ما انجام خواهد داد.

پوشه جدیدی به نام Middlewares ایجاد کرده و و سپس کلاسی به نام CacheMiddleware به آن اضافه کنید.

کد های کامل میان افزار مطابق زیر خواهد بود:

public class CacheMiddleware
{
    protected RequestDelegate NextMiddleware;

    public CacheMiddleware(RequestDelegate nextMiddleware)
    {
        NextMiddleware = nextMiddleware;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        using (var responseStream = new MemoryStream())
        {
            var fullResponse = httpContext.Response.Body;
            httpContext.Response.Body = responseStream;
            await NextMiddleware.Invoke(httpContext);
            responseStream.Seek(0, SeekOrigin.Begin);
            await responseStream.CopyToAsync(fullResponse);
        }
    }
}

سپس جهت معرفی این میان افزار در فایل Startup و در متد Configure خط زیر را قبل از app.UseMvc() قرار دهید:

app.UseMiddleware<CacheMiddleware>();

فیلتر سفارشی Cache

به فیلتر سفارشی CacheAttribute باز گشته و فیلد های زیر را به آن اضافه کنید:

protected ICacheService CacheService { set; get; }
public int Duration { set; get; }

فیلد Duration صرفا جهت تعیین مدت زمان کش در صورتی که مایل به تغییر مقدار آن باشیم به کار خواهد رفت. درباره CacheService در ادامه صحبت خواهیم کرد.

متد OnResultExecuted را مطابق زیر پیاده سازی کنید:

public override void OnResultExecuted(ResultExecutedContext context)
{
    CacheService = context.HttpContext.RequestServices.GetService<ICacheService>();

    var cacheKey = context.HttpContext.Request.GetEncodedUrl().ToMd5();
    var httpResponse = context.HttpContext.Response;
    var responseStream = httpResponse.Body;

    responseStream.Seek(0, SeekOrigin.Begin);

    using (var streamReader = new StreamReader(responseStream, Encoding.UTF8, true, 512, true))
    {
        var toCache = streamReader.ReadToEnd();
        var contentType = httpResponse.ContentType;
        var statusCode = httpResponse.StatusCode.ToString();
        Task.Factory.StartNew(() =>
        {
            CacheService.Store(cacheKey + "_contentType", contentType, Duration);
            CacheService.Store(cacheKey + "_statusCode", statusCode, Duration);
            CacheService.Store(cacheKey, toCache, Duration);
        });

    }
    base.OnResultExecuted(context);
}

کار این متد چیست؟ ابتدا با استفاده از الگوی Service Locator دسترسی به CacheService را برای ما مهیا می کند، در ادامه کل جریان پاسخ درخواست را (که با استفاده از میان افزاری سفارشی ذکر شده امکان استفاده از آن را فراهم آوردیم) خوانده و پاسخ درخواست، نوع درخواست و کد وضعیت درخواست را در Cache ذخیره می کند. این پروسه در پس زمینه اجرا شده و هیچ عملیاتی به خاطر آن بلاک نخواهد شد. در آخر متد OnResultExecuted کلاس پایه فراخوانی شده و دیگر فیلتر های برنامه هم اعمال خواهند شد.

حال که پاسخ درخواست را کش کردیم نیاز به روشی داریم که درخواست را رهگیری کرده و پاسخ را در خروجی درخواست بنویسیم، متد OnActionExecuting را مطابق زیر پیاده سازی نمایید:

public void OnActionExecuting(ActionExecutingContext context)
{
    CacheService = context.HttpContext.RequestServices.GetService<ICacheService>();

    var requestUrl = context.HttpContext.Request.GetEncodedUrl();
    var cacheKey = requestUrl.ToMd5();
    var cachedResult = CacheService.Get<string>(cacheKey);
    var contentType = CacheService.Get<string>(cacheKey + "_contentType");
    var statusCode = CacheService.Get<string>(cacheKey + "_statusCode");
    if (!string.IsNullOrEmpty(cachedResult) && !string.IsNullOrEmpty(contentType) &&
        !string.IsNullOrEmpty(statusCode))
    {
        //cache hit
        var httpResponse = context.HttpContext.Response;
        httpResponse.ContentType = contentType;
        httpResponse.StatusCode = Convert.ToInt32(statusCode);

        var responseStream = httpResponse.Body;
        responseStream.Seek(0, SeekOrigin.Begin);
        if (responseStream.Length <= cachedResult.Length)
        {
            responseStream.SetLength((long)cachedResult.Length << 1);
        }
        using (var writer = new StreamWriter(responseStream, Encoding.UTF8, 4096, true))
        {
            writer.Write(cachedResult);
            writer.Flush();
            responseStream.Flush();
            context.Result = new ContentResult { Content = cachedResult };
        }
    }
    else
    {
        //cache miss
    }
}

اینبار به دنبال کلیدهای ایجاد شده از روی درخواست درون کش جستجو می کنیم و در صورتی که موارد پیدا شوند یعنی که پاسخ درخواست از قبل در کش موجود می باشد. در این صورت ما نوع محتوا Content Type و وضعیت Status Code را از کش مقدار دهی کرده و HTML خوانده شده از کش را در Stream خروجی می نویسیم. در نظر داشته باشید که مقداردهی context.Result بسیار مهم می باشد، از آنجا که پیاده سازی متد بعدی بر اساس آن خواهد بود:

public override void OnResultExecuting(ResultExecutingContext context)
{
    if (context.Result is ContentResult)
    {
        context.Cancel = true;
    }
}

این کد در صورتی که پاسخ درخواست از کش خوانده شده باشد ادامه پردازش آنرا متوقف می کند. (البته مطمئنا میتوان موارد بیشتری را جهت پشتیبانی از حالت های مختلف اضافه کرد اما در اینجا، هدف صرفا توضیح ایده کلی استفاده از کش است).

متد OnActionExecuted نیاز به پیاده سازی ندارد می توانید آنرا خالی رها کنید.

پیاده سازی سرویس Cache

تنها بخش باقی مانده پیاده سازی خود CacheService هست. اکنون همانطور که از عنوان مقاله مشخص است این پیاده سازی بر اساس پایگاه داده مقیم در حافظه Redis خواهد بود. البته در ادامه یک پیاده سازی از نوع MemoryCache هم انجام خواهد شد تا در صورتی که به سرور Redis دسترسی ندارید بتوانید از آن جهت تست استفاده کنید.

کلاس جدیدی به نام RedisCacheService در مسیر CacheToolkit ایجاد کنید:

public class RedisCacheService : ICacheService
{
    protected IDistributedCache Cache;
    private static int DefaultCacheDuration => 60;

    public RedisCacheService(IDistributedCache cache)
    {
        Cache = cache;
    }

    public void Store(string key, object content)
    {
        Store(key, content, DefaultCacheDuration);
    }

    public void Store(string key, object content, int duration)
    {
        string toStore;
        if (content is string)
        {
            toStore = (string)content;
        }
        else
        {
            toStore = JsonConvert.SerializeObject(content);
        }

        duration = duration <= 0 ? DefaultCacheDuration : duration;
        Cache.Set(key, Encoding.UTF8.GetBytes(toStore), new DistributedCacheEntryOptions()
        {
            AbsoluteExpiration = DateTime.Now + TimeSpan.FromSeconds(duration)
        });
    }

    public T Get<T>(string key) where T : class
    {
        var fromCache = Cache.Get(key);
        if (fromCache == null)
        {
            return null;
        }

        var str = Encoding.UTF8.GetString(fromCache);
        if (typeof(T) == typeof(string))
        {
            return str as T;
        }

        return JsonConvert.DeserializeObject<T>(str);
    }
}

این کلاس یک وابستگی از نوع IDistributedCache دارد که توسط خود ASP.NET Core در اختیار ما قرار خواهد گرفت، تنها کاری که ما باید برای استفاده از آن انجام دهیم مشخص سازی تنظیمات اولیه آن می باشد. به فایل Startup بازگشته و در متد ConfigureServices کد های زیر را اضافه کنید:

services.AddMemoryCache();

string redisConnection = Configuration["Redis:ConnectionString"],
    redisInstance = Configuration["Redis:InstanceName"];

services.AddSingleton<IDistributedCache>(factory =>
{
    var cache = new RedisCache(new RedisCacheOptions
    {
        Configuration = redisConnection,
        InstanceName = redisInstance,
    });

    return cache;
});

همانطور که مشاهده می کنید این کد از یک سری تنظیمات از پیش تعریف شده استفاده می کند، در برنامه های ASP.NET Core این تنظیمات در فایل appsettings.json ذخیره می شوند. این فایل را گشوده و تنظیمات زیر را را به آن اضافه کنید:

  "Redis": {
    "ConnectionString": "redis-10090.c12.us-east-1-4.ec2.cloud.redislabs.com:10090",
    "InstanceName": "mrpDb"
}

در صورتی که قصد استفاده از سرور ابری معرفی شده در ابتدای مطلب را دارید باید مقدار Endpoint را که پس از ثبت نام در اختیار شما قرار میدهد در اینجا جایگزین کنید.

آخرین قدم باقی مانده معرفی کردن CacheAttribute و RedisCacheService  به برنامه می باشد. درفایل Startup کدهای زیر را در همان قسمت ConfigureServices و بعد از کدهای قبلی مرتبط با تنظیمات Redis اضافه کنید:

services.AddSingleton<ICacheService, RedisCacheService>();
services.AddTransient<CacheAttribute>();

تست برنامه

خب، کار ما تمام شد؛ جهت تست برنامه، کدهای Home/Index.cshtml را با نمونه زیر جایگزین کنید:

@model dynamic

Server Time: @DateTime.Now
<br />
Local Time: 
<script type="text/javascript">
    document.write("<p>" + new Date().toLocaleString() + "</p>");
</script>

و فیلتر CacheAttribute را به HomeController و اکشن Index اضافه کنید:

[Cache(Duration = 30)]
public IActionResult Index()
{
  return View();
}

پروژه را اجرا کنید، مشاهده خواهید کرد که درخواست برای بار اول تمام چرخه درخواست را طی خواهد کرد، اما بار دوم پاسخ از کش خوانده و ارسال خواهد شد.

Cached Request Full Request
AspNetCoreRedisCacheSample AspNetCoreRedisCacheSample

تصویر کلیدهای ایجاد شده در Redis server:

Redis Desktop Manager

 استفاده از Memory به جای Redis جهت کش کردن HTML

از آنجا که طراحی خود را با استفاده از interface ها انجام دادیم، می توانیم به سادگی RedisCacheService را با یک پیاده سازی بر پایه In-Memory جایگزین کنیم. کدهای این کلاس را در زیر مشاهده می کنید:

public class MemoryCacheService : ICacheService
{
    protected IMemoryCache Cache;

    public MemoryCacheService(IMemoryCache cache)
    {
        Cache = cache;
    }

    public void Store(string key, object content)
    {
        Store(key, content, DefaultCacheDuration);
    }

    public void Store(string key, object content, int duration)
    {
        object cached;
        if (Cache.TryGetValue(key, out cached))
        {
            Cache.Remove(key);
        }

        Cache.Set(key, content,
            new MemoryCacheEntryOptions
            {
                AbsoluteExpiration = DateTime.Now + TimeSpan.FromSeconds(duration),
                Priority = CacheItemPriority.Low
            });
    }


    private static int DefaultCacheDuration => 60;

    public T Get<T>(string key) where T : class
    {
        object result;
        if (Cache.TryGetValue(key, out result))
        {
            return result as T;
        }
        return null;
    }
}

تنها کاری که برای جایگزینی باید بکنید، اضافه کردن خط زیر در فایل Startup است:

//services.AddSingleton<ICacheService, RedisCacheService>();
services.AddSingleton<ICacheService, MemoryCacheService>();

سورس کامل این مطلب را می توانید از مخزن گیت دانلود کنید.

پیاده سازی برنامه های چند مستاجری در ASP.NET Core

سناریویی را در نظر بگیرید که یک برنامه وب نوشته شده قرار است به چندین مستاجر (مشتری یا tenant) خدماتی ارائه کند، در این حالت اطلاعات هر مشتری به صورت کاملا جدا شده از دیگر مشتریان در سیستم قرار دارد و فقط به همان قسمت ها دسترسی دارد.

مثلا یک برنامه مدیریت رستوران را در نظر بگیرید که برای هر مشتری دامین مخصوص به خود قرار دارد و همه آنها به یک سیستم متمرکز متصل شده و اطلاعات خود را از آنجا دریافت می کنند.

در معماری Multi-Tenancy، چندین کاربر می‌توانند از یک نمونه (Single Instance) از اپلیکیشن نرم‌افزاری استفاده کنند. یعنی این نمونه روی سرور اجرا می‌شود و به چندین کاربر سرویس می‌دهد. هر کاربر را یک Tenant می‌نامیم. می‌توان به Tenantها امکان تغییر و شخصی‌سازی بخشی از اپلیکیشن را داد مثلا رنگ رابط کاربری یا قوانین کسب‌وکار، اما آنها نمی‌توانند کدهای اپلیکیشن را شخصی‌سازی کنند.

منبع: TipTech

بدون داشتن دانش کافی، پیاده سازی معماری multi tenant می تواند تبدیل یه یک چالش بزرگ شود. مخصوصا در نسخه قبلی ASP.NET که یکپارچه نبودن فریم ورک های مختلف می توانست باعث ایجاد چندین پیاده سازی مختلف در برنامه شود. موضوع وقتی پیچیده تر می شد که شما قصد داشتید در یک برنامه چندین فریم ورک مختلف مثل SignalR, MVC, Web API را مورد استفاده قرار بدهید.

خوشبختانه اوضاع با وجود 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

بعد از اینکار ما باید به SaasKit اطلاع بدیم که چطور مستاجرهای ما را شناسایی کند.

شناسایی مستاجر (tenant)

اولین جنبه در معماری multi-tenant شناسایی مستاجر بر اساس اطلاعات درخواست جاری می باشد که می تواند از hostname ، کاربر جاری یا یک HTTP header باشد.

ابتدا به تعریف کلاس مستاجر می پردازیم.

public class AppTenant
{
    public string Name { get; set; }
    public string[] Hostnames { get; set; }
}

سپس از طریق پیاده سازی اینترفیس ITenantResolver  و نوشتن یک tenant resolver به SaasKit اطلاع می دهیم که چطور مستاجر جاری رو بر اساس اطلاعات درخواست جاری شناسایی کند و در صورتی که موفق به شناسایی شود یک وهله از جنس  TenantContext<TTenant> بازگشت خواهد داد.

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;
    }
}

در نظر داشته باشید که اینجا ما اطلاعات مستاجر رو از روی hostname استخراج کردیم اما از آنجا که شما به شی HttpContext دسترسی کامل دارید می توانید از هر چیزی که مایل باشید استفاده کنید مثل URL، اطلاعات کاربر، هدر های HTTP و غیره. در اینجا فعلا مشخصات مستاجر های خودمون رو توی کد نوشتیم اما شما می توانید در برنامه خودتون این اطلاعات رو از یک فایل تنظیمات یا بانک اطلاعاتی دریافت کنید.

سیم کشی کردن

بعد از پیاده سازی این اینترفیس نوبت به سیم کشی های SaasKit میرسد، من در اینجا سعی کردم که مثل الگوی برنامه های ASP.NET Core عمل کنم. ابتدا نیاز داریم که وابستگی های SaasKit رو ثبت کنیم. فایل startups.cs  رو باز کنید و کدهای زیر را در متد ConfigureServices اضافه نمایید:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMultitenancy<AppTenant, AppTenantResolver>();
}

سپس باید میان افزار SaasKit رو ثبت کنیم، کدهای زیر رو به متد Configure اضافه کنید:

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;
    }
}

به عنوان مثال قصد داریم نام مستاجر را در عنوان سایت نمایش دهیم، برای اینکار ما از قابلیت جدید MVC 6 یعنی تزریق سرویس ها به View استفاده خواهیم کرد.

در فایل _Layout.cshtml تکه کد زیر را به بالای صفحه اضافه کنید:

@inject AppTenant Tenant;

این کد، AppTenant را برای ما در تمامی View ها از طریق شی Tenant قابل دسترس می کند. حالا می توانیم در View خود از جزییات مستاجر به شکل زیر استفاده کنیم:

<a asp-controller="Home" asp-action="Index" class="navbar-brand">@Tenant.Name</a>

اجرای نمونه مثال

فایل project.json را باز کنید و مقدار web را به شکل زیر مقدار دهی کنید: (در اینجا برای سایت خود دو آدرس را نگاشت کردیم)

"commands": {
  "web": "Microsoft.AspNet.Server.Kestrel --server.urls=http://localhost:6000;http://localhost:6001;http://localhost:6002",
},

سپس کنسول را در محل ریشه پروژه باز نموده و دستور زیر را اجرا کنید:

dnx web

حالا اگر در مرورگر خود آدرس http://localhost:6000 را وارد کنیم، مستاجر 1 را مشاهده می کنیم:

 

و اگر آدرس http://localhost:6001 را وارد کنیم، مستاجر 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; }
}

حالا نیاز داریم که به برنامه اعلام کنیم که تنظیمات مورد نیاز خود را از فایل appsettings.json بخواند، کد زیر را به ConfigureServices اضافه کنید:

services.Configure<MultitenancyOptions>(Configuration.GetSection("Multitenancy"));

سپس کدهای resolver خود را جهت دریافت اطلاعات از MultitenancyOptions مطابق زیر تغییر می دهیم:

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);
    }
}

برنامه را یکبار re-build کرده و اجرا کنید .

در آخر

اولین قدم در پیاده سازی یک معماری multi-tenant تصمیم گیری درباره این موضوع است که شما چطور مستاجر خود را شناسایی کنید، به محض این شناسایی شما می توانید عملیات های بعدی خود مثل تفکیک بخشی از برنامه، فیلتر کردن داده ای، نمایش یک view خاص برای هر مستاجر و یا بازنویسی قسمت های مختلف برنامه بر اساس هر مستاجر را انجام بدید.

_ سورس مثال بالا در گیت هاب قابل دریافت می باشد.

_ منبع: اینجا

آیا لازمه پروژه ASP.NET MVC خودتون رو به چند پروژه تقسیم کنید؟

این سوالی هست که ممکنه برای خیلی ها موقع کار روی یک پروژه MVC براشون پیش اومده باشه، اینکه یک پروژه رو به چند پروژه تقسیم بندی کنند؛ و جواب در یک کلام این هست که خیر!

دقیقا نمی دونم این داستان از کجا شروع شد اما برنامه نویس های زیادی رو دیدم که به دلایلی پروژه خودشون رو به چند پروژه دیگه تقسیم بندی می کنند، یک پروژه وب جهت لایه ارائه یا presentation، در کنار دو پروژه دیگه که معمولا با نام های MyProject.BLL و MyProject.DAL شناخته می شوند.

سطح بندی یا Tier !

خیلی اوقات اینطور فرض می شود که این لایه بندی باعث میگردد پروژه ی ما n-tier  و یا multi-tier شود، اصلا یک برنامه n-tier یعنی چی؟ به طور مختصر یعنی که یک برنامه از لحاظ فیزیکی جدا سازی شده باشه و روی چند کامپیوتر مختلف یا چند جای شبکه توزیع شده باشه. اغلب برنامه های تحت وب ذاتا n-tier هستند، از این جهت که ما در یک برنامه تحت وب سطوح زیر رو به صورت پیش فرض داریم:

1- لایه نمایش / سمت کلاینت: قسمت هایی که روی کامپیوتر کاربر اجرا می شوند.

2- لایه منطق / سمت سرور: قسمتی که توسط یک تکنولوژی سمت سرور (مثل ASP.NET MVC یا PHP) ساخته میشه و روی کامپیوتر سرور اجرا میشه.

3- لایه داده / بانک اطلاعاتی: شامل بانک اطلاعاتی، فایل سیستم یا هر نوع سیستم ذخیره سازی اطلاعات.

وقتی که داریم درباره ASP.NET MVC صحبت می کنیم، در واقع منظورمون همون قسمت سمت سرور هست، تقسیم بندی پروژه MVC به چند پروژه دیگه عملا باعث نمیشه که یک tier جدید به طراحیمون اضافه بشه. شما لایه DAL رو روی یک کامپیوتر دیگه قرار نمی دید. بیشتر اوقات (اگر نگیم همیشه) شما این سه پروژه رو یکجا کامپایل کرده و طی یک پروسه روی یک ماشین توسعه می دهید، ماشینی که همون وب سرور شماست. بنابراین وقتی که یه نفر از سایت شما بازدید می کنه، این سه dll با هم درون یک (یا چند) پروسه بارگذاری شده و توسط IIS پردازش می شوند.

لایه (Layer) و سطح (Tier)

لایه بندی و سطح بندی معمولا به یک منظور به کار برده می شوند اما این دو اساسا دو مفهوم متفاوت هستند. لایه ها جداسازی های منطقی هستند، سطوح جداسازی های فیزیکی هستند: یعنی توزیع کردن بخش های برنامه روی چند کامپیوتر مختلف. لایه یک مفهوم انتزاعی در ذهن برنامه نویس هست. یک class library یا یک پوشه یک لایه نیست، شما می تونید چند کلاس رو در یک پوشه و یا کلاس های لایه های مختلف برنامه رو در یک پوشه قرار بدید اما وابسته به یکدیگر باشند! این نشونه ای از یک طراحی بد و بهم پیچیده یا coupling هست. قرار دادن کلاس های مختلف در یک پوشه یا یک پروژه جداگانه به اسم DAL باعث نمیشه که شما ناگهان صاحب یک معماری تمیز و لایه بندی شده شوید.

البته منظور من این هست که این قسمت های DAL و BLL رو میشه (بهتره) که در همون پروژه وب قرارشون داد و صرفا انتقال اونها به یک پروژه ی دیگه هیچ ارزشی رو بهمون اضافه نمی کنه و به شکل جادویی باعث نمیشه که طراحیمون لایه بندی بشه.

اساسا به دو دلیل ما یک پروژه رو به چند پروژه تقسیم بندی می کنیم: قابلیت استفاده مجدد (reusability) و طراحی مستقل (independently deploying).

قابلیت استفاده مجدد (reusability)

یکی از علت های تقسیم یک پروژه به چند پروژه قابلیت استفاده مجدد هست. تا حالا برای من پیش نیامده و لازم نشده که بخوام از لایه های DAL و BLL یک برنامه در برنامه ی دیگه ای استفاده مجدد کنم، این یک ذهنیت قدیمی هست و در عمل برنامه های مدرن خیلی خاص هستند و من حتی در یک سازمان به خصوص هم ندیدم که پیش بیاد که لایه های یک برنامه بدرد یک برنامه دیگه در همون سازمان بخورند. خیلی اوقات کدهایی که شما در این لایه می نویسید به طور خاص برای همون برنامه نوشته می شوند تا منطق خاص اون برنامه رو به اجرا بگذارند و این چنین کدها و کلاس هایی رو نمیشه به راحتی طوری نوشت که دارای قابلیت استفاده مجدد باشند (اصلا اگر امکانش باشه!).

طراحی مستقل (independently deploying)

دلیل دیگه تقسیم بندی یک پروژه، طراحی مستقل هست، اگر شما قصد دارید که بخش های مختلف رو به شکل مستقل از هم توسعه و ورژن بندی کنید منطقی هست که بخواید این تقسیم بندی ها رو به کار ببرید. اما این کار معمولا برای فریم ورک ها مناسب هست و نه برنامه های سازمانی و باصطلاح Enterprise.

کتابخانه Entity Framework مثال خوبی توی این زمینه هست، این کتابخانه از چند اسمبلی مختلف تشکیل شده که هر کدوم امکانات خاصی رو ارائه می کنند. ما یک اسمبلی پایه داریم که شامل کدهای هسته کتابخانه هست، اسمبلی دیگه ای برای کار با SQL داریم و اسمبلی دیگه ای برای کار با SQLite و به همین منوال با این طراحی ما می تونیم فقط بخش هایی که لازم داریم رو دانلود کنیم و بهشون ارجاع بدیم.

تصور کنید که کل کتابخانه Entity Framework فقط یک اسمبلی بود، اونوقت ما با یک کتابخانه سنگین پر از کدهایی که بهشون احتیاج نداریم مواجه بودیم. همچنین هر موقع که تیم پشتیبانی یک قابلیت جدید اضافه، یا یک باگ رو رفع می کردند باید تمامی اسمبلی از ابتدا کامپایل شده و در اختیار برنامه نویسان قرار می گرفت. و در اون صورت این اسمبلی بسیار ناپایدار و شکننده  می شد. فرض کنیم شما دارید از این کتابخانه در کنار بانک اطلاعاتی SQL استفاده می کنید، چرا یک رفع ایراد در قسمت کار با بانک اطلاعاتی SQLite باید روی برنامه ما تاثیر بزاره؟ خب نباید بزاره و به همین دلیل این کتابخانه به شکل مستقل از هم و ماژولار طراحی شده است.

در اکثر پروژه های واقعی سازمانی، ما این لایه های DAL و BLL رو با هم نسخه بندی می کنیم پس انتقال اونها به سه اسمبلی مختلف سودی برای ما نداره.

سناریو های جداسازی فیزیکی

حالا سوال اینه که چه موقع ما واقعا نیاز داریم که پروژه مون رو از لحاظ فیزیکی به چند پروژه تقسیم بندی کنیم. چند تا سناریو رو بررسی می کنیم:

1- وجود چند لایه نمایش: فرض کنیم یه برنامه پردازش سفارش خرید داریم، این برنامه ویندوزی توسط کارمندان سازمان شما در حال استفاده هست و شما تصمیم می گیرید که برای این برنامه ویندوزی یه واسط تحت وب هم پیاده سازی کنید تا کارمندان بتونند از هرجایی با برنامه شما کار کنند. شما قصد دارید که از منطق تجاری و داده های موجود در پروژه جاری استفاده مجدد کنید و همینطور که قبلا اشاره کردم یکی از دلایل تقسیم سازی، قابلیت استفاده مجدد هست. بنابراین در این سناریو لازم هست که شما پروژه خودتون رو به سه پروژه تقسیم بندی کنید:

OrderProcessing.Core
OrderProcessing.Web
OrderProcessing.Desktop

توجه کنید که همینجا هم من دو لایه DAL و BLL رو ندارم و در واقع هر دوی اونها رو در یک پروژه OrderProcessing.Core کپسوله کرده ام.

خب چرا این پروژه رو به دو قسمت تقسیم نکردم (DAL و BLL)؟ چون تمام هدف DAL فراهم کردن نحوه ذخیره سازی اطلاعات برای لایه BLL هست، و خیلی بعید هست که این پروژه به تنهایی برای یک برنامه دیگه استفاده شود.

همچنین طبق اصل معکوس سازی وابستگی در طراحی شی گراء، وابستگی باید از DAL به BLL باشه و نه طور دیگه ای. و این به این معناست که هرجا که به به اسمبلی DAL ارجاعی وجود داره باید به BLL هم ارجاعی وجود داشته باشه. در واقع این دو به شدت در هم تنیده و غیر قابل جدا سازی هستند. وقتی که چیزهای منسجم رو از هم جدا می کنید، بعدا با مشکلات عجیب و غریبی مواجهه خواهید شد.

2- چند برنامه تحت یک پورتال: یک حالت دیگه هاست شدن چندین برنامه کوچک تحت یک پوتال هست که در نظر کاربر نهایی این سایت ها جدا نیستند و دامنه های (حوزه کاری) مختلف یک برنامه هستند. اما از نقطه نظر توسعه دهنده هر برنامه مستقل از دیگری هست. هر برنامه می تواند بانک اطلاعاتی خودش رو داشته باشه؛ یکی از Excel استفاده کنه یکی دیگه از SQL Server و دیگری از Oracle.

در این سناریو، احتمالا هر برنامه توسط تیم یا توسعه دهنده ی متفاوتی کار میشه و معمولا مستقل از هم توسعه/ورژن بندی/پیاده سازی می شوند. که می رسیم به  علت دوم جهت جدا سازی فیزیکی.

در این سناریو ما می تونیم پروژه مون رو به شکل زیر داشته باشیم:

OrderProcessing.Core (a class library)
Shipping.Core
CustomerSupport.Core
MainPortal (an ASP.NET MVC project)

یکباره دیگه، باز هم خبری از DAL و BLL نیست، هر لایه شامل پیاده سازی منطق برنامه و نحوه کار با داده در درون خودش هست.

 

خلاصه

  • لایه ها (Layer)، همون سطوح (Tier) نیستند.
  • سطح، یعنی جداسازی و توزیع فیزیکی یک نرم افزار روی کامپیوتر های مختلف.
  • لایه ها مفاهیم انتزاعی هستند، دقیقا یک مدل ارائه فیزیکی در کد نویسی برای اونها وجود نداره، داشتن یک پوشه یا یک اسمبلی با نام DAL به این معنی نیست که شما پروژه تون رو به خوبی لایه بندی کرده اید و یا قابلیت نگهداری (maintainability) رو افزایش داده اید.
  • قابلیت نگهدای یعنی کدهای تمیز، متد های کوچک، کلاس های کوچک که تنها یک وظیفه دارند و وابستگی محدود شده بین این کلاس ها. جدا سازی یک پروژه با کلی متد ها و کلاس های بزرگ به دو تا پروژه DAL/BLL هیچ قابلیت نگهداری رو در سیستم افزایش نمی دهد.
  • اسمبلی ها واحد های ورژن بندی و توسعه هستند.
  • در صورتی یک پروژه رو به چند پروژه تقسیم کنید که بخواهید از یک بخش بخصوص در پروژه ی دیگه ای استفاده کنید و یا قصد دارید به طور مستقل هر پروژه رو ورژن بندی و توسعه و منتشر کنید.

همیشه اصل KISS رو به خاطر داشته باشید و همه چیز رو ساده نگه دارید 🙂

 

منبع: programmingwithmosh