بازخوردهای دوره
حذف یک ردیف از اطلاعات به همراه پویانمایی محو شدن اطلاعات آن توسط jQuery در ASP.NET MVC
البته راه دیگری هم پیدا کردم.
به نظرم جالب اومد.
ابتدا یک اکشن فیلتر تعریف شده و در ان هدر درخواست خوانده شده و با محتوی کوکی چک می‌شود در صورتی که برابر باشند عمل انجام خواهد شد با این تفاوت باید توکن در زمان ارسال در هدر درخواست قرار گیرد مانند زیر:
کد اکشن فیلتر:
public class ValidateJsonAntiForgeryTokenAttribute : ActionFilterAttribute
    {
#region Methods (1) 

// Public Methods (1) 

        /// <summary>
        /// Called when [action executing].
        /// </summary>
        /// <param name="actionContext">The action context.</param>
        public void OnActionExecuting(HttpActionContext actionContext)
        {
            try
            {
                var cookieName = AntiForgeryConfig.CookieName;
                var headers = actionContext.Request.Headers;
                var cookie = headers
                    .GetCookies()
                    .Select(c => c[AntiForgeryConfig.CookieName])
                    .FirstOrDefault();
                var rvt = headers.GetValues("__RequestVerificationToken").FirstOrDefault();
                AntiForgery.Validate(cookie != null ? cookie.Value : null, rvt);
            }
            catch
            {
                actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, "Unauthorized request.");
            }
        }

#endregion Methods 
    }
سپس در اکشن دلخواه باید این اکشن فیلتر را بکار برد (در درخواست‌های json)
[HttpPost]
        //[ValidateAntiForgeryToken]
        [ValidateJsonAntiForgeryToken]
        [OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
        public virtual ActionResult DeletePhoto(int? id)
        {
            string ret="error";
            if (id != null && id > 0)
            {
                bool result = _imageGalleryService.Value.Photos.Value.Delete(id.ToInt32());
                ret = result ? "ok" : "nok";
            }
            return Content(ret);
            //return Json(new { ret });
        }
سپس در طرف ویو به صورت زیر عمل شود
$.ajax({
                       ....
                    type: "POST",
                    url: '@Url',
                    data: JSON.stringify({ id: Id }),
                    contentType: "application/json; charset=utf-8",
                    // این قسمت اضافه شود
                    headers: { __RequestVerificationToken: $("input[name=__RequestVerificationToken]").val() },
                    dataType: "json",
                     ..........
امیدوارم برای دوستان هم کاربرد داشته باشد




مطالب
لینک‌های هفته‌ی آخر دی

وبلاگ‌ها ، سایت‌ها و مقالات ایرانی (داخل و خارج از ایران)


Visual Studio


ASP. Net



طراحی و توسعه وب



PHP


اس‌کیوال سرور


سی شارپ


عمومی دات نت


ویندوز


مسایل اجتماعی و انسانی برنامه نویسی


متفرقه


مطالب
احراز هویت و اعتبارسنجی کاربران در برنامه‌های Angular - قسمت ششم - کار با منابع محافظت شده‌ی سمت سرور
پس از تکمیل کنترل دسترسی‌ها به قسمت‌های مختلف برنامه بر اساس نقش‌های انتسابی به کاربر وارد شده‌ی به سیستم، اکنون نوبت به کار با سرور و دریافت اطلاعات از کنترلرهای محافظت شده‌ی آن است.



افزودن کامپوننت دسترسی به منابع محافظت شده، به ماژول Dashboard

در اینجا قصد داریم صفحه‌ای را به برنامه اضافه کنیم تا در آن بتوان اطلاعات کنترلرهای محافظت شده‌ی سمت سرور، مانند MyProtectedAdminApiController (تنها قابل دسترسی توسط کاربرانی دارای نقش Admin) و MyProtectedApiController (قابل دسترسی برای عموم کاربران وارد شده‌ی به سیستم) را دریافت و نمایش دهیم. به همین جهت کامپوننت جدیدی را به ماژول Dashboard اضافه می‌کنیم:
 >ng g c Dashboard/CallProtectedApi
سپس به فایل dashboard-routing.module.ts ایجاد شده مراجعه کرده و مسیریابی کامپوننت جدید ProtectedPage را اضافه می‌کنیم:
import { CallProtectedApiComponent } from "./call-protected-api/call-protected-api.component";

const routes: Routes = [
  {
    path: "callProtectedApi",
    component: CallProtectedApiComponent,
    data: {
      permission: {
        permittedRoles: ["Admin", "User"],
        deniedRoles: null
      } as AuthGuardPermission
    },
    canActivate: [AuthGuard]
  }
];
توضیحات AuthGuard و AuthGuardPermission را در قسمت قبل مطالعه کردید. در اینجا هدف این است که تنها کاربران دارای نقش‌های Admin و یا User قادر به دسترسی به این مسیر باشند.
لینکی را به این صفحه نیز در فایل header.component.html به صورت ذیل اضافه خواهیم کرد تا فقط توسط کاربران وارد شده‌ی به سیستم (isLoggedIn) قابل مشاهده باشد:
<li *ngIf="isLoggedIn" routerLinkActive="active">
        <a [routerLink]="['/callProtectedApi']">‍‍Call Protected Api</a>
</li>


نمایش و یا مخفی کردن قسمت‌های مختلف صفحه بر اساس نقش‌های کاربر وارد شده‌ی به سیستم

در ادامه می‌خواهیم دو دکمه را بر روی صفحه قرار دهیم تا اطلاعات کنترلرهای محافظت شده‌ی سمت سرور را بازگشت دهند. دکمه‌ی اول قرار است تنها برای کاربر Admin قابل مشاهده باشد و دکمه‌ی دوم توسط کاربری با نقش‌های Admin و یا User.
به همین جهت call-protected-api.component.ts را به صورت ذیل تغییر می‌دهیم:
import { Component, OnInit } from "@angular/core";
import { AuthService } from "../../core/services/auth.service";

@Component({
  selector: "app-call-protected-api",
  templateUrl: "./call-protected-api.component.html",
  styleUrls: ["./call-protected-api.component.css"]
})
export class CallProtectedApiComponent implements OnInit {

  isAdmin = false;
  isUser = false;
  result: any;

  constructor(private authService: AuthService) { }

  ngOnInit() {
    this.isAdmin = this.authService.isAuthUserInRole("Admin");
    this.isUser = this.authService.isAuthUserInRole("User");
  }

  callMyProtectedAdminApiController() {
  }

  callMyProtectedApiController() {
  }
}
در اینجا دو خاصیت عمومی isAdmin و isUser، در اختیار قالب این کامپوننت قرار گرفته‌اند. مقدار دهی آن‌ها نیز توسط متد isAuthUserInRole که در قسمت قبل توسعه دادیم، انجام می‌شود. اکنون که این دو خاصیت مقدار دهی شده‌اند، می‌توان از آن‌ها به کمک یک ngIf، به صورت ذیل در قالب call-protected-api.component.html جهت مخفی کردن و یا نمایش قسمت‌های مختلف صفحه استفاده کرد:
<button *ngIf="isAdmin" (click)="callMyProtectedAdminApiController()">
  Call Protected Admin API [Authorize(Roles = "Admin")]
</button>

<button *ngIf="isAdmin || isUser" (click)="callMyProtectedApiController()">
  Call Protected API ([Authorize])
</button>

<div *ngIf="result">
  <pre>{{result | json}}</pre>
</div>


دریافت اطلاعات از کنترلرهای محافظت شده‌ی سمت سرور

برای دریافت اطلاعات از کنترلرهای محافظت شده، باید در قسمتی که HttpClient درخواست خود را به سرور ارسال می‌کند، هدر مخصوص Authorization را که شامل توکن دسترسی است، به سمت سرور ارسال کرد. این هدر ویژه را به صورت ذیل می‌توان در AuthService تولید نمود:
  getBearerAuthHeader(): HttpHeaders {
    return new HttpHeaders({
      "Content-Type": "application/json",
      "Authorization": `Bearer ${this.getRawAuthToken(AuthTokenType.AccessToken)}`
    });
  }

روش دوم انجام اینکار که مرسوم‌تر است، اضافه کردن خودکار این هدر به تمام درخواست‌های ارسالی به سمت سرور است. برای اینکار باید یک HttpInterceptor را تهیه کرد. به همین منظور فایل جدید app\core\services\auth.interceptor.ts را به برنامه اضافه کرده و به صورت ذیل تکمیل می‌کنیم:
import { Injectable } from "@angular/core";
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from "@angular/common/http";
import { Observable } from "rxjs/Observable";

import { AuthService, AuthTokenType } from "./auth.service";

@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private authService: AuthService) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    const accessToken = this.authService.getRawAuthToken(AuthTokenType.AccessToken);
    if (accessToken) {
      request = request.clone({
        headers: request.headers.set("Authorization", `Bearer ${accessToken}`)
      });
    }

    return next.handle(request);
  }
}
در اینجا یک clone از درخواست جاری ایجاد شده و سپس به headers آن، یک هدر جدید Authorization که به همراه توکن دسترسی است، اضافه خواهد شد.
به این ترتیب دیگری نیازی نیست تا به ازای هر درخواست و هر قسمتی از برنامه، این هدر را به صورت دستی تنظیم کرد و اضافه شدن آن پس از تنظیم ذیل، به صورت خودکار انجام می‌شود:
import { HTTP_INTERCEPTORS } from "@angular/common/http";

