شروع به کار با DNTFrameworkCore - قسمت 4 - پیاده‌سازی CRUD API موجودیت‌ها
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: پنج دقیقه

پس از معرفی DNTFrameworkCore، طراحی موجودیت‌های سیستم و پیاده‌سازی DTOها، اعتبارسنج‌ها و سرویس‌های متناظر آنها، در این مطلب روش پیاده سازی CRUD API یکسری موجودیت فرضی را با استفاده از امکانات این زیرساخت بررسی خواهیم کرد.

برای شروع لازم است بسته نیوگت زیر را نصب کنید:
PM> Install-Package DNTFrameworkCore.Web

همچنین برای اعمال خودکار مهاجرت‌های بانک اطلاعاتی، بسته نیوگت زیر را نصب کنید:
PM> Install-Package DNTFrameworkCore.Web.EntityFramework

سپس با استفاده از متد الحاقی MigrateDbContext به شکل زیر می‌توان فرآیند مذکور را خودکار کرد:
public class Program
{
    public static void Main(string[] args)
    {
        CreateWebHostBuilder(args).Build()
            .MigrateDbContext<ProjectDbContext>()
            .Run();
    }

//...
}

مثال اول: پیاده سازی CRUD API یک موجودیت ساده
[Route("api/[controller]")]
public class BlogsController : CrudController<IBlogService, int, BlogModel>
{
    public BlogsController(IBlogService service) : base(service)
    {
    }

    protected override string CreatePermissionName => PermissionNames.Blogs_Create;
    protected override string EditPermissionName => PermissionNames.Blogs_Edit;
    protected override string ViewPermissionName => PermissionNames.Blogs_View;
    protected override string DeletePermissionName => PermissionNames.Blogs_Delete;
}
کار با ارث‌بری از CrudController جنریک شروع می‌شود؛ سپس نیاز است نوع سرویس، نوع مدل و همچنین نوع شناسه مرتبط با موجودیت مورد‎‌نظر را از طریق Type Parameter مشخص کنید. این کنترلر پایه تعاریف مختلفی دارد که برحسب نیاز خود می‌توانید از آنها استفاده کنید. در ادامه نیز برای اعمال دسترسی خاصی برای عملیات CRUD، نیاز است نام دسترسی‌ها را مشخص کنید. کار تمام است؛ برای استفاده از آن می‌توانید با اجرای پروژه DNTFrameworkCore.TestAPI به شکل زیر عمل کنید:
ابتدا نیاز است با استفاده از افزونه‌ی Postman یک Environment جدید ایجاد کنید.

در اینجا دو متغیر endpoint و token ‌برای سرعت بخشیدن به فرآیند تست API تولیدی ایجاد شده‌اند. مقدار endpoint آن از ابتدا مشخص می‌باشد؛ ولی برای مقداردهی token، از ترفند زیر می‌توان استفاده کرد:

در برگه Tests آن می‌توان متغیر تعریف شده در Environment ایجاد شده را با قطعه کد زیر مقداردهی کرد:
 var data = JSON.parse(responseBody)
pm.environment.set("token", data.token);
و برای استفاده از این متغیر به شکل زیر عمل کنید:

حال برای ارسال درخواست‌های HTTP به BlogsController به شکل زیر عمل کنید:

پشت صحنه اکشن GET مربوط به BlogsController از متد سرویس ReadPagedListAsync استفاده می‌شود که خروجی آن در صورت مشخص نکردن TReadModel، برای یک موجودیت ساده مانند واحد سنجش، از همان TModel استفاده خواهد شد. در تصویر بالا لیست صفحه بندی شده موجودیت Blog را مشاهده می‌کنید. برای درخواست صفحه دیگر و جستجوی پویا می‌توان به شکل زیر عمل کرد:
query={"page":1,"pageSize":100,"filter":{"logic":"and","filters":[{"field":"title","value":"Blog1","operator":"startswith"}]}}
همانطور که در مطالب گذشته اشاره شد، ورودی متدهای Read موجود در سرویس‌ها از نوع IFilteredPagedQueryModel می‌باشد. یک ModelBinder سفارشی هم برای بایند خودکار این کوئری استرینک با محتوای یک شیء JSON، در زیرساخت طراحی شده است.

در پشت صحنه اکشن POST از متد CreateAsync سرویس مرتبط استفاده می‌شود و همانطور که در قسمت قبلی عنوان شد، Id و RowVersion مدل ارسالی، مقداردهی خواهد شد. 

در پشت صحنه اکشن ‎‎‎‎‎‎GET/{id}‎ از متد FindAsync سرویس مرتبط استفاده می‌شود و خروجی آن از نوع TModel می‌باشد. 

