لیستی از منابع DDD
مجموعه Awesome Domain-Driven Design
کدهای این قسمت بهروزرسانی شده و از این ریپازیتوری قابل دسترسی است.
Event Sourcing
در این قسمت قصد داریم تا اطلاعات Commandهای خود را بعد از Process، داخل یک دیتابیس Append-Only ذخیره کنیم. با استفاده از این روش میتوانیم بفهمیم در یک تاریخ مشخص، با چه ورودیهایی ( Request )، چه جواب ( Response ) ای در آن لحظه از برنامه برگشت داده شدهاست.
برای پیاده سازی Event Sourcing از دیتابیس EventStore که سورس آن نیز در گیتهاب قابل دسترسی است، استفاده خواهیم کرد. توجه داشته باشید که شما میتوانید از دیتابیسهای دیگری مثل Elasticsearch, Redis و ... بهمنظور دیتابیس Event Store خود استفاده کنید و محدود به EventStore نیستید.
ما برای راه اندازی دیتابیس EventStore در این قسمت، از Docker استفاده خواهیم کرد. آموزش Docker قبلا طی مقالاتی (2 , 1) در سایت قرار گرفتهاست و در این مقاله به تکرار نحوه استفاده از آن نخواهیم پرداخت.
با استفاده از دستور زیر، EventStore را از روی Docker Hub که Registry پیشفرض است، Pull و اجرا میکنیم و پورتهای 2113 و 1113 آن را به بیرون Expose میکنیم تا داخل برنامه خود، از آنها استفاده کنیم:
docker run --name eventstore-node -d -p 2113:2113 -p 1113:1113 eventstore/eventstore
EventStore دارای پنل ادمینی است که از طریق http://localhost:2113 قابل دسترسی است. Username پیشفرض آن برابر با admin و کلمه عبور آن برابر با changeit است.
بعد از لاگین در پنل ادمین، با چنین Dashboard ای مواجه خواهید شد و نشان از این دارد که EventStore بهدرستی اجرا شده است:
برای استفاده از EventStore داخل برنامه خود، مانند دیگر دیتابیسها، Client موجود آن را برای #C، از NuGet نصب میکنیم:
Install-Package EventStore.Client
سپس کلاسی بنام EventStoreDbContext ایجاد و منطق ارتباط با EventStore را داخل آن قرار میدهیم :
public class EventStoreDbContext : IEventStoreDbContext { public async Task<IEventStoreConnection> GetConnection() { IEventStoreConnection connection = EventStoreConnection.Create( new IPEndPoint(IPAddress.Loopback, 1113), nameof(MediatrTutorial)); await connection.ConnectAsync(); return connection; } public async Task AppendToStreamAsync(params EventData[] events) { const string appName = nameof(MediatrTutorial); IEventStoreConnection connection = await GetConnection(); await connection.AppendToStreamAsync(appName, ExpectedVersion.Any, events); } }
همانطور که میبینید، با استفاده از IP 1113 که در بالاتر با استفاده از Docker آن را Expose کرده بودیم، به EventStore متصل شدهایم. همچنین برای متد AppendToStreamAsync خود EventStore ، یک Facade نوشتهایم که نحوه کار با آن را برایمان راحتتر کردهاست.
با توجه به اینکه EventStore در Documentation خود بیان کرده که Thread-Safe است، در DI Container خود، EventStoreDbContext را بصورت Singleton ثبت و Register میکنیم و در طول عمر برنامه، یک instance از آن خواهیم داشت:
services.AddSingleton<IEventStoreDbContext, EventStoreDbContext>();
قصد داریم Request هایی را که از نوع Command هستند، همراه با Response آنها داخل EventStore ذخیره کنیم. برای تشخیص Query/Command بودن یک Request ، از نام آنها استفاده خواهیم کرد. همانطور که در قسمتهای قبل گفتیم ، Commandها باید با ذکر "Command" در پایان نامشان همراه باشند.
این یک Convention در برنامه ماست که باید رعایت شود. ( Convention Over Configuration )
مانند Behaviorهای قبلی، یک Behavior جدید را بنام EventLoggerBehavior ایجاد و از IPipelineBehavior ارث بری کرده و EventStoreDbContext خود را به آن Inject میکنیم:
public class EventLoggerBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { readonly IEventStoreDbContext _eventStoreDbContext; public EventLoggerBehavior(IEventStoreDbContext eventStoreDbContext) { _eventStoreDbContext = eventStoreDbContext; } public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next) { TResponse response = await next(); string requestName = request.ToString(); // Commands convention if (requestName.EndsWith("Command")) { Type requestType = request.GetType(); string commandName = requestType.Name; var data = new Dictionary<string, object> { { "request", request }, { "response", response } }; string jsonData = JsonConvert.SerializeObject(data); byte[] dataBytes = Encoding.UTF8.GetBytes(jsonData); EventData eventData = new EventData(eventId: Guid.NewGuid(), type: commandName, isJson: true, data: dataBytes, metadata: null); await _eventStoreDbContext.AppendToStreamAsync(eventData); } return response; } }
با استفاده از این Behavior، فقط Request هایی را که Command هستند و State برنامه را تغییر میدهند، داخل EventStore ذخیره میکنیم. اکنون کافیست تا این Behavior را داخل DI Container خود اضافه کنیم :
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(EventLoggerBehavior<,>));
اگر برنامه را اجرا و یکی از Commandها را مانند CreateCustomerCommand، با استفاده از api/Customers <= POST فراخوانی کنید، Request و Response شما با Type آن Command و همراه با DateTime ای که این Request رخ دادهاست، داخل EventStore ذخیره خواهد شد که در Admin Panel مربوط به EventStore، در تب Stream Browser قابل مشاهده است :
سه مطلب کوتاه
مشکل فایرفاکس با سایتهای msdn و codeplex
هر از چند گاهی در بلاگهای msdn و یا سایت codeplex با خطای زیر از طرف سایت مواجه میشوم:
Bad Request - Request Too Long
HTTP Error 400. The size of the request headers is too long.
اگر با این مشکل مواجه شدید، تمامی کوکیهای مربوط به سایتهای مذکور را یافته و حذف کنید.
به نظر باگی در فایرفاکس در این زمینه سبب میشود که کوکیهای تمام زیر سایتهای فوق با هم ترکیب شده و رشتهای بسیار طولانی بجای کوکی اصلی آن زیر سایت به هاست ارسال شود.
که البته عکس العمل سایتهای مایکروسافت از دیدگاه امنیتی هم جالب توجه است (برای برنامه نویسهای وب).
IE8 و ارائهی fakepath بجای آدرس فایل
کنترل استاندارد آپلود فایل در مرورگرهای جدید، دیگر آدرس محلی فایل را حتی در اختیار اسکریپتهای سمت کاربر نیز قرار نمیدهند. فایرفاکس مدت زیادی است که این مورد را پیاده سازی کرده. اما IE بجای اینکه یک رشتهی خالی را بازگشت دهد مسیر c:\fakepath را ارائه خواهد داد (fakepath جزو استاندارد html 5 است). اگر احیانا با این مورد برخورد داشتید، با استفاده از تنظیم زیر میتوان مانند سابق مسیر کامل را نیز دریافت کرد:
Internet Explorer -> Tools -> Internet Option -> Security -> Custom ->
find the "Include local directory path when uploading files to a server"
-> click on "Enable"
اهمیت این مورد هم برای من این است که IE، یعنی همان مرورگر کاری در اکثر شرکتها (و این مورد فوق را به سادگی از طریق گروپ پالیسی میتوان به تمام کامپیوترها اعمال کرد).
علت تایم آوت و باز نشدن یک سری از سایتها در ایران
مدت زیادی این سؤال برای من وجود داشت که وجه مشترک سایتهایی مانند dotnetkicks.com ، summerofnhibernate.com ، lessthandot.com و امثال آن چیست که از این طرف باز نمیشوند؟
اگر به آدرسهای فوق که به سایت domaintools.com ختم میشوند، مراجعه کنید و سپس برگهی Server Stats آنها را ملاحظه نمائید، همگی توسط Godaddy.com Inc هاست میشوند. این شرکت غیرمحترم، IP های ایرانی را بسته است (مطلب جدیدی هم نیست).
ابتدا فایل yml زیر را در پوشهی github\workflows\deploy.yml. قرار دهید (پوشهای را به این نام، در ریشهی پروژهی خود ایجاد کنید):
name: Deploy to GitHub Pages # Run workflow on every push to the main branch on: push: branches: [ main ] jobs: deploy-to-github-pages: # use ubuntu-latest image to run steps on runs-on: ubuntu-latest steps: # uses GitHub's checkout action to checkout code form the main branch - uses: actions/checkout@v2 # sets up .NET Core SDK - name: Setup .NET Core SDK uses: actions/setup-dotnet@v1 with: dotnet-version: 5.0.302 # publishes Blazor project to the release-folder - name: Publish .NET Core Project run: dotnet publish ./src/DNTPersianComponents.Blazor.WasmSample/Server/DNTPersianComponents.Blazor.WasmSample.Server.csproj -c Release -o release --nologo # changes the base-tag in index.html from '/' to 'DNTPersianComponents.Blazor' to match GitHub Pages repository subdirectory - name: Change base-tag in index.html from / to DNTPersianComponents.Blazor run: sed -i 's/<base href="\/" \/>/<base href="\/DNTPersianComponents.Blazor\/" \/>/g' release/wwwroot/index.html # copy index.html to 404.html to serve the same file when a file is not found - name: copy index.html to 404.html run: cp release/wwwroot/index.html release/wwwroot/404.html # add .nojekyll file to tell GitHub pages to not treat this as a Jekyll project. (Allow files and folders starting with an underscore) - name: Add .nojekyll file run: touch release/wwwroot/.nojekyll - name: Commit wwwroot to GitHub Pages uses: JamesIves/github-pages-deploy-action@3.7.1 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BRANCH: github-pages FOLDER: release/wwwroot
- نام شاخهی اصلی پروژه؛ که یا main است و یا master.
- شماره نگارش دات نت مورد استفاده.
- مسیر فایل csproj پروژهی wasm.
- نام اصلی مخزن کد.
سپس آنرا به مخزن کد خود commit کنید. بعد به قسمت settings->pages در github مراجعه کرده و source را بر روی نام شاخهی جدید github-pages (فوق در قسمت آخر کار) قرار داده و آنرا ذخیره کنید. الان سایت دموی شما در مسیری که در همین قسمت pages پس از ذخیره سازی، نمایش میدهد، آمادهاست.
یک نکتهی مهم
چون base href، توسط action فوق اصلاح میشود تا به پوشهی نسبی محل قرارگیری برنامه اشاره کند، نیاز است navlinkها با href شروع شدهی با / نباشند؛ چون به ریشهی سایت اشاره میکنند و نه مسیر نسبی محل قرارگیری برنامه. کلا در هر قسمتی از برنامه، این نکته باید رعایت شود. مثلا اگر فونت وبی را در فایل app.css تعریف کردهاید، مسیر آن نباید با / شروع شود؛ وگرنه یافت نخواهد شد. یک مثال:
فایل app.css برنامه در مسیر wwwroot\css\app.css قرار دارد و داخل آن فایل، فونتهای پوشهی دیگر wwwroot\lib\samim-font را به صورت زیر تعریف کردهایم؛ که یعنی مسیر فونت را از ریشهی سایت پیدا کن:
src: url('/lib/samim-font/Samim-Bold.eot?v=4.0.5');
src: url('../lib/samim-font/Samim-Bold.eot?v=4.0.5');
در تصویر بالا دو مسیر با آدرسهای : ShowPage1/ و ShowPage2/ تعریف شده است که هر کدام به یک view مشخص و یک Controller برای مدیریت آن اشاره میکند.
زمانی که ما از تزریق وابستگیها در AngularJs استفاده میکنیم و یک شیء را به کنترلر تزریق میکنیم، Angular توسط Injector$ سعی در پیدا کردن وابستگی مربوطه و سپس تزریق آن به کنترلر را انجام میدهد. برای استفاده از امکان مسیریابی Route ، ما نیز باید از پروایدر مخصوص آن برای تزریق استفاده کنیم. در Angular مسیرهای برنامه توسط پروایدری به نام routeProvider$ شناسایی میشود که خدمات مسیریابی را به ما ارائه میدهد. این سرویس به ما کمک میکند تا بتوانیم اتصال بین کنترلر ها، ویوها و آدرس URL جاری مرورگرها را به آسانی برقرار کنیم.
بهتر است کار را شروع کنیم . یک فایل JS ایجاد و سپس محتویات زیر را در آن قرار دهید :
var myFirstRoute = angular.module('myFirstRoute', []); myFirstRoute.config(['$routeProvider', function($routeProvider) { $routeProvider. when('/pageOne', { templateUrl: 'templates/page_one.html', controller: 'ShowPage1Controller' }). when('/pageTwo', { templateUrl: 'templates/page_two.html', controller: 'ShowPage2Controller' }). otherwise({ redirectTo: '/pageOne' }); }]); myFirstRoute.controller('ShowPage1Controller', function($scope) { $scope.message = 'Content of page-one.html'; }); myFirstRoute.controller('ShowPage2Controller', function($scope) { $scope.message = 'Content of page-two.html'; });
در کدهای بالا ابتدا یک ماژول تعریف کرده ایم و سپس توسط ()config. تنظیمات مربوط به مسیریابی را انجام داده ایم. با استفاده از متدهای when. و otherwise. میتوانیم مسیرها را تعریف کنیم. برای هر مسیر دو پارامتر وجود دارد که اولین پارامتر نام مسیر و دومین پارامتر شامل 2 قسمت میشود که templateUrl آن آدرسی که باز خواهد شد و controller نیز نام کنترلری که ویو را مدیریت میکند.
توسط otherwise میتوانیم مسیر پیشفرض را نیز تعریف کنیم تا درصورتی که مسیری با آدرسهای بالای آن مطابقت نداشت به این آدرس منتقل شود.
در قطعه کد بالا همچنین دو مسیر با نامهای pageOne/ و pageTwo/ تعریف کرده ایم که هر کدام به ترتیب به Viewهای : templates/page_one.html و templates/page_two.html مرتبط شده اند. همچنین دو کنترلر برای مدیریت ویوها نیز تعریف شده است.
زمانی که ما آدرس http://appname/#pageOne را در نوار آدرس مرورگر وارد میکنیم، Angular به صورت اتوماتیک آدرس URL را با تنظیماتی که ما در اینجا تعریف کرده ایم مطابقت میدهد و در صورت وجود چنین آدرسی ، view مربوطه را بارگزاری میکند و در این مثال نیز مطابق با تنظیمات بالا، صفحهی templates/page_one.html برای ما بارگزاری و سپس کنترلر ShowPage1Controller را فراخوانی میکند ، جایی که ما منطق کار را در آن قرار میدهیم.
محتویات فایل main.html :
<body ng-app="app"> <div> <div> <div> <ul> <li><a href="#pageOne"> Show page one </a></li> <li><a href="#pageTwo"> Show page two </a></li> </ul> </div> <div> <div ng-view></div> </div> </div> </div> <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.7/angular.min.js"></script> <script src="app.js"></script> </body>
در قطعه کد بالا دو لینک تعریف شده است که ویژگی href از علامت هش و نام صفحه تشکیل شده است. یکی از چیزهایی که شایان ذکر است ، دایرکتیو ng-view است. مکانی برای بارگزاری صفحات در آن.
ما میتوانیم این تگ را به سه شکل زیر نیز استفاده کنیم :
<div ng-view></div> .. <ng-view></ng-view> .. <div class="ng-view"></div>
محتویات صفحه templates/page_one.html :
<h2>Page One</h2> {{ message }}
محتویات صفحه templates/page_two.html :
<h2>Page Two</h2> {{ message }}
حال اگر پروژه را اجرا کنید و به کنسول مرور گر خود نگاه کنید متوجه میشوید که مسیریاب از مسیر پیشفرض استفاده کرده است و صفحهی page_one.html را به صورت ایجکسی فراخوانی کرده است :
و اگر روی لینک Show Page two کلیک کنید ، صفحهی page_two.html نیز به صورت ایجکسی فراخوانی میشود.
دوباره بر روی لینک Show page one کلیک کنید. بله. هیچ درخواستی به سمت سرور ارسال نشد و صفحهی page_one.html به خوبی نمایش داده شد. یکی از مزیتهای سیستم مسیریابی قابلیت کش کردن صفحات است تا در صورت فراخوانی مجدد، درخواستی به سمت سرور ارسال نشود و خیلی سریع به شما نمایش داده شود.
مثال این مطلب : RouteExample.zip
ادامه دارد ...