import { AuthInterceptor } from "./services/auth.interceptor";

@NgModule({
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true
    }
  ]
})
export class CoreModule {}
در اینجا نحوه‌ی معرفی این HttpInterceptor جدید را به قسمت providers مخصوص CoreModule مشاهده می‌کنید.

در این حالت اگر برنامه را اجرا کنید، خطای ذیل را در کنسول توسعه‌دهنده‌های مرورگر مشاهده خواهید کرد:
compiler.js:19514 Uncaught Error: Provider parse errors:
Cannot instantiate cyclic dependency! InjectionToken_HTTP_INTERCEPTORS ("[ERROR ->]"): in NgModule AppModule in ./AppModule@-1:-1
در سازنده‌ی کلاس سرویس AuthInterceptor، سرویس Auth تزریق شده‌است که این سرویس نیز دارای HttpClient تزریق شده‌ی در سازنده‌ی آن است. به همین جهت Angular تصور می‌کند که ممکن است در اینجا یک بازگشت بی‌نهایت بین این interceptor و سرویس Auth رخ‌دهد. اما از آنجائیکه ما هیچکدام از متدهایی را که با HttpClient کار می‌کنند، در اینجا فراخوانی نمی‌کنیم و تنها کاربرد سرویس Auth، دریافت توکن دسترسی است، این مشکل را می‌توان به صورت ذیل برطرف کرد:
import { Injector } from "@angular/core";

  constructor(private injector: Injector) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const authService = this.injector.get(AuthService);
ابتدا سرویس Injector را به سازنده‌ی کلاس AuthInterceptor تزریق می‌کنیم و سپس توسط متد get آن، سرویس Auth را درخواست خواهیم کرد (بجای تزریق مستقیم آن در سازنده‌ی کلاس):
@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(private injector: Injector) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const authService = this.injector.get(AuthService);
    const accessToken = authService.getRawAuthToken(AuthTokenType.AccessToken);
    if (accessToken) {
      request = request.clone({
        headers: request.headers.set("Authorization", `Bearer ${accessToken}`)
      });
    }

    return next.handle(request);
  }
}


تکمیل متدهای دریافت اطلاعات از کنترلرهای محافظت شده‌ی سمت سرور

اکنون پس از افزودن AuthInterceptor، می‌توان متدهای CallProtectedApiComponent را به صورت ذیل تکمیل کرد. ابتدا سرویس‌های Auth ،HttpClient و همچنین تنظیمات آغازین برنامه را به سازنده‌ی CallProtectedApiComponent تزریق می‌کنیم:
  constructor(
    private authService: AuthService,
    private httpClient: HttpClient,
    @Inject(APP_CONFIG) private appConfig: IAppConfig,
  ) { }
سپس متدهای httpClient.get و یا هر نوع متد مشابه دیگری را به صورت معمولی فراخوانی خواهیم کرد:
  callMyProtectedAdminApiController() {
    this.httpClient
      .get(`${this.appConfig.apiEndpoint}/MyProtectedAdminApi`)
      .map(response => response || {})
      .catch((error: HttpErrorResponse) => Observable.throw(error))
      .subscribe(result => {
        this.result = result;
      });
  }

  callMyProtectedApiController() {
    this.httpClient
      .get(`${this.appConfig.apiEndpoint}/MyProtectedApi`)
      .map(response => response || {})
      .catch((error: HttpErrorResponse) => Observable.throw(error))
      .subscribe(result => {
        this.result = result;
      });
  }

در این حالت اگر برنامه را اجرا کنید، افزوده شدن خودکار هدر مخصوص Authorization:Bearer را در درخواست ارسالی به سمت سرور، مشاهده خواهید کرد:



مدیریت خودکار خطاهای عدم دسترسی ارسال شده‌ی از سمت سرور

ممکن است کاربری درخواستی را به منبع محافظت شده‌ای ارسال کند که به آن دسترسی ندارد. در AuthInterceptor تعریف شده می‌توان به وضعیت این خطا، دسترسی یافت و سپس کاربر را به صفحه‌ی accessDenied که در قسمت قبل ایجاد کردیم، به صورت خودکار هدایت کرد:
    return next.handle(request)
      .catch((error: any, caught: Observable<HttpEvent<any>>) => {
        if (error.status === 401 || error.status === 403) {
          this.router.navigate(["/accessDenied"]);
        }
        return Observable.throw(error);
      });
در اینجا ابتدا نیاز است سرویس Router، به سازنده‌ی کلاس تزریق شود و سپس متد catch درخواست پردازش شده، به صورت فوق جهت عکس العمل نشان دادن به وضعیت‌های 401 و یا 403 و هدایت کاربر به مسیر accessDenied تغییر کند:
@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(
    private injector: Injector,
    private router: Router) { }

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const authService = this.injector.get(AuthService);
    const accessToken = authService.getRawAuthToken(AuthTokenType.AccessToken);
    if (accessToken) {
      request = request.clone({
        headers: request.headers.set("Authorization", `Bearer ${accessToken}`)
      });
      return next.handle(request)
        .catch((error: any, caught: Observable<HttpEvent<any>>) => {
          if (error.status === 401 || error.status === 403) {
            this.router.navigate(["/accessDenied"]);
          }
          return Observable.throw(error);
        });
    } else {
      // login page
      return next.handle(request);
    }
  }
}
برای آزمایش آن، یک کنترلر سمت سرور جدید را با نقش Editor اضافه می‌کنیم:
using ASPNETCore2JwtAuthentication.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;