در پشت صحنه اکشن PUT از متد EditAsync سرویس مرتبط استفاده می‌شود که ورودی آن نوع TModel می‌باشد. همانطور که قبلا اشاره شده بود و در خروجی حاصل از درخواست ویرایش بالا مشخص می‎‌باشد، مکانیزم مدیریت استثناهای حاصل از مباحث همزمانی نیز به درستی انجام شده است.
برای حذف یک Blog می‌توان با ارسال درخواست DELETE به آدرس زیر به این هدف رسید:
{{endpoint}}/blogs/10
در پشت صحنه این اکشن نیز از متد DeleteAsync سرویس مرتبط استفاده می‌شود.
‌‌‌
مثال دوم: پیاده سازی و استفاده از CRUD API در سناریوهای Master-Detail
[Route("api/[controller]")]
public class UsersController : CrudController<IUserService, long, UserReadModel, UserModel>
{
    private readonly ILookupService _lookupService;

    public UsersController(IUserService service, ILookupService lookupService) : base(service)
    {
        _lookupService = lookupService ?? throw new ArgumentNullException(nameof(lookupService));
    }

    protected override string CreatePermissionName => PermissionNames.Users_Create;
    protected override string EditPermissionName => PermissionNames.Users_Edit;
    protected override string ViewPermissionName => PermissionNames.Users_View;
    protected override string DeletePermissionName => PermissionNames.Users_Delete;

    [HttpGet("[action]")]
    [PermissionAuthorize(PermissionNames.Users_Create, PermissionNames.Users_Edit)]
    public async Task<IActionResult> RoleList()
    {
        var result = await _lookupService.ReadRolesAsync();
        return Ok(result);
    }
}
‌‌
موجودیت User از جمله موجودیت‌هایی می‌باشد که نیاز است ReadModel مجزایی برای آن درنظر گرفت؛ چرا که در زمان نمایش لیستی کاربران، نیاز به واکشی گروه‌های کاربری متصل و دسترسی‌های خاص آن، نمی‌باشد. همچنین اکشن متد RoleList برای دریافت لیست گروه‌های کاربری موجود در سیستم نیز پیاده‌سازی شده است. باتوجه به اینکه نیاز است از این اکشن متد در عملیات ثبت و ویرایش استفاده کرد، بر روی آن با استفاده از فیلتر سفارشی PermissionAuthorize، بررسی شده است که کاربر جاری یکی از دسترسی‌های Users_Create یا Users_Edit را داشته باشد.
‎‎‎‎‎
درخواست‌های GET و DELETE مشابه مثال اول می‌باشد؛ برای درخواست‌های POST و PUT آن می‌توان به شکل زیر عمل کرد:

باتوجه به اینکه UserRole به عنوان یکی از وابستگی‌های موجودیت User محسوب می‌شود، در پاسخ درخواست GET مرتبط با کاربری با شناسه 2، roles آن، لیستی از UserRoleModel هستند که به عنوان یک DetailModel طراحی شده است. به عنوان مثال برای حذف اتصال یک گروه کاربری باید درخواست PUT را به شکل زیر ارسال کنید:

این‌بار اگر برای درخواست GET کاربر با شناسه 2 اقدام کنیم، به خروجی زیر خواهم رسید:

{
    "userName": "rabbal",
    "displayName": "غلامرضا ربال",
    "password": null,
    "isActive": false,
    "roles": [],
    "permissions": [],
    "ignoredPermissions": [],
    "rowVersion": "AAAAAAACGxI=",
    "id": 2
}