namespace ASPNETCore2JwtAuthentication.WebApp.Controllers
{
    [Route("api/[controller]")]
    [EnableCors("CorsPolicy")]
    [Authorize(Policy = CustomRoles.Editor)]
    public class MyProtectedEditorsApiController : Controller
    {
        public IActionResult Get()
        {
            return Ok(new
            {
                Id = 1,
                Title = "Hello from My Protected Editors Controller! [Authorize(Policy = CustomRoles.Editor)]",
                Username = this.User.Identity.Name
            });
        }
    }
}
و برای فراخوانی سمت کلاینت آن در CallProtectedApiComponent خواهیم داشت:
  callMyProtectedEditorsApiController() {
    this.httpClient
      .get(`${this.appConfig.apiEndpoint}/MyProtectedEditorsApi`)
      .map(response => response || {})
      .catch((error: HttpErrorResponse) => Observable.throw(error))
      .subscribe(result => {
        this.result = result;
      });
  }
چون این نقش جدید به کاربر جاری انتساب داده نشده‌است (جزو اطلاعات سمت سرور او نیست)، اگر آن‌را توسط متد فوق فراخوانی کند، خطای 403 را دریافت کرده و به صورت خودکار به مسیر accessDenied هدایت می‌شود:



نکته‌ی مهم: نیاز به دائمی کردن کلیدهای رمزنگاری سمت سرور

اگر برنامه‌ی سمت سرور ما که توکن‌ها را اعتبارسنجی می‌کند، ری‌استارت شود، چون قسمتی از کلیدهای رمزگشایی اطلاعات آن با اینکار مجددا تولید خواهند شد، حتی با فرض لاگین بودن شخص در سمت کلاینت، توکن‌های فعلی او برگشت خواهند خورد و از مرحله‌ی تعیین اعتبار رد نمی‌شوند. در این حالت کاربر خطای 401 را دریافت می‌کند. بنابراین پیاده سازی مطلب «غیرمعتبر شدن کوکی‌های برنامه‌های ASP.NET Core هاست شده‌ی در IIS پس از ری‌استارت آن» را فراموش نکنید.



کدهای کامل این سری را از اینجا می‌توانید دریافت کنید.
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه‌ی ASPNETCore2JwtAuthentication.AngularClient وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng serve -o، برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد (و یا همان اجرای فایل ng-serve.bat). همچنین باید به پوشه‌ی ASPNETCore2JwtAuthentication.WebApp نیز مراجعه کرده و فایل dotnet_run.bat را اجرا کنید، تا توکن سرور برنامه نیز فعال شود. 
مطالب دوره‌ها
تزریق خودکار وابستگی‌ها در ASP.NET Web API به همراه رها سازی خودکار منابع IDisposable
در انتهای مطلب « تزریق خودکار وابستگی‌ها در برنامه‌های ASP.NET MVC » اشاره‌ای کوتاه به روش DependencyResolver توکار Web API شد که این روش پس از بررسی‌های بیشتر (^ و ^) به دلیل ماهیت service locator بودن آن و همچنین از دست دادن Context جاری Web API، مردود اعلام شده و استفاده از IHttpControllerActivator توصیه می‌گردد. در ادامه این روش را توسط Structure map 3 پیاده سازی خواهیم کرد.

پیش نیازها
- شروع یک پروژه‌ی جدید وب با پشتیبانی از Web API
- نصب دو بسته‌ی نیوگت مرتبط با Structure map 3
 PM>install-package structuremap
PM>install-package structuremap.web

پیاده سازی IHttpControllerActivator توسط Structure map

using System;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Dispatcher;
using StructureMap;

namespace WebApiDISample.Core
{
    public class StructureMapHttpControllerActivator : IHttpControllerActivator
    {
        private readonly IContainer _container;
        public StructureMapHttpControllerActivator(IContainer container)
        {
            _container = container;
        }

        public IHttpController Create(
                HttpRequestMessage request,
                HttpControllerDescriptor controllerDescriptor,
                Type controllerType)
        {
            var nestedContainer = _container.GetNestedContainer();
            request.RegisterForDispose(nestedContainer);
            return (IHttpController)nestedContainer.GetInstance(controllerType);
        }
    }
}
در اینجا نحوه‌ی پیاده سازی IHttpControllerActivator را توسط StructureMap ملاحظه می‌کنید.
نکته‌ی مهم آن استفاده از NestedContainer آن است. معرفی آن به متد request.RegisterForDispose سبب می‌شود تا کلیه کلاس‌های IDisposable نیز در پایان کار به صورت خودکار رها سازی شده و نشتی حافظه رخ ندهد.


معرفی StructureMapHttpControllerActivator به برنامه

فایل WebApiConfig.cs را گشوده و تغییرات ذیل را در آن اعمال کنید:
using System.Web.Http;
using System.Web.Http.Dispatcher;
using StructureMap;
using WebApiDISample.Core;
using WebApiDISample.Services;

namespace WebApiDISample
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // IoC Config
            ObjectFactory.Configure(c => c.For<IEmailsService>().Use<EmailsService>());

            var container = ObjectFactory.Container;
            GlobalConfiguration.Configuration.Services.Replace(
                typeof(IHttpControllerActivator), new StructureMapHttpControllerActivator(container));


            // Web API routes
            config.MapHttpAttributeRoutes();
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}
 در ابتدا تنظیمات متداول کلاس‌ها و اینترفیس‌ها صورت می‌گیرد. سپس نحوه‌ی معرفی  StructureMapHttpControllerActivator را به GlobalConfiguration.Configuration.Services مخصوص Web API ملاحظه می‌کنید. این مورد سبب می‌شود تا به صورت خودکار کلیه وابستگی‌های مورد نیاز یک Web API Controller به آن تزریق شوند.


تهیه سرویسی برای آزمایش برنامه

namespace WebApiDISample.Services
{
    public interface IEmailsService
    {
        void SendEmail();
    }
}

using System;

namespace WebApiDISample.Services
{
    /// <summary>
    /// سرویسی که دارای قسمت دیسپوز نیز هست
    /// </summary>
    public class EmailsService : IEmailsService, IDisposable
    {
        private bool _disposed;

        ~EmailsService()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        public void SendEmail()
        {
            //todo: send email!
        }

        protected virtual void Dispose(bool disposeManagedResources)
        {
            if (_disposed) return;
            if (!disposeManagedResources) return;

            //todo: clean up resources here ...

            _disposed = true;
        }
    }
}
در اینجا یک سرویس ساده ارسال ایمیل را بدون پیاده سازی خاصی مشاهده می‌کنید.
نکته‌ی مهم آن استفاده از IDisposable در این کلاس خاص است (ضروری نیست؛ صرفا جهت بررسی بیشتر اضافه شده‌است). اگر در کدهای برنامه، یک چنین کلاسی وجود داشت، نیاز است متد Dispose آن نیز توسط IoC Container فراخوانی شود. برای آزمایش آن یک break point را در داخل متد Dispose قرار دهید.


استفاده از سرویس تعریف شده در یک Web API Controller

using System.Web.Http;
using WebApiDISample.Services;

namespace WebApiDISample.Controllers
{
    public class ValuesController : ApiController
    {
        private readonly IEmailsService _emailsService;
        public ValuesController(IEmailsService emailsService)
        {
            _emailsService = emailsService;
        }

        // GET api/values/5
        public string Get(int id)
        {
            _emailsService.SendEmail();
            return "_emailsService.SendEmail(); called!";
        }
    }
}
در اینجا مثال ساده‌ای را از نحوه‌ی تزریق سرویس ارسال ایمیل را در ValuesController مشاهده می‌کنید.
تزریق وهله‌ی مورد نیاز آن، به صورت خودکار توسط StructureMapHttpControllerActivator که در ابتدای بحث معرفی شد، صورت می‌گیرد.

فراخوانی متد Get آن‌را نیز توسط کدهای سمت کاربر ذیل انجام خواهیم داد:
<h2>Index</h2>

@section scripts
{
    <script type="text/javascript">
        $(function () {
            $.getJSON('/api/values/1?timestamp=' + new Date().getTime(), function (data) {
                alert(data);
            });
        });
    </script>
}
درون متد Get کنترلر، یک break point قرار دهید. همچنین داخل متد Dispose لایه سرویس نیز جهت بررسی بیشتر یک break point قرار دهید.
اکنون برنامه را اجرا کنید. هنگام فراخوانی متد Get، وهله‌ی سرویس مورد نظر، نال نیست. همچنین متد Dispose نیز به صورت خودکار فراخوانی می‌شود.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید
WebApiDISample.zip 
مطالب
ساخت یک بلاگ ساده با Ember.js، قسمت پنجم
مقدمات ساخت بلاگ مبتنی بر ember.js در قسمت قبل به پایان رسید. در این قسمت صرفا قصد داریم بجای استفاده از HTML 5 local storage از یک REST web service مانند یک ASP.NET Web API Controller و یا یک ASP.NET MVC Controller استفاده کنیم و اطلاعات نهایی را به سرور ارسال و یا از آن دریافت کنیم.


تنظیم Ember data برای کار با سرور

Ember data به صورت پیش فرض و در پشت صحنه با استفاده از Ajax برای کار با یک REST Web Service طراحی شده‌است و کلیه تبادلات آن نیز با فرمت JSON انجام می‌شود. بنابراین تمام کدهای سمت کاربر قسمت قبل نیز در این حالت کار خواهند کرد. تنها کاری که باید انجام شود، حذف تنظیمات ابتدایی آن برای کار با HTML 5 local storage است.
برای این منظور ابتدا فایل index.html را گشوده و سپس مدخل localstorage_adapter.js را از آن حذف کنید:
 <!--<script src="Scripts/Libs/localstorage_adapter.js" type="text/javascript"></script>-->
همچنین دیگر نیازی به store.js نیز نمی‌باشد:
 <!--<script src="Scripts/App/store.js" type="text/javascript"></script>-->

اکنون برنامه را اجرا کنید، چنین پیام خطایی را مشاهده خواهید کرد:

همانطور که عنوان شد، ember data به صورت پیش فرض با سرور کار می‌کند و در اینجا به صورت خودکار، یک درخواست Get را به آدرس http://localhost:25918/posts جهت دریافت آخرین مطالب ثبت شده، ارسال کرده‌است و چون هنوز وب سرویسی در برنامه تعریف نشده، با خطای 404 و یا یافت نشد، مواجه شده‌است.
این درخواست نیز بر اساس تعاریف موجود در فایل Scripts\Routes\posts.js، به سرور ارسال شده‌است:
Blogger.PostsRoute = Ember.Route.extend({
    model: function () {
        return this.store.find('post');
    }
});
Ember data شبیه به یک ORM عمل می‌کند. تنظیمات ابتدایی آن‌را تغییر دهید، بدون نیازی به تغییر در کدهای اصلی برنامه، می‌تواند با یک منبع داده جدید کار کند.


تغییر تنظیمات پیش فرض آغازین Ember data

آدرس درخواستی http://localhost:25918/posts به این معنا است که کلیه درخواست‌ها، به همان آدرس و پورت ریشه‌ی اصلی سایت ارسال می‌شوند. اما اگر یک ASP.NET Web API Controller را تعریف کنیم، نیاز است این درخواست‌ها، برای مثال به آدرس api/posts ارسال شوند؛ بجای /posts.
برای این منظور پوشه‌ی جدید Scripts\Adapters را ایجاد کرده و فایل web_api_adapter.js را با این محتوا به آن اضافه کنید:
 DS.RESTAdapter.reopen({
      namespace: 'api'
});
سپس تعریف مدخل آن‌را نیز به فایل index.html اضافه نمائید:
 <script src="Scripts/Adapters/web_api_adapter.js" type="text/javascript"></script>
تعریف فضای نام در اینجا سبب خواهد شد تا درخواست‌های جدید به آدرس api/posts ارسال شوند.


تغییر تنظیمات پیش فرض ASP.NET Web API

در سمت سرور، بنابر اصول نامگذاری خواص، نام‌ها با حروف بزرگ شروع می‌شوند:
namespace EmberJS03.Models
{
    public class Post
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }
    }
}
اما در سمت کاربر و کدهای اسکریپتی، عکس آن صادق است. به همین جهت نیاز است که CamelCasePropertyNamesContractResolver را در JSON.NET تنظیم کرد تا به صورت خودکار اطلاعات ارسالی به کلاینت‌ها را به صورت camel case تولید کند:
using System;
using System.Web.Http;
using System.Web.Routing;
using Newtonsoft.Json.Serialization;
 
namespace EmberJS03
{
    public class Global : System.Web.HttpApplication
    {
 
        protected void Application_Start(object sender, EventArgs e)
        {
            RouteTable.Routes.MapHttpRoute(
               name: "DefaultApi",
               routeTemplate: "api/{controller}/{id}",
               defaults: new { id = RouteParameter.Optional }
               );
 
            var settings = GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings;
            settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        }
    }
}


نحوه‌ی صحیح بازگشت اطلاعات از یک ASP.NET Web API جهت استفاده در Ember data

با تنظیمات فوق، اگر کنترلر جدیدی را به صورت ذیل جهت بازگشت لیست مطالب تهیه کنیم:
namespace EmberJS03.Controllers
{
    public class PostsController : ApiController
    {
        public IEnumerable<Post> Get()
        {
            return DataSource.PostsList;
        } 
    }
}
با یک چنین خطایی در سمت کاربر مواجه خواهیم شد:
  WARNING: Encountered "0" in payload, but no model was found for model name "0" (resolved model name using DS.RESTSerializer.typeForRoot("0"))