برای بررسی بیشتر، پیشنهاد می‌کنم پروژه  DNTFrameworkCore.TestAPI  موجود در مخزن این زیرساخت را بازبینی کنید.  
  • #
    ‫۵ سال و ۳ ماه قبل، سه‌شنبه ۳۱ اردیبهشت ۱۳۹۸، ساعت ۱۵:۲۷
    در هنگام تست سرویس‌ها خطای 403 Forbidden می‌دهد، در جدول Permission رکوردی با نام PERMISSION:Users_Create ایجاد گردید ولی مثل اینکه این Permission معتبر نمی‌باشد، Policy مورد نظر جهت احراز هویت باید دقیقا چه مقداری درون دیتابیس ذخیره گردد؟
    • #
      ‫۵ سال و ۳ ماه قبل، سه‌شنبه ۳۱ اردیبهشت ۱۳۹۸، ساعت ۱۵:۳۸
      پیشوند «PERMISSION:‎» برای شناسایی سیاست‌هایی که می‌بایست به صورت خودکار به سیستم معرفی و ثبت شوند، استفاده می‌شود. نیازی به ذخیره‌سازی این پیشوند نیست.

      • #
        ‫۵ سال و ۳ ماه قبل، سه‌شنبه ۳۱ اردیبهشت ۱۳۹۸، ساعت ۱۶:۰۵
        هدف  از ایجاد جدول Permission چه چیزی بوده است؟ (میزان دسترسی هر کاربر را که میتوان در جداول UserClaim و RoleClaim اضافه کرد و با آنها کارکرد)
        فیلدی نیز بنام Discriminator در جدول Permission موجود است که ایندکس هم میشود کاربرد آن چیست؟
        • #
          ‫۵ سال و ۳ ماه قبل، سه‌شنبه ۳۱ اردیبهشت ۱۳۹۸، ساعت ۱۶:۲۸
          دو مدل طراحی مختلف هستند؛ اینکه در جداول UserClaim یا RoleClaim دسترسی‌ها با یک ClaimType بنام Permission مدیریت شوند یا با استفاده از مدل ارث‌بری TPH (همان دلیل وجود فیلد Discriminator) یک جدول Permission داشته باشید برای مدیریت دسترسی‌های مرتبط با گروه کاربری، کاربر و ...
          یا حتی می‌توان آنها را ادغام کرد و به یک جدول Claim طراحی شده با مدل ارث‌بری TPH رسید.
      • #
        ‫۵ سال و ۳ ماه قبل، سه‌شنبه ۳۱ اردیبهشت ۱۳۹۸، ساعت ۱۷:۴۵
        اگر بخواهیم بر اساس Role کاربر که اگر در نقش Administrator باشد کل Permission‌ها Ignore شوند و تمام مجوزهای دسترسی باز شوند، بدون آنکه به سراغ بررسی Claim‌ها برود، تمهیداتی اندیشیده‌اید؟
  • #
    ‫۵ سال و ۱ ماه قبل، شنبه ۵ مرداد ۱۳۹۸، ساعت ۰۶:۴۲
    برای عملیات بروزرسانی جدول Permission و حذف یا اضافه کردن نقش جدید برای یک گروه کاربری، از طریق درخواست PUT با آدرس api/Roles/id امکان افزودن نقش(های) جدید به جدول دیتابیس امکان پذیر نیست و همچنین از لایه سرویس Role و با استفاده از BeforeEditAsync امکان دسترسی به جدول Permission وجود ندارد. لطفن در این زمینه توضیح دهید.
    • #
      ‫۵ سال و ۱ ماه قبل، دوشنبه ۱۴ مرداد ۱۳۹۸، ساعت ۰۲:۲۰
      بررسی شد، مشکلی یافت نشد. برای تست، پروژه DNTFrameworkCore.TestAPI را اجرا کرده و با ابزار Postman درخواست زیر را انجام دهید.

      درخواست PUT (حذف Blogs_Edit، ویرایش Blogs_Create و افزودن Blogs_UseCase)
      {
          "name": "Administrators",
          "description": "حذف گروه کاربری پیش فرض «مدیران سیستم» باعث ایجاد اختلال در کارکرد صحیح سیستم خواهد شد.",
          "permissions": [
              {
                  "name": "Blogs_View1",
                  "id": 1,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Tasks_Edit",
                  "id": 2,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Tasks_Create",
                  "id": 3,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Tasks_View",
                  "id": 4,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Users_Delete",
                  "id": 5,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Users_Edit",
                  "id": 6,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Users_Create",
                  "id": 7,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Tasks_Delete",
                  "id": 8,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Users_View",
                  "id": 9,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Roles_Edit",
                  "id": 10,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Roles_Create",
                  "id": 11,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Roles_View",
                  "id": 12,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Blogs_Delete",
                  "id": 13,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Blogs_Create1",
                  "id": 15,
                  "trackingState": "Modified",
                  "modifiedProperties": null
              },
              {
                  "name": "Blogs_UseCase",
                  "trackingState": "Added"
              },
              {
                  "name": "Blogs_Edit",
                  "id": 14,
                  "trackingState": "Deleted"
              },
          ],
          "rowVersion": "AAAAAAAAF3M=",
          "id": 1
      }

      پاسخ سرور:
      {
          "name": "Administrators",
          "description": "حذف گروه کاربری پیش فرض «مدیران سیستم» باعث ایجاد اختلال در کارکرد صحیح سیستم خواهد شد.",
          "permissions": [
              {
                  "name": "Blogs_View1",
                  "id": 1,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Tasks_Edit",
                  "id": 2,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Tasks_Create",
                  "id": 3,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Tasks_View",
                  "id": 4,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Users_Delete",
                  "id": 5,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Users_Edit",
                  "id": 6,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Users_Create",
                  "id": 7,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Tasks_Delete",
                  "id": 8,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Users_View",
                  "id": 9,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Roles_Edit",
                  "id": 10,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Roles_Create",
                  "id": 11,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Roles_View",
                  "id": 12,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Blogs_Delete",
                  "id": 13,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Blogs_Create1",
                  "id": 15,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              },
              {
                  "name": "Blogs_UseCase",
                  "id": 17,
                  "trackingState": "Unchanged",
                  "modifiedProperties": null
              }
          ],
          "rowVersion": "AAAAAAAAF3Q=",
          "id": 1
      }

      دقت کنید که خاصیت راهبری Permissions را در متد BuildFindQuery کلاس سرویس خود، Include کرده باشد.
      نکته تکمیلی: در نسخه‌های جدید این زیرساخت، خصوصیت ModifiedProperties به کلاس TrackableEntity به عنوان کلاس پایه موجودیت‎‌هایی که نیاز به TrackingState دارند، اضافه شده است. اگر قصد دارید همه خصوصیات یک رکورد ویرایش شوند، این خصوصیت را با null مقداردهی کنید.