این خطا از آنجا ناشی می‌شود که Ember data، اطلاعات دریافتی از سرور را بر اساس قرارداد JSON API دریافت می‌کند. برای حل این مشکل راه‌حل‌های زیادی مطرح شده‌اند که تعدادی از آن‌ها را در لینک‌های زیر می‌توانید مطالعه کنید:
http://jsonapi.codeplex.com
https://github.com/xqiu/MVCSPAWithEmberjs
https://github.com/rmichela/EmberDataAdapter
https://github.com/MilkyWayJoe/Ember-WebAPI-Adapter
http://blog.yodersolutions.com/using-ember-data-with-asp-net-web-api
http://emadibrahim.com/2014/04/09/emberjs-and-asp-net-web-api-and-json-serialization

و خلاصه‌ی آن‌ها به این صورت است:
خروجی JSON تولیدی توسط ASP.NET Web API چنین شکلی را دارد:
[
  {
    Id: 1,
    Title: 'First Post'
  }, {
    Id: 2,
    Title: 'Second Post'
  }
]
اما Ember data نیاز به یک چنین خروجی دارد:
{
  posts: [{
    id: 1,
    title: 'First Post'
  }, {
    id: 2,
    title: 'Second Post'
  }]
}
به عبارتی آرایه‌ی مطالب را از ریشه‌ی posts باید دریافت کند (مطابق فرمت JSON API). برای انجام اینکار یا از لینک‌های معرفی شده استفاده کنید و یا راه حل ساده‌ی ذیل هم پاسخگو است:
using System.Web.Http;
using EmberJS03.Models;
 
namespace EmberJS03.Controllers
{
    public class PostsController : ApiController
    {
        public object Get()
        {
            return new { posts = DataSource.PostsList };
        }
    }
}
در اینجا ریشه‌ی posts را توسط یک anonymous object ایجاد کرده‌ایم.
اکنون اگر برنامه را اجرا کنید، در صفحه‌ی اول آن، لیست عناوین مطالب را مشاهده خواهید کرد.


تاثیر قرارداد JSON API در حین ارسال اطلاعات به سرور توسط Ember data

در تکمیل کنترلرهای Web API مورد نیاز (کنترلرهای مطالب و نظرات)، نیاز به متدهای Post، Update و Delete هم خواهد بود. دقیقا فرامین ارسالی توسط Ember data توسط همین HTTP Verbs به سمت سرور ارسال می‌شوند. در این حالت اگر متد Post کنترلر نظرات را به این شکل طراحی کنیم:
 public HttpResponseMessage Post(Comment comment)
کار نخواهد کرد؛ چون مطابق فرمت JSON API ارسالی توسط Ember data، یک چنین شیء JSON ایی را دریافت خواهیم کرد:
{"comment":{"text":"data...","post":"3"}}
بنابراین Ember data چه در حین دریافت اطلاعات از سرور و چه در زمان ارسال اطلاعات به آن، اشیاء جاوا اسکریپتی را در یک ریشه‌ی هم نام آن شیء قرار می‌دهد.
برای پردازش آن، یا باید از راه حل‌های ثالث مطرح شده در ابتدای بحث استفاده کنید و یا می‌توان مطابق کدهای ذیل، کل اطلاعات JSON ارسالی را توسط کتابخانه‌ی JSON.NET نیز پردازش کرد:
namespace EmberJS03.Controllers
{
    public class CommentsController : ApiController
    {
        public HttpResponseMessage Post(HttpRequestMessage requestMessage)
        {
            var jsonContent = requestMessage.Content.ReadAsStringAsync().Result;
            // {"comment":{"text":"data...","post":"3"}}
            var jObj = JObject.Parse(jsonContent);
            var comment = jObj.SelectToken("comment", false).ToObject<Comment>();


            var id = 1;
            var lastItem = DataSource.CommentsList.LastOrDefault();
            if (lastItem != null)
            {
                id = lastItem.Id + 1;
            }
            comment.Id = id;
            DataSource.CommentsList.Add(comment);

            // ارسال آی دی با فرمت خاص مهم است
            return Request.CreateResponse(HttpStatusCode.Created, new { comment = comment });
        }
    }
}
در اینجا توسط requestMessage به محتوای ارسال شده‌ی به سرور که همان شیء JSON ارسالی است، دسترسی خواهیم داشت. سپس متد JObject.Parse، آن‌را به صورت عمومی تبدیل به یک شیء JSON می‌کند و نهایتا با استفاده از متد SelectToken آن می‌توان ریشه‌ی comment و یا در کنترلر مطالب، ریشه‌ی post را انتخاب و سپس تبدیل به شیء Comment و یا Post کرد.
همچنین فرمت return نهایی هم مهم است. در این حالت خروجی ارسالی به سمت کاربر، باید مجددا با فرمت JSON API باشد؛ یعنی باید comment اصلاح شده را به همراه ریشه‌ی comment ارسال کرد. در اینجا نیز anonymous object تهیه شده، چنین کاری را انجام می‌دهد.


Lazy loading در Ember data

تا اینجا اگر برنامه را اجرا کنید، لیست مطالب صفحه‌ی اول را مشاهده خواهید کرد، اما لیست نظرات آن‌ها را خیر؛ از این جهت که ضرورتی نداشت تا در بار اول ارسال لیست مطالب به سمت کاربر، تمام نظرات متناظر با آن‌ها را هم ارسال کرد. بهتر است زمانیکه کاربر یک مطلب خاص را مشاهده می‌کند، نظرات خاص آن‌را به سمت کاربر ارسال کنیم.
در تعاریف سمت کاربر Ember data، پارامتر دوم رابطه‌ی hasMany که با async:true مشخص شده‌است، دقیقا معنای lazy loading را دارد.
Blogger.Post = DS.Model.extend({
   title: DS.attr(),
   body: DS.attr(),
   comments: DS.hasMany('comment', { async: true } /* lazy loading */)
});
در سمت سرور، دو راه برای فعال سازی این lazy loading تعریف شده در سمت کاربر وجود دارد:
الف) Idهای نظرات هر مطلب را به صورت یک آرایه، در بار اول ارسال لیست نظرات به سمت کاربر، تهیه و ارسال کنیم:
namespace EmberJS03.Models
{
    public class Post
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }
 
        // lazy loading via an array of IDs
        public int[] Comments { set; get; } 
    }
}
در اینجا خاصیت Comments، تنها کافی است لیستی از Idهای نظرات مرتبط با مطلب جاری باشد. در این حالت در سمت کاربر اگر مطلب خاصی جهت مشاهده‌ی جزئیات آن انتخاب شود، به ازای هر Id ذکر شده، یکبار دستور Get صادر خواهد شد.
ب) این روش به علت تعداد رفت و برگشت بیش از حد به سرور، کارآیی آنچنانی ندارد. بهتر است جهت مشاهده‌ی جزئیات یک مطلب، تنها یکبار درخواست Get کلیه نظرات آن صادر شود.
برای اینکار باید مدل برنامه را به شکل زیر تغییر دهیم:
namespace EmberJS03.Models
{
    public class Post
    {
        public int Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }
 
        // load related models via URLs instead of an array of IDs
        // ref. https://github.com/emberjs/data/pull/1371
        public object Links { set; get; }
 
        public Post()
        {
            Links = new { comments = "comments" }; // api/posts/id/comments
        }
    }
}
در اینجا یک خاصیت جدید به نام Links ارائه شده‌است. نام Links در Ember data استاندارد است و از آن برای دریافت کلیه اطلاعات لینک شده‌ی به یک مطلب استفاده می‌شود. با تعریف این خاصیت به نحوی که ملاحظه می‌کنید، اینبار Ember data تنها یکبار درخواست ویژه‌ای را با فرمت api/posts/id/comments، به سمت سرور ارسال می‌کند. برای مدیریت آن، قالب مسیریابی پیش فرض {api/{controller}/{id را می‌توان به صورت {api/{controller}/{id}/{name اصلاح کرد:
namespace EmberJS03
{
    public class Global : System.Web.HttpApplication
    {
 
        protected void Application_Start(object sender, EventArgs e)
        {
            RouteTable.Routes.MapHttpRoute(
               name: "DefaultApi",
               routeTemplate: "api/{controller}/{id}/{name}",
               defaults: new { id = RouteParameter.Optional, name = RouteParameter.Optional }
               );
 
            var settings = GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings;
            settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
        }
    }
}
اکنون دیگر درخواست جدید api/posts/3/comments با پیام 404 یا یافت نشد مواجه نمی‌شود.
در این حالت در طی یک درخواست می‌توان کلیه نظرات را به سمت کاربر ارسال کرد. در اینجا نیز ذکر ریشه‌ی comments همانند ریشه posts، الزامی است:
namespace EmberJS03.Controllers
{
    public class PostsController : ApiController
    {
        //GET api/posts/id
        public object Get(int id)
        {
            return
                new
                {
                    posts = DataSource.PostsList.FirstOrDefault(post => post.Id == id),
                    comments = DataSource.CommentsList.Where(comment => comment.Post == id).ToList()
                };
        }
    }
}


پردازش‌های async و متد transitionToRoute در Ember.js

اگر متد حذف مطالب را نیز به کنترلر Posts اضافه کنیم:
namespace EmberJS03.Controllers
{
    public class PostsController : ApiController
    {
        public HttpResponseMessage Delete(int id)
        {
            var item = DataSource.PostsList.FirstOrDefault(x => x.Id == id);
            if (item == null)
                return Request.CreateResponse(HttpStatusCode.NotFound);

            DataSource.PostsList.Remove(item);

            //حذف کامنت‌های مرتبط
            var relatedComments = DataSource.CommentsList.Where(comment => comment.Post == id).ToList();
            relatedComments.ForEach(comment => DataSource.CommentsList.Remove(comment));

            return Request.CreateResponse(HttpStatusCode.OK, new { post = item });
        }
    }
}
قسمت سمت سرور کار تکمیل شده‌است. اما در سمت کاربر، چنین خطایی را دریافت خواهیم کرد:
 Attempted to handle event `pushedData` on  while in state root.deleted.inFlight.
منظور از حالت inFlight در اینجا این است که هنوز کار حذف سمت سرور تمام نشده‌است که متد transitionToRoute را صادر کرده‌اید. برای اصلاح آن، فایل Scripts\Controllers\post.js را باز کرده و پس از متد destroyRecord، متد then را قرار دهید:
Blogger.PostController = Ember.ObjectController.extend({
    isEditing: false,
    actions: {
        edit: function () {
            this.set('isEditing', true);
        },
        save: function () {
            var post = this.get('model');
            post.save();
 
            this.set('isEditing', false);
        },
        delete: function () {
            if (confirm('Do you want to delete this post?')) {
                var thisController = this;
                var post = this.get('model');
                post.destroyRecord().then(function () {
                    thisController.transitionToRoute('posts');
                });
            }
        }
    }
});
به این ترتیب پس از پایان عملیات حذف سمت سرور، قسمت then اجرا خواهد شد. همچنین باید دقت داشت که this اشاره کننده به کنترلر جاری را باید پیش از فراخوانی then ذخیره و استفاده کرد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
EmberJS03_05.zip
مطالب
ساخت یک Web API که از عملیات CRUD پشتیبانی می کند
در این مقاله با استفاده از ASP.NET Web API یک سرویس HTTP خواهیم ساخت که از عملیات CRUD پشتیبانی می‌کند. CRUD مخفف Create, Read, Update, Delete است که عملیات پایه دیتابیسی هستند. بسیاری از سرویس‌های HTTP این عملیات را بصورت REST API هم مدل سازی می‌کنند. در مثال جاری سرویس ساده ای خواهیم ساخت که مدیریت لیستی از محصولات (Products) را ممکن می‌سازد. هر محصول شامل فیلدهای شناسه (ID)، نام، قیمت و طبقه بندی خواهد بود.

سرویس ما متدهای زیر را در دسترس قرار می‌دهد.

 Relative URl
 HTTP method
 Action
 api/products/  GET  گرفتن لیست تمام محصولات
 api/products/id/  GET  گرفتن یک محصول بر اساس شناسه
 api/products?category=category/  GET  گرفتن یک محصول بر اساس طبقه بندی
 api/products/  POST  ایجاد یک محصول جدید
 api/products/id/  PUT  بروز رسانی یک محصول
 api/products/id/  DELETE  حذف یک محصول

همانطور که مشاهده می‌کنید برخی از آدرس ها، شامل شناسه محصول هم می‌شوند. بعنوان مثال برای گرفتن محصولی با شناسه 28، کلاینت یک درخواست GET را به آدرس زیر ارسال می‌کند:

http://hostname/api/products/28

منابع

سرویس ما آدرس هایی برای دستیابی به دو نوع منبع (resource) را تعریف می‌کند:

URI
 Resource
 api/products/  لیست تمام محصولات
 api/products/id/  یک محصول مشخص

متد ها

چهار متد اصلی HTTP یعنی همان GET, PUT, POST, DELETE می‌توانند بصورت زیر به عملیات CRUD نگاشت شوند:

  • متد GET یک منبع (resource) را از آدرس تعریف شده دریافت می‌کند. متدهای GET هیچگونه تاثیری روی سرور نباید داشته باشند. مثلا حذف رکوردها با متد اکیدا اشتباه است.
  • متد PUT یک منبع را در آدرس تعریف شده بروز رسانی می‌کند. این متد برای ساختن منابع جدید هم می‌تواند استفاده شود، البته در صورتی که سرور به کلاینت‌ها اجازه مشخص کردن آدرس‌های جدید را بدهد. در مثال جاری پشتیبانی از ایجاد منابع توسط متد PUT را بررسی نخواهیم کرد.
  • متد POST منبع جدیدی می‌سازد. سرور آدرس آبجکت جدید را تعیین می‌کند و آن را بعنوان بخشی از پیام Response بر می‌گرداند.
  • متد DELETE منبعی را در آدرس تعریف شده حذف می‌کند.

نکته: متد PUT موجودیت محصول (product entity) را کاملا جایگزین میکند. به بیان دیگر، از کلاینت انتظار می‌رود که آبجکت کامل محصول را برای بروز رسانی ارسال کند. اگر می‌خواهید از بروز رسانی‌های جزئی/پاره ای (partial) پشتیبانی کنید متد PATCH توصیه می‌شود. مثال جاری متد PATCH را پیاده سازی نمی‌کند.

یک پروژه Web API جدید بسازید

ویژوال استودیو را باز کنید و پروژه جدیدی از نوع ASP.NET MVC Web Application بسازید. نام پروژه را به "ProductStore" تغییر دهید و OK کنید.

در دیالوگ New ASP.NET Project قالب Web API را انتخاب کرده و تایید کنید.

افزودن یک مدل

یک مدل، آبجکتی است که داده اپلیکیشن شما را نمایندگی می‌کند. در ASP.NET Web API می‌توانید از آبجکت‌های Strongly-typed بعنوان مدل هایتان استفاده کنید که بصورت خودکار برای کلاینت به فرمت‌های JSON, XML مرتب (Serialize) می‌شوند. در مثال جاری، داده‌های ما محصولات هستند. پس کلاس جدیدی بنام Product می‌سازیم.

در پوشه Models کلاس جدیدی با نام Product بسازید.

حال خواص زیر را به این کلاس اضافه کنید.

namespace ProductStore.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Category { get; set; }
        public decimal Price { get; set; }
    }
}

افزودن یک مخزن

ما نیاز به ذخیره کردن کلکسیونی از محصولات داریم، و بهتر است این کلکسیون از پیاده سازی سرویس تفکیک شود. در این صورت بدون نیاز به بازنویسی کلاس سرویس می‌توانیم منبع داده‌ها را تغییر دهیم. این نوع طراحی با نام الگوی مخزن یا Repository Pattern شناخته می‌شود. برای شروع نیاز به یک قرارداد جنریک برای مخزن‌ها داریم.

روی پوشه Models کلیک راست کنید و گزینه Add, New Item را انتخاب نمایید.

نوع آیتم جدید را Interface انتخاب کنید و نام آن را به IProductRepository تغییر دهید.

حال کد زیر را به این اینترفیس اضافه کنید.

namespace ProductStore.Models
{
    public interface IProductRepository
    {
        IEnumerable<Product> GetAll();
        Product Get(int id);
        Product Add(Product item);
        void Remove(int id);
        bool Update(Product item);
    }
}
حال کلاس دیگری با نام ProductRepository در پوشه Models ایجاد کنید. این کلاس قرارداد IProductRepository را پیاده سازی خواهد کرد. کد زیر را به این کلاس اضافه کنید.

namespace ProductStore.Models
{
    public class ProductRepository : IProductRepository
    {
        private List<Product> products = new List<Product>();
        private int _nextId = 1;

        public ProductRepository()
        {
            Add(new Product { Name = "Tomato soup", Category = "Groceries", Price = 1.39M });
            Add(new Product { Name = "Yo-yo", Category = "Toys", Price = 3.75M });
            Add(new Product { Name = "Hammer", Category = "Hardware", Price = 16.99M });
        }

        public IEnumerable<Product> GetAll()
        {
            return products;
        }

        public Product Get(int id)
        {
            return products.Find(p => p.Id == id);
        }

        public Product Add(Product item)
        {
            if (item == null)
            {
                throw new ArgumentNullException("item");
            }
            item.Id = _nextId++;
            products.Add(item);
            return item;
        }

        public void Remove(int id)
        {
            products.RemoveAll(p => p.Id == id);
        }

        public bool Update(Product item)
        {
            if (item == null)
            {
                throw new ArgumentNullException("item");
            }
            int index = products.FindIndex(p => p.Id == item.Id);
            if (index == -1)
            {
                return false;
            }
            products.RemoveAt(index);
            products.Add(item);
            return true;
        }
    }
}

مخزن ما لیست محصولات را در حافظه محلی نگهداری می‌کند. برای مثال جاری این طراحی کافی است، اما در یک اپلیکیشن واقعی داده‌های شما در یک دیتابیس یا منبع داده ابری ذخیره خواهند شد. همچنین استفاده از الگوی مخزن، تغییر منبع داده‌ها در آینده را راحت‌تر می‌کند.


افزودن یک کنترلر Web API

اگر قبلا با ASP.NET MVC کار کرده باشید، با مفهوم کنترلر‌ها آشنایی دارید. در ASP.NET Web API کنترلر‌ها کلاس هایی هستند که درخواست‌های HTTP دریافتی از کلاینت را به اکشن متدها نگاشت می‌کنند. ویژوال استودیو هنگام ساختن پروژه شما دو کنترلر به آن اضافه کرده است. برای مشاهد آنها پوشه Controllers را باز کنید.

  • HomeController یک کنترلر مرسوم در ASP.NET MVC است. این کنترلر مسئول بکار گرفتن صفحات وب است و مستقیما ربطی به Web API ما ندارد.
  • ValuesController یک کنترلر نمونه WebAPI است.

کنترلر ValuesController را حذف کنید، نیازی به این آیتم نخواهیم داشت. حال برای اضافه کردن کنترلری جدید مراحل زیر را دنبال کنید.

در پنجره Solution Explorer روی پوشه Controllers کلیک راست کرده و گزینه Add, Controller را انتخاب کنید.

در دیالوگ Add Controller نام کنترلر را به ProductsController تغییر داده و در قسمت Scaffolding Options گزینه Empty API Controller را انتخاب کنید.

حال فایل کنترلر جدید را باز کنید و عبارت زیر را به بالای آن اضافه نمایید.

using ProductStore.Models;
یک فیلد هم برای نگهداری وهله ای از IProductRepository اضافه کنید.
public class ProductsController : ApiController
{
    static readonly IProductRepository repository = new ProductRepository();
}

فراخوانی ()new ProductRepository طراحی جالبی نیست، چرا که کنترلر را به پیاده سازی بخصوصی از این اینترفیس گره می‌زند. بهتر است از تزریق وابستگی (Dependency Injection) استفاده کنید. برای اطلاعات بیشتر درباره تکنیک DI در Web API به این لینک مراجعه کنید.


گرفتن منابع

ProductStore API اکشن‌های متعددی در قالب متدهای HTTP GET در دسترس قرار می‌دهد. هر اکشن به متدی در کلاس ProductsController مرتبط است.

 Relative URl
 HTTP Method
 Action
 api/products/  GET  دریافت لیست تمام محصولات
 api/products/id/  GET  دریافت محصولی مشخص بر اساس شناسه
 api/products?category=category/  GET  دریافت محصولات بر اساس طبقه بندی

برای دریافت لیست تمام محصولات متد زیر را به کلاس ProductsController اضافه کنید.

public class ProductsController : ApiController
{
    public IEnumerable<Product> GetAllProducts()
    {
        return repository.GetAll();
    }
    // ....
}
نام این متد با "Get" شروع می‌شود، پس بر اساس قراردادهای توکار پیش فرض به درخواست‌های HTTP GET نگاشت خواهد شد. همچنین از آنجا که این متد پارامتری ندارد، به URl ای نگاشت می‌شود که هیچ قسمتی با نام مثلا id نداشته باشد.

برای دریافت محصولی مشخص بر اساس شناسه آن متد زیر را اضافه کنید.
public Product GetProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound); 
    }
    return item;
}

نام این متد هم با "Get" شروع می‌شود اما پارامتری با نام id دارد. این پارامتر به قسمت id مسیر درخواست شده (request URl) نگاشت می‌شود. تبدیل پارامتر به نوع داده مناسب (در اینجا int) هم بصورت خودکار توسط فریم ورک ASP.NET Web API انجام می‌شود.

متد GetProduct در صورت نامعتبر بودن پارامتر id استثنایی از نوع HttpResponseException تولید می‌کند. این استثنا بصورت خودکار توسط فریم ورک Web API به خطای 404 (Not Found) ترجمه می‌شود.

در آخر متدی برای دریافت محصولات بر اساس طبقه بندی اضافه کنید.
public IEnumerable<Product> GetProductsByCategory(string category)
{
    return repository.GetAll().Where(
        p => string.Equals(p.Category, category, StringComparison.OrdinalIgnoreCase));
}

اگر آدرس درخواستی پارامتر‌های query string داشته باشد، Web API سعی می‌کند پارامتر‌ها را با پارامتر‌های متد کنترلر تطبیق دهد. بنابراین درخواستی به آدرس "api/products?category=category" به این متد نگاشت می‌شود.

ایجاد منبع جدید

قدم بعدی افزودن متدی به ProductsController برای ایجاد یک محصول جدید است. لیست زیر پیاده سازی ساده ای از این متد را نشان می‌دهد.

// Not the final implementation!
public Product PostProduct(Product item)
{
    item = repository.Add(item);
    return item;
}
به دو چیز درباره این متد توجه کنید:

  • نام این متد با "Post" شروع می‌شود. برای ساختن محصولی جدید کلاینت یک درخواست HTTP POST ارسال می‌کند.
  • این متد پارامتری از نوع Product می‌پذیرد. در Web API پارامترهای پیچیده (complex types) بصورت خودکار با deserialize کردن بدنه درخواست بدست می‌آیند. بنابراین در اینجا از کلاینت انتظار داریم که آبجکتی از نوع Product را با فرمت XML یا JSON ارسال کند.

پیاده سازی فعلی این متد کار می‌کند، اما هنوز کامل نیست. در حالت ایده آل ما می‌خواهیم پیام HTTP Response موارد زیر را هم در بر گیرد:

  • Response code: بصورت پیش فرض فریم ورک Web API کد وضعیت را به 200 (OK) تنظیم می‌کند. اما طبق پروتکل HTTP/1.1 هنگامی که یک درخواست POST منجر به ساخته شدن منبعی جدید می‌شود، سرور باید با کد وضعیت 201 (Created) پاسخ دهد.
  • Location: هنگامی که سرور منبع جدیدی می‌سازد، باید آدرس منبع جدید را در قسمت Location header پاسخ درج کند.

ASP.NET Web API دستکاری پیام HTTP response را آسان می‌کند. لیست زیر پیاده سازی بهتری از این متد را نشان می‌دهد.

public HttpResponseMessage PostProduct(Product item)
{
    item = repository.Add(item);
    var response = Request.CreateResponse<Product>(HttpStatusCode.Created, item);

    string uri = Url.Link("DefaultApi", new { id = item.Id });
    response.Headers.Location = new Uri(uri);
    return response;
}
توجه کنید که حالا نوع بازگشتی این متد HttpResponseMessage است. با بازگشت دادن این نوع داده بجای Product، می‌توانیم جزئیات پیام HTTP response را کنترل کنیم. مانند تغییر کد وضعیت و مقدار دهی Location header.

متد CreateResponse آبجکتی از نوع HttpResponseMessage می‌سازد و بصورت خودکار آبجکت Product را مرتب (serialize) کرده و در بدنه پاسخ می‌نویسد. نکته دیگر آنکه مثال جاری، مدل را اعتبارسنجی نمی‌کند. برای اطلاعات بیشتر درباره اعتبارسنجی مدل‌ها در Web API به این لینک مراجعه کنید.


بروز رسانی یک منبع

بروز رسانی یک محصول با PUT ساده است.

public void PutProduct(int id, Product product)
{
    product.Id = id;
    if (!repository.Update(product))
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }
}
نام این متد با "Put" شروع می‌شود، پس Web API آن را به درخواست‌های HTTP PUT نگاشت خواهد کرد. این متد دو پارامتر می‌پذیرد، یکی شناسه محصول مورد نظر و دیگری آبجکت محصول آپدیت شده. مقدار پارامتر id از مسیر (route) دریافت می‌شود و پارامتر محصول با deserialize کردن بدنه درخواست.


حذف یک منبع

برای حذف یک محصول متد زیر را به کلاس ProductsController اضافه کنید.

public void DeleteProduct(int id)
{
    Product item = repository.Get(id);
    if (item == null)
    {
        throw new HttpResponseException(HttpStatusCode.NotFound);
    }

    repository.Remove(id);
}
اگر یک درخواست DELETE با موفقیت انجام شود، می‌تواند کد وضعیت 200 (OK) را بهمراه بدنه موجودیتی که وضعیت فعلی را نمایش می‌دهد برگرداند. اگر عملیات حذف هنوز در حال اجرا است (Pending) می‌توانید کد 202 (Accepted) یا 204 (No Content) را برگردانید.

در مثال جاری متد DeleteProduct نوع void را بر می‌گرداند، که فریم ورک Web API آن را بصورت خودکار به کد وضعیت 204 (No Content) ترجمه می‌کند.
بازخوردهای دوره
بوت استرپ (نگارش 3) چیست؟
سلام
میخواستم بدونم چرا به جای خواندن جی کوئری از سایت آن
 < script src = "http://code.jquery.com/jquery-latest.min.js" ></ script >
اون رو هم مثل بقیه فایل‌های جاوا اسکریپت دانلود نمیکنیم تا به صورت local استفاده کنیم؟
 < script src = "Scripts/jquery-latest.min.js  " ></ script >
اشتراک‌ها
کتابخانه overscroll
Overscroll is a jQuery Plugin and polyfill for mobile safari's overflow-scrolling style. It is intended for use on desktop browsers, with the latest version of jQuery. Demo
کتابخانه overscroll
نظرات مطالب
یکدست کردن "ی" و "ک" در ASP.NET MVC با پیاده‌سازی یک Model Binder
ظاهرن System.Web.ModelBinding برای استفاده در Asp.net Webforms هست.

The System.Web.ModelBinding namespace provides classes that enable you to bind data objects to ASP.NET Web Forms server controls.

و برای پروژه‌های mvc از فضانام مربوط به خودش یعنی system.web.mvc استفاده می‌کنیم.

فضانام سومی به نام System.Web.Http.ModelBinding  هم وجود داره که برای Asp.net Web API استفاده میشه.
نظرات مطالب
اعمال تزریق وابستگی‌ها به مثال رسمی ASP.NET Identity
سلام
من از متد OnValidateIdentity برای بررسی آخرین وضعیت کاربر و .. در بازه زمانی X استفاده می‌کنم. مشکلی که من با این متد دارم تغییرات claim‌ها رو به کوکی response اعمال می‌کنه (مثلا تغییر نام کاربر) و تغییرات تا درخواست بعدی کاربر قابل استفاده نیست یعنی CurrentPrincipal تا درخواست بعدی آپدیت نمیشه. در هنگام لاگین این موضوع به چشم نمی‌آید چون با ریدایرکت کردن صفحه request جدیدی که حاوی کوکی آپدیت شده است ایجاد میشه به دنبال اون CurrentPrincipal بروز شده است.