مطالب
استفاده از EF در اپلیکیشن های N-Tier : قسمت سوم

در قسمت قبلی بروز رسانی موجودیت‌های منفصل با WCF را بررسی کردیم. در این قسمت خواهیم دید چگونه می‌توان تغییرات موجودیت‌ها را تشخیص داد و عملیات CRUD را روی یک Object Graph اجرا کرد.

تشخیص تغییرات با Web API

فرض کنید می‌خواهیم از سرویس‌های Web API برای انجام عملیات CRUD استفاده کنیم، اما بدون آنکه برای هر موجودیت متدهایی مجزا تعریف کنیم. به بیان دیگر می‌خواهیم عملیات مذکور را روی یک Object Graph انجام دهیم. مدیریت داده‌ها هم با مدل Code-First پیاده سازی می‌شود. در مثال جاری یک اپلیکیشن کنسول خواهیم داشت که بعنوان یک کلاینت سرویس را فراخوانی می‌کند. هر پروژه نیز در Solution مجزایی قرار دارد، تا یک محیط n-Tier را شبیه سازی کنیم.

مدل زیر را در نظر بگیرید.

همانطور که می‌بینید مدل ما آژانس‌های مسافرتی و رزرواسیون آنها را ارائه می‌کند. می‌خواهیم مدل و کد دسترسی داده‌ها را در یک سرویس Web API پیاده سازی کنیم تا هر کلاینتی که به HTTP دسترسی دارد بتواند عملیات CRUD را انجام دهد. برای ساختن سرویس مورد نظر مراحل زیر را دنبال کنید:

  • در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe3.Service تغییر دهید.
  • کنترلر جدیدی بنام TravelAgentController به پروژه اضافه کنید.
  • دو کلاس جدید با نام‌های TravelAgent و Booking بسازید و کد آنها را مطابق لیست زیر تغییر دهید.
public class TravelAgent
{
    public TravelAgent()
    {
        this.Bookings = new HashSet<Booking>();
    }

    public int AgentId { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Booking> Bookings { get; set; }
}

public class Booking
{
    public int BookingId { get; set; }
    public int AgentId { get; set; }
    public string Customer { get; set; }
    public DateTime BookingDate { get; set; }
    public bool Paid { get; set; }
    public virtual TravelAgent TravelAgent { get; set; }
}
  • با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
  • کلاس جدیدی بنام Recipe3Context بسازید و کد آن را مطابق لیست زیر تغییر دهید.
public class Recipe3Context : DbContext
{
    public Recipe3Context() : base("Recipe3ConnectionString") { }
    public DbSet<TravelAgent> TravelAgents { get; set; }
    public DbSet<Booking> Bookings { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<TravelAgent>().HasKey(x => x.AgentId);
        modelBuilder.Entity<TravelAgent>().ToTable("TravelAgents");
        modelBuilder.Entity<Booking>().ToTable("Bookings");
    }
}

  • فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه کنید.
<connectionStrings>
  <add name="Recipe3ConnectionString"
    connectionString="Data Source=.;
    Initial Catalog=EFRecipes;
    Integrated Security=True;
    MultipleActiveResultSets=True"
    providerName="System.Data.SqlClient" />
</connectionStrings>
  • فایل Global.asax را باز کنید و کد زیر را به متد Application_Start اضافه نمایید. این کد بررسی Model Compatibility در EF را غیرفعال می‌کند. همچنین به JSON serializer می‌گوییم که self-referencing loop خاصیت‌های پیمایشی را نادیده بگیرد. این حلقه بدلیل ارتباط bidirectional بین موجودیت‌ها بوجود می‌آید.
protected void Application_Start()
{
    // Disable Entity Framework Model Compatibilty
    Database.SetInitializer<Recipe1Context>(null);

    // The bidirectional navigation properties between related entities
    // create a self-referencing loop that breaks Web API's effort to
    // serialize the objects as JSON. By default, Json.NET is configured
    // to error when a reference loop is detected. To resolve problem,
    // simply configure JSON serializer to ignore self-referencing loops.
    GlobalConfiguration.Configuration.Formatters.JsonFormatter
        .SerializerSettings.ReferenceLoopHandling =
        Newtonsoft.Json.ReferenceLoopHandling.Ignore;
    ...
}
  • فایل RouteConfig.cs را باز کنید و قوانین مسیریابی را مانند لیست زیر تغییر دهید.
public static void Register(HttpConfiguration config)
{
    config.Routes.MapHttpRoute(
      name: "ActionMethodSave",
      routeTemplate: "api/{controller}/{action}/{id}",
      defaults: new { id = RouteParameter.Optional });
}
  • در آخر کنترلر TravelAgent را باز کنید و کد آن را مطابق لیست زیر بروز رسانی کنید.
public class TravelAgentController : ApiController
{
    // GET api/travelagent
    [HttpGet]
    public IEnumerable<TravelAgent> Retrieve()
    {
        using (var context = new Recipe3Context())
        {
            return context.TravelAgents.Include(x => x.Bookings).ToList();
        }
    }

    /// <summary>
    /// Update changes to TravelAgent, implementing Action-Based Routing in Web API
    /// </summary>
    public HttpResponseMessage Update(TravelAgent travelAgent)
    {
        using (var context = new Recipe3Context())
        {
            var newParentEntity = true;
            // adding the object graph makes the context aware of entire
            // object graph (parent and child entities) and assigns a state
            // of added to each entity.
            context.TravelAgents.Add(travelAgent);
            if (travelAgent.AgentId > 0)
            {
                // as the Id property has a value greater than 0, we assume
                // that travel agent already exists and set entity state to
                // be updated.
                context.Entry(travelAgent).State = EntityState.Modified;
                newParentEntity = false;
            }

            // iterate through child entities, assigning correct state.
            foreach (var booking in travelAgent.Bookings)
            {
                if (booking.BookingId > 0)
                    // assume booking already exists if ID is greater than zero.
                    // set entity to be updated.
                    context.Entry(booking).State = EntityState.Modified;
            }

            context.SaveChanges();
            HttpResponseMessage response;
            // set Http Status code based on operation type
            response = Request.CreateResponse(newParentEntity ? HttpStatusCode.Created : HttpStatusCode.OK, travelAgent);
            return response;
        }
    }

    [HttpDelete]
    public HttpResponseMessage Cleanup()
    {
        using (var context = new Recipe3Context())
        {
            context.Database.ExecuteSqlCommand("delete from [bookings]");
            context.Database.ExecuteSqlCommand("delete from [travelagents]");
        }
        return Request.CreateResponse(HttpStatusCode.OK);
    }
}

در قدم بعدی کلاینت پروژه را می‌سازیم که از سرویس Web API مان استفاده می‌کند.

  • در ویژوال استودیو پروژه جدیدی از نوع Console application بسازید و نام آن را به Recipe3.Client تغییر دهید.
  • فایل program.cs را باز کنید و کد آن را مطابق لیست زیر بروز رسانی کنید.
internal class Program
{
    private HttpClient _client;
    private TravelAgent _agent1, _agent2;
    private Booking _booking1, _booking2, _booking3;
    private HttpResponseMessage _response;

    private static void Main()
    {
        Task t = Run();
        t.Wait();
        Console.WriteLine("\nPress <enter> to continue...");
        Console.ReadLine();
    }

    private static async Task Run()
    {
        var program = new Program();
        program.ServiceSetup();
        // do not proceed until clean-up is completed
        await program.CleanupAsync();
        program.CreateFirstAgent();
        // do not proceed until agent is created
        await program.AddAgentAsync();
        program.CreateSecondAgent();
        // do not proceed until agent is created
        await program.AddSecondAgentAsync();
        program.ModifyAgent();
        // do not proceed until agent is updated
        await program.UpdateAgentAsync();
        // do not proceed until agents are fetched
        await program.FetchAgentsAsync();
    }

    private void ServiceSetup()
    {
        // set up infrastructure for Web API call
        _client = new HttpClient {BaseAddress = new Uri("http://localhost:6687/")};
        // add Accept Header to request Web API content negotiation to return resource in JSON format
        _client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
    }

    private async Task CleanupAsync()
    {
        // call cleanup method in service
        _response = await _client.DeleteAsync("api/travelagent/cleanup/");
    }

    private void CreateFirstAgent()
    {
        // create new Travel Agent and booking
        _agent1 = new TravelAgent {Name = "John Tate"};
        _booking1 = new Booking
        {
            Customer = "Karen Stevens",
            Paid = false,
            BookingDate = DateTime.Parse("2/2/2010")
        };

        _booking2 = new Booking
        {
            Customer = "Dolly Parton",
            Paid = true,
            BookingDate = DateTime.Parse("3/10/2010")
        };
  
        _agent1.Bookings.Add(_booking1);
        _agent1.Bookings.Add(_booking2);
    }

    private async Task AddAgentAsync()
    {
        // call generic update method in Web API service to add agent and bookings
        _response = await _client.PostAsync("api/travelagent/update/",
            _agent1, new JsonMediaTypeFormatter());

        if (_response.IsSuccessStatusCode)
        {
            // capture newly created travel agent from service, which will include
            // database-generated Ids for each entity
            _agent1 = await _response.Content.ReadAsAsync<TravelAgent>();
            _booking1 = _agent1.Bookings.FirstOrDefault(x => x.Customer == "Karen Stevens");
            _booking2 = _agent1.Bookings.FirstOrDefault(x => x.Customer == "Dolly Parton");

            Console.WriteLine("Successfully created Travel Agent {0} and {1} Booking(s)",
            _agent1.Name, _agent1.Bookings.Count);
        }
        else
            Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase);
    }

    private void CreateSecondAgent()
    {
        // add new agent and booking
        _agent2 = new TravelAgent {Name = "Perry Como"};
        _booking3 = new Booking {
            Customer = "Loretta Lynn",
            Paid = true,
            BookingDate = DateTime.Parse("3/15/2010")};
        _agent2.Bookings.Add(_booking3);
    }

    private async Task AddSecondAgentAsync()
    {
        // call generic update method in Web API service to add agent and booking
        _response = await _client.PostAsync("api/travelagent/update/", _agent2, new JsonMediaTypeFormatter());

        if (_response.IsSuccessStatusCode)
        {
            // capture newly created travel agent from service
            _agent2 = await _response.Content.ReadAsAsync<TravelAgent>();
            _booking3 = _agent2.Bookings.FirstOrDefault(x => x.Customer == "Loretta Lynn");
            Console.WriteLine("Successfully created Travel Agent {0} and {1} Booking(s)",
                _agent2.Name, _agent2.Bookings.Count);
        }
        else
            Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase);
    }

    private void ModifyAgent()
    {
        // modify agent 2 by changing agent name and assigning booking 1 to him from agent 1
        _agent2.Name = "Perry Como, Jr.";
        _agent2.Bookings.Add(_booking1);
    }

    private async Task UpdateAgentAsync()
    {
        // call generic update method in Web API service to update agent 2
        _response = await _client.PostAsync("api/travelagent/update/", _agent2, new JsonMediaTypeFormatter());
        if (_response.IsSuccessStatusCode)
        {
            // capture newly created travel agent from service, which will include Ids
            _agent1 = _response.Content.ReadAsAsync<TravelAgent>().Result;
            Console.WriteLine("Successfully updated Travel Agent {0} and {1} Booking(s)", _agent1.Name, _agent1.Bookings.Count);
        }
        else
            Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase);
    }

    private async Task FetchAgentsAsync()
    {
        // call Get method on service to fetch all Travel Agents and Bookings
        _response = _client.GetAsync("api/travelagent/retrieve").Result;
        if (_response.IsSuccessStatusCode)
        {
            // capture newly created travel agent from service, which will include Ids
            var agents = await _response.Content.ReadAsAsync<IEnumerable<TravelAgent>>();

            foreach (var agent in agents)
            {
                Console.WriteLine("Travel Agent {0} has {1} Booking(s)", agent.Name, agent.Bookings.Count());
            }
        }
        else
            Console.WriteLine("{0} ({1})", (int) _response.StatusCode, _response.ReasonPhrase);
    }
}
  • در آخر کلاس‌های TravelAgent و Booking را به پروژه کلاینت اضافه کنید. اینگونه کدها بهتر است در لایه مجزایی قرار گیرند و بین پروژه‌ها به اشتراک گذاشته شوند.

اگر اپلیکیشن کنسول (کلاینت) را اجرا کنید با خروجی زیر مواجه خواهید شد.

(Successfully created Travel Agent John Tate and 2 Booking(s
(Successfully created Travel Agent Perry Como and 1 Booking(s
(Successfully updated Travel Agent Perry Como, Jr. and 2 Booking(s
(Travel Agent John Tate has 1 Booking(s
(Travel Agent Perry Como, Jr. has 2 Booking(s


شرح مثال جاری

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

سپس اپلیکیشن کنسول را باز کنید، روی خط اول کد فایل program.cs یک breakpoint قرار دهید و آن را اجرا کنید. ابتدا آدرس سرویس Web API را نگاشت می‌کنیم و با تنظیم مقدار خاصیت Accept Header از سرویس درخواست می‌کنیم که اطلاعات را با فرمت JSON بازگرداند.

بعد از آن با استفاده از آبجکت HttpClient متد DeleteAsync را فراخوانی می‌کنیم که روی کنترلر TravelAgent تعریف شده است. این متد تمام داده‌های پیشین را حذف میکند.

در قدم بعدی سه آبجکت جدید می‌سازیم: یک آژانس مسافرتی و دو رزرواسیون. سپس این آبجکت‌ها را با فراخوانی متد PostAsync روی آبجکت HttpClient به سرویس ارسال می‌کنیم. اگر به متد Update در کنترلر TravelAgent یک breakpoint اضافه کنید، خواهید دید که این متد آبجکت آژانس مسافرتی را بعنوان یک پارامتر دریافت می‌کند و آن را به موجودیت TravelAgents در Context جاری اضافه می‌نماید. این کار آبجکت آژانس مسافرتی و تمام آبجکت‌های فرزند آن را در حالت Added اضافه می‌کند و باعث می‌شود که context جاری شروع به ردیابی (tracking) آنها کند.

نکته: قابل ذکر است که اگر موجودیت‌های متعددی با مقداری یکسان در خاصیت کلید اصلی (Primary-key value) دارید باید مجموعه آبجکت‌های خود را Add کنید و نه Attach. در مثال جاری چند آبجکت Booking داریم که مقدار کلید اصلی آنها صفر است (Bookings with Id = 0). اگر از Attach استفاده کنید EF پیغام خطایی صادر می‌کند چرا که چند موجودیت با مقادیر کلید اصلی یکسان به context جاری اضافه کرده اید.

بعد از آن بر اساس مقدار خاصیت Id مشخص می‌کنیم که موجودیت‌ها باید بروز رسانی شوند یا خیر. اگر مقدار این فیلد بزرگتر از صفر باشد، فرض بر این است که این موجودیت در دیتابیس وجود دارد بنابراین خاصیت EntityState را به Modified تغییر می‌دهیم. علاوه بر این فیلدی هم با نام newParentEntity تعریف کرده ایم که توسط آن بتوانیم کد وضعیت مناسبی به کلاینت بازگردانیم. در صورتی که مقدار فیلد Id در موجودیت TravelAgent برابر با یک باشد، مقدار خاصیت EntityState را به همان Added رها می‌کنیم.

سپس تمام آبجکت‌های فرزند آژانس مسافرتی (رزرواسیون ها) را بررسی میکنیم و همین منطق را روی آنها اعمال می‌کنیم. یعنی در صورتی که مقدار فیلد Id آنها بزرگتر از 0 باشد وضعیت EntityState را به Modified تغییر می‌دهیم. در نهایت متد SaveChanges را فراخوانی می‌کنیم. در این مرحله برای موجودیت‌های جدید اسکریپت‌های Insert و برای موجودیت‌های تغییر کرده اسکریپت‌های Update تولید می‌شود. سپس کد وضعیت مناسب را به کلاینت بر می‌گردانیم. برای موجودیت‌های اضافه شده کد وضعیت 201 (Created) و برای موجودیت‌های بروز رسانی شده کد وضعیت 200 (OK) باز می‌گردد. کد 201 به کلاینت اطلاع می‌دهد که رکورد جدید با موفقیت ثبت شده است، و کد 200 از بروز رسانی موفقیت آمیز خبر می‌دهد. هنگام تولید سرویس‌های REST-based بهتر است همیشه کد وضعیت مناسبی تولید کنید.

پس از این مراحل، آژانس مسافرتی و رزرواسیون جدیدی می‌سازیم و آنها را به سرویس ارسال می‌کنیم. سپس نام آژانس مسافرتی دوم را تغییر می‌دهیم، و یکی از رزرواسیون‌ها را از آژانس اولی به آژانس دومی منتقل می‌کنیم. اینبار هنگام فراخوانی متد Update تمام موجودیت‌ها شناسه ای بزرگتر از 1 دارند، بنابراین وضعیت EntityState آنها را به Modified تغییر می‌دهیم تا هنگام ثبت تغییرات دستورات بروز رسانی مناسب تولید و اجرا شوند.

در آخر کلاینت ما متد Retreive را روی سرویس فراخوانی می‌کند. این فراخوانی با کمک متد GetAsync انجام می‌شود که روی آبجکت HttpClient تعریف شده است. فراخوانی این متد تمام آژانس‌های مسافرتی بهمراه رزرواسیون‌های متناظرشان را دریافت می‌کند. در اینجا با استفاده از متد Include تمام رکوردهای فرزند را بهمراه تمام خاصیت هایشان (properties) بارگذاری می‌کنیم.

دقت کنید که مرتب کننده JSON تمام خواص عمومی (public properties) را باز می‌گرداند، حتی اگر در کد خود تعداد مشخصی از آنها را انتخاب کرده باشید.

نکته دیگر آنکه در مثال جاری از قرارداد‌های توکار Web API برای نگاشت درخواست‌های HTTP به اکشن متدها استفاده نکرده ایم. مثلا بصورت پیش فرض درخواست‌های POST به متدهایی نگاشت می‌شوند که نام آنها با "Post" شروع می‌شود. در مثال جاری قواعد مسیریابی را تغییر داده ایم و رویکرد مسیریابی RPC-based را در پیش گرفته ایم. در اپلیکیشن‌های واقعی بهتر است از قواعد پیش فرض استفاده کنید چرا که هدف Web API ارائه سرویس‌های REST-based است. بنابراین بعنوان یک قاعده کلی بهتر است متدهای سرویس شما به درخواست‌های متناظر HTTP نگاشت شوند. و در آخر آنکه بهتر است لایه مجزایی برای میزبانی کدهای دسترسی داده ایجاد کنید و آنها را از سرویس Web API تفکیک نمایید.

نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 14 - فعال سازی اعتبارسنجی ورودی‌های کاربران
باسلام. اگر بخواهیم متدهای اعتبارسنجی را از مدل بگیرد و بصورت جاوااسکریپتی و قبل از مراجعه مجدد به سرور اعتبارسنجی را انجام دهد چکار باید بکنیم؟
[Required]
public string Title { get; set; }

مطالب
مرتب‌سازی، فیلتر کردن و صفحه‌بندی اطلاعات در ASP.NET Core

مقدمه

اگر با Apiها کار کرده باشید احتمالاً با این چالش که گاهی نیاز است منابعی (Resources) که به کاربر ارسال می‌شوند مرتب (Sort)، بر اساس درخواست کاربر فیلتر (Filter) و در صفحه‌بندی (Paging) مشخصی تحویل داده شوند، برخورد کرده‌اید. این نیاز خصوصاً در پاسخ (Response) با روش GET از استاندارد HTTP مشهود است. در این مطلب به معرفی کتابخانه‌ای می‌پردازیم که با استفاده از آن می‌توان عملیات فوق را پیاده‌سازی نمود. Sieve یک چارچوب (Framework) ساده، تمیز و قابل توسعه برای NET Core. است. در زمان نگارش این مقاله ویرایش ۲.۱.۳ از این کتابخانه در دسترس است و همانگونه که اشاره شد، این کتابخانه منبع باز (Open Source) بوده و می‌توانید آن را از مخزن گیت‌هاب در این پیوند دریافت نمایید.
  
سیستمی با یک موجودیت به نام "پست" (Post) مفروض است. با استفاده از کتابخانه Sieve عملیات مرتب‌سازی، فیلتر و صفحه‌بندی را هنگام درخواست (GET) تمامی پست‌ها اعمال خواهیم کرد.
// Post

public int Id { get; set; }

public string Title { get; set; }

public int LikeCount { get; set; }

public int CommentCount { get; set; }

public DateTimeOffset DateCreated { get; set; } = DateTimeOffset.UtcNow;

۱. نصب کتابخانه

ابتدا لازم است از طریق Package Manager Console و اجرای دستور فوق اقدام به نصب این کتابخانه نمایید:  Install-Package Sieve -Version 2.1.3

۲. اضافه کردن سرویس‌

در فایل Startup.cs سرویس SieveProcessor را تزریق کنید:
services.AddScoped<SieveProcessor>();
 

۳. تعیین ویژگی‌هایی از کلاس برای اعمال مرتب‌سازی و فیلتر

باید ویژگی‌هایی (Properties) از کلاس را که می‌خواهید اعمال مرتب‌سازی و فیلتر بر روی آن‌ها انجام شوند، مشخص کنید. به دو روش این امر ممکن است:
 

۱.۳. از طریق اضافه کردن صفت (Attribute) به ویژگی‌ها

تنها ویژگی‌هایی از کلاس که دارای صفت [(Sieve(CanSort = true, CanFilter = true] باشند، می‌توانند مرتب و یا فیلتر شوند (می‌توان تنها از یکی از آن‌ها نیز استفاده نمود).
لذا کلاس پست به صورت زیر ویرایش می‌شود:
// Post

public int Id { get; set; }

[Sieve(CanFilter = true, CanSort = true)]
public string Title { get; set; }

[Sieve(CanFilter = true, CanSort = true)]
public int LikeCount { get; set; }

[Sieve(CanFilter = true, CanSort = true)]
public int CommentCount { get; set; }

[Sieve(CanFilter = true, CanSort = true, Name = "created")]
public DateTimeOffset DateCreated { get; set; } = DateTimeOffset.UtcNow;

 

۲.۳. از طریق Fluent API

برای استفاده از این روش، ابتدا کلاسی را ایجاد و از کلاس SieveProcessor مشتق کنید. سپس تابع MapProperties موجود در کلاس والد را override کنید.
// ApplicationSieveProcessor

public class ApplicationSieveProcessor : SieveProcessor
{
    public ApplicationSieveProcessor(
        IOptions<SieveOptions> options, 
        ISieveCustomSortMethods customSortMethods, 
        ISieveCustomFilterMethods customFilterMethods) 
        : base(options, customSortMethods, customFilterMethods)
    {
    }

    protected override SievePropertyMapper MapProperties(SievePropertyMapper mapper)
    {
        mapper.Property<Post>(p => p.Title)
            .CanFilter()
            .HasName("a_different_query_name_here");

        mapper.Property<Post>(p => p.CommentCount)
            .CanSort();

        mapper.Property<Post>(p => p.DateCreated)
            .CanSort()
            .CanFilter()
            .HasName("created_on");

        return mapper;
    }
}
حال باید کلاس جدید را تزریق نمایید:
services.AddScoped<ISieveProcessor, ApplicationSieveProcessor>();
در هر دو روش پارامتری دیگر با نام "Name" نیز وجود دارد که می‌توانید با استفاده از آن برای هر ویژگی، نامی غیر از نام اصلی آن را اتخاذ نمایید.
 

۴. دریافت پرس‌و‌جوهای (Queries) مرتب/فیلتر/صفحه‌بندی با اضافه کردن SieveModel به کنترلر (Controller)

برای دریافت پرس‌و‌جوهای مرتب/فیلتر/صفحه‌بندی، SieveModel را به اکشنی (Action) که پست‌ها را برگشت می‌دهد به عنوان پارامتر اضافه کنید. سپس با فراخوانی تابع Apply با استفاده از تزریق SieveProcessor در کنترلر خود، پرس‌و‌جوها را به منابع خود اعمال کنید.
[HttpGet]
public JsonResult GetPosts(SieveModel sieveModel) 
{
    var result = _dbContext.Posts;
    result = _sieveProcessor.Apply(sieveModel, result);
    return Json(result.ToList());
}
توجه داشته باشید مقادیر پرس‌و‌جوها اختیاری است و هر کدام می‌توانند به تنهایی و یا با هم مورد استفاده قرار گیرند.
 

۵. ارسال درخواست

با تمام موارد گفته شده، اکنون می‌توانید درخواستی را برای دریافت (GET) شامل پرس‌و‌جوهای مرتب/فیلتر/صفحه‌بندی ارسال نمایید. برای مثال:
GET /GetPosts

?sorts=     LikeCount,CommentCount,-created         // sort by likes, then comments, then descendingly by date created 
&filters=   LikeCount>10,Title@=awesome title,      // filter to posts with more than 10 likes, and a title that contains the phrase "awesome title"
&page=      1                                       // get the first page...
&pageSize=  10                                      // ...which contains 10 posts
sorts= LikeCount,CommentCount,-created ?: مرتب‌سازی بر اساس تعداد محبوبیت، سپس تعداد نظرات و در آخر تاریخ ثبت پست به صورت نزولی.
filters= LikeCount>10,Title@=awesome title &: فیلتر بر اساس پست‌هایی که تعداد محبوبیت آن‌ها بیش از ۱۰ است و در عنوان خود شامل عبارت "awesome title" می‌باشند.
page=1 &: صفحه اول ...
pageSize=10 &: ... که شامل ۱۰ پست است.

به صورت رسمی‌تر:
sorts: فهرست دستورالعمل‌هایی شامل نام ویژگی‌هایی است که مرتب‌سازی بر روی آن‌ها اعمال می‌شود و از طریق کاما (,) از یکدیگر تمایز داده می‌شوند. با اضافه کردن - قبل از نام ویژگی، آن را به صورت نزولی مرتب نمایید.
  • filters: دستورالعمل‌های جدا شده توسط کاما (,) به صورت {Name}{Operator}{Value} که در آن:
    • {Name} نام ویژگی‌ای است که صفت Sieve بر روی آن تعریف شده و یا نام سفارشی‌ای است که کاربر تعیین کرده است.
      • همچنین می‌توانید بیش از یک نام (برای یای منطقی (OR)) در درون جفت پرانتز باز و بسته و جداکننده یای منطقی (|) داشته باشید. برای مثال: LikeCount|CommentCount)>10) مشخص می‌کند مقدار LikeCount و یا CommentCount بیش از ۱۰ باشد.
    • {Operator} یکی از عملگرهای ممکن است.
    • {Value} مقداری است که در عمل فیلتر مورد استفاده قرار می‌گیرد.
  • page: شماره صفحه‌ای است که برگشت داده می‌شود.
  • pageSize: تعداد مواردی است که در هر صفحه برگردانده خواهد شد.
 

۶. عملگرها (Operators)

عملگر
توضیحات
عملگر
توضیحات
== برابر =@  شامل
=! مخالف
=_  شروع شود با
< بزرگ‌تر
*=@  شامل (حساس به حروف)*
> کوچک‌تر
*=_  شروع شود با (حساس به حروف)
=< بزرگ‌تر مساوی
*==  برابر (حساس به حروف)
=> کوچک‌تر مساوی

 
* حساس به بزرگی و کوچکی حروف

۷. پیکربندی

برای پیکربندی شامل مواردی چون حساس به بزرگ و کوچک بودن نام ویژگی، تعداد صفحات پیشفرض، حداکثر تعداد صفحه مجاز و نحوه برخورد با نام ویژگی ناموجود در ویژگی‌های کلاس ابتدا قطعه زیر را به appsettings اضافه کنید.
{
    "Sieve": {
        "CaseSensitive": "boolean: should property names be case-sensitive? Defaults to false",
        "DefaultPageSize": "int number: optional number to fallback to when no page argument is given. Set <=0 to disable paging if no pageSize is specified (default).",
        "MaxPageSize": "int number: maximum allowed page size. Set <=0 to make infinite (default)",
        "ThrowExceptions": "boolean: should Sieve throw exceptions instead of silently failing? Defaults to false"
    }
}

 سپس سرویس فوق را در Startup.cs اضافه کنید:
services.Configure<SieveOptions>(Configuration.GetSection("Sieve"));

مطالب
اصول طراحی شیء گرا: OO Design Principles - قسمت سوم

اصل هفتم: Liskove Substitution Principle

"ارث بری باید به صورتی باشد که زیر نوع را بتوان بجای ابر نوع استفاده کرد"

این اصل می‌گوید اگر قرار است از ارث بری استفاده شود، نحوه‌ی استفاده باید بدین گونه باشد که اگر یک شیء از کلاس والد ( Base-Parent-Super type ) داشته باشیم، باید بتوان آن را با شیء کلاس فرزند ( Sub Type-Child ) بدون هیچ گونه تغییری در منطق کد استفاده کننده از شیء مورد نظر، تغییر داد. به زبان ساده باید بتوان شیء فرزند را جایگزین شیء والد کرد.

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

با رعایت این اصل، میتوانیم در مواقعی که شروط مرتبط با کلاس فرزند را نداریم و یک سری منطق و قیود کلی مرتبط با کلاس والد را داریم، از شیء کلاس والد استفاده نماییم و وظیفه  نمونه گیری (instantiation ) آن را به یک کلاس دیگر محول کنیم. به مثال زیر توجه کنید:

 public class Parent
    {
        public string Name { get; set; }
        public int X { get; set; }
        public int Y { get; set; }
        public Parent()
        {
            X = Y = 0;
        }
        public virtual void Move()
        {
            X += 5;
            Y += 5;
        }
        public void Shoot() { }
        public virtual void Pass() { } 
    }
    public class Child1 : Parent
    {
        public override void Move()
        {
            X += 10;
            Y += 10;
        }
    }
    public class Child2 : Parent
    {
        public override void Move()
        {
            X += 20;
            Y += 20;
        }
    }
    public enum State
    {
        Start,
        Move,
        Shoot,
        Pass
    }
    public class Creator
    {
        public static Parent GetInstance(bool? condition)
        {
            if (condition == null)
            {
                return new Parent();
            }
            if (condition == true)
            {
                return new Child1();
            }
            else
            {
                return new Child2();
            }
        }
    }
    public class Context
    {
        public void SetState(ref State s)
        {
            s = State.Move;
        }
        public void Main()
        {
            State state =State.Start;
            
            // در مورد نوع این شیء چیزی نمیدانیم و وابسته به شرایط نوع آن متغیر است
            // در حقیقت شیء کلاس فرزند را جای شیء کلاس والد  قرار می‌دهیم و نه بالعکس

            Parent obj = Creator.GetInstance(null);
            
            // منطق برنامه وضعیت را تغییر می‌دهد
            SetState(ref state);

            // قواعد کلی و عمومی که بدون در نظر گرفتن کلاس (نوع) شیء بر آن اعمال می‌شود
            switch (state)
            {
                case State.Move:
                    obj.Move();
                    break;
                case State.Shoot:
                    obj.Shoot();
                    break;
                case State.Pass:
                    obj.Pass();
                    break;
                default:
                    break;
            }           
        }
    }

همانطور که در کدها نیز توضیح داده‌ام، کلاس‌های فرزند را جایگزین کلاس والد کرده‌ایم. اگر می‌خواستیم عکس رابطه را (شیء والد را به شیء فرزند انتقال دهیم) اعمال کنیم باید تغییر زیر را ایجاد میکردیم که با خطا روبرو خواهد شد:

Child1 obj = Creator.GetInstance(null);



اصل هشتم: Interface segregation

"واسط‌های کوچک بهتر از واسط‌های حجیم است"

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

 public interface IHuman
    {
        void Move();
        void Eat();
        void LevelUp();
        void FireBullet();
    }
    public class Player : IHuman
    {
        public void Eat() { }
        public void FireBullet() { }
        public void LevelUp() { }
        public void Move() { }
    }
    public class Enemy : IHuman
    {
        public void Eat() { }
        public void FireBullet() { }
        public void LevelUp() { }
        public void Move() { }
    }
    public class Citizen : IHuman
    {
        public void Eat() { }
        public void FireBullet() { }
        public void LevelUp() { }
        public void Move() { }
    }

در این مثال که مربوط به مدل یک بازی با نقش‌های بازیکن، دشمن و شهروند (بی گناه!) است، طراحی به گونه‌ای است که دشمن و شهروند، توابعی را که نیاز ندارند، باید پیاده سازی کنند. در دشمن: Eat(), LevelUp() و در شهروند: Eat(), LevelUp(), FireBullet() . لذا واسط IHuman یک واسط کلی با وظیفه‌های متعدد است.

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

public interface IMovable { void Move(); }
    public interface IEatable { void Eat(); }
    public interface IPlayer { void LevelUp(); }
    public interface IShooter { void FireBullet(); }
    public class PlayerBetter : IPlayer, IMovable, IEatable, IShooter
    {
        public void Eat() { }
        public void FireBullet() { }
        public void LevelUp() { }
        public void Move() { }
    }
    public class EnemyBetter : IMovable, IShooter
    {
        public void FireBullet() { }
        public void Move() { }
    }
    public class CitizenBetter : IMovable
    {
        public void Move() { }
    }

در اینجا برای هر وظیفه یک واسط تعریف کرده ایم که باعث قوی شدن معنای هر واسط می‌شود.



اصل نهم: Dependency inversion

"وابستگی بین ماژول‌ها  را به وابستگی آن‌ها به انتزاع (واسط) تغییر بده"

این اصل که نمود آن را در الگو‌های طراحی dependency injection و factory میبینیم، میگوید که ماژول‌های بالادست (ماژول استفاده کننده ماژول پایین دست) به جای آنکه ارجاع مستقیمی را به ماژول‌های پایین دست داشته باشند، به انتزاعی (واسط) ارجاع بدهند که ماژول پایین دست آنرا پیاده سازی می‌کند یا به ارث میبرد. در واقع این اصل برای از بین بردن وابستگی قوی بین ماژول‌های بالا دست و پایین دست، به میدان آمده است. دو حکم اصلی از این اصل بر می‌آید:

الف – ماژول‌های بالا دست نباید وابسته به ماژول‌های پایین دست باشند. هر دو باید وابسته به انتزاع (واسط) باشند. وابستگی ماژول بالا دست از نوع ارجاع و وابستگی ماژول پایین دست از نوع ارث بری است.

ب – انتزاع نباید وابسته به جزییات باشد، بلکه جزییات باید وابسته به انتزاع باشد. یعنی در پیاده سازی منطق برنامه (که جزییات محسوب می‌شود) باید از واسط‌ها یا کلاس‌های انتزاعی استفاده کنیم و همچنین در نوشتن کلاس‌های انتزاعی نباید هیچ گونه ارجاعی را به کلاس‌های جزیی داشته باشیم.

در مثال زیر با نمونه‌ای از طراحی ناقض این اصل روبرو هستیم:

public class Controller
    {
        public Service Service { get; set; }
        public Controller()
        {
            // کنترلر باید نحوه نمونه گیری را بداند (ورودی‌های لازم) و این از وظایف آن خارج است
            Service = new Service(1);
        }
        public void DoWork()
        {
            Service.RunService();
        }
    }

    public class Service
    {
        public int State { get; set; }
        public Service(int s)
        {
            State = s;
        }
        public void RunService() { }
    }

در این مثال کلاس کنترلر، ماژول بالادست و کلاس سرویس، ماژول پایین دست محسوب میگردد. در ادامه طراحی مطلوب را نیز ارائه داده‌ام:

public class ControllerBetter
    {
        // ارجاع به واسط باعث انعطاف و کاهش وابستگی شده است
        public IService Service { get; set; }
        public ControllerBetter(IService service)
        {
            // یک کلاس دیگر وظیفه ارسال سرویس به سازنده کلاس کنترلر را دارد 
            // و مسئولیت نمونه گیری را از دوش کنترلر برداشته است
            Service = service;
        }
        public void DoWork()
        {
            Service.RunService();
        }
    }
    // کاهش وابستگی با تعریف واسط و تغییر وابستگی مستقیم بین کنترلر و سرویس
    public interface IService
    {
        void RunService();
    }
    // وابستگی جزییات به انتزاع
    public class ServiceBetter : IService
    {
        public int State { get; set; }
        public ServiceBetter(int s)
        {
            State = s;
        }
        public void RunService() { }
    }

نحوه بهبود طراحی را در توضیحات داخل کد مشاهده میکنید. در مقاله بعدی به اصول GRASP خواهم پرداخت. 

نظرات مطالب
اصول طراحی شی گرا SOLID - #بخش سوم اصل LSP
ممنون.
کلاس های Rectangle و Square  هر دو به همون شکل باقی میمونند با این تفاوت که هر دو از کلاس Shape مشتق شده اند و میتوانند خاصیت‌های Width و Height را طبق نیاز خود دوباره نویسی کنند (override).
کلاس Restangle:
public class Rectangle : Shape
{
    //شما میتوانید خاصیت‌ها طول و عرض در کلاس پایه را در صورت نیاز دوباره نویسی کنید
}
کلاس Square :
public class Square : Shape
{
    //دوباره نویسی کردن خاصیت‌های طول و عرض در کلاس پایه جهت برابر کردن طول و عرض مربع
    public override int Width
    {
        get{return base.Width;}
        set
        {
            base.Height = value;
            base.Width = value;
        }
    }
    public override int Height
    {
        get{return base.Height;}
        set
        {
            base.Height = value;
            base.Width = value;
        }
    }        
}
که با توجه به کدهای بالا ، کلاسهای مشتق شده‌ی Square و Restangle میتوانند جایگزین کلاس پایه خود یعنی Shape شوند :
Shape o = new Rectangle();
o.Width = 5;
o.Height = 6;
 
Shape o = new Square();
o.Width = 5; //طول و عرض هر دو برابر 5 میشوند
o.Height = 6; //عرض و طول هر دو برابر 6 میشوند

مطالب
Bind کردن Enum به ItemsSource در XAML
شاید تا به حال در یک برنامه سازمانی نیاز به Bind کردن یک Enum به کنترل‌های XAML به چشمتان خورده باشد ، روشی که من برای این کار استفاده می‌کنم توسط یک Markup Extension به صورت زیر است :
public class ByteEnumerationExtention : MarkupExtension
    {
        public ByteEnumerationExtention(Type enumType)
        {
            this.enumType = enumType;
        }

        private Type enumType;

        public Type EnumType
        {
            get { return enumType; }
            private set
            {
                enumType = value;
            }
        }

        public override object ProvideValue(IServiceProvider serviceProvider)
        {
            return (from x in Enum.GetValues(EnumType).OfType<Enum>()
                    select new EnumerationMember
                    {
                        Value = GetValue(x),
                        Description = GetDescription(x)
                    }).ToArray();
        }

        private byte? GetValue(object enumValue)
        {
            return Convert.ToByte(enumValue.GetType().GetField("value__").GetValue(enumValue));
        }

        public object GetObjectValue(object enumValue)
        {
            return enumValue.GetType().GetField("value__").GetValue(enumValue);
        }

        public string GetDescription(object enumValue)
        {
            var descAttrib = EnumType.GetField(enumValue.ToString())
                .GetCustomAttributes(typeof(DescriptionAttribute), false)
                .FirstOrDefault() as DescriptionAttribute;
            return descAttrib != null ? descAttrib.Description : enumValue.ToString();
        }
    }

    public class EnumerationMember
    {
        public string Description { get; set; }

        public byte? Value { get; set; }
    }
XAML :
<ComboBox ItemsSource="{Binding Source={ Extensions:ByteEnumerationExtention {x:Type type:MyEnum} }}"  DisplayMemberPath="Description"
 SelectedValuePath="Value" SelectedValue="{Binding SelectedItemInViewModel}"/>
Enum :
public enum MyEnum : short
    {
        [Description("گزینه 1")]
        Item1 = 1,

        [Description("گزینه 2")]
        Item1 = 2,

        [Description("گزینه 3")]
        Item1 = 3,

        [Description("گزینه 4")]
        Item1 = 4,

        [Description("گزینه 5")]
        Item1 = 5,
.
.
.
    }
SelectedItem در ViewModel :
        short? selectedItemInViewModel;
        public short? SelectedItemInViewModel
        {
            get
            {
                return selectedItemInViewModel;
            }
            set
            {
                selectedItemInViewModel = value; 
                 RaisePropertyChanged("SelectedItemInViewModel");
               //do calculations if needed
            }
        }

مطالب
ویژگی های کمتر استفاده شده در NET. - بخش ششم

#Execute VB code via C

می توان از طریق #C، ماکروهای Visual Basic مورد استفاده‌ی در Office را تولید کرد.
static void AddChartButton( Workbook workBook,
                            Worksheet xlWorkSheetNew,
                            Range currentRange,  int macroId,
                            int startRow, int endRow,
                            int startCol, int endCol,
                            string buttonImagePath )
{
    var cell = currentRange.Next;
    var width = cell.Width;
    var height = 24;
    var left = cell.Left;
    var top = System.Math.Max( cell.Top + cell.Height - height, 0 );
    var button = xlWorkSheetNew.Shapes.AddPicture( buttonImagePath,
                                                    MsoTriState.msoFalse,
                                                    MsoTriState.msoCTrue,
                                                    left, top, width, height );
    var module = workBook.VBProject.VBComponents.Add( vbext_ComponentType.vbext_ct_StdModule );
    module.CodeModule.AddFromString( GetMacro( macroId,
                                                startRow, endRow,
                                                startCol, endCol ) );
    button.OnAction = "Macro" + macroId;
}

static string GetMacro( int macroId,
                        int startRow,  int endRow,
                        int startCol, int endCol )
{
    var sb = new StringBuilder();
    var range = "ActiveSheet.Range(Cells(" + startRow + "," + startCol + "), Cells(" + endRow + "," + endCol + ")).Select";
    sb.AppendLine( "Sub Macro" + macroId + "()" );
    sb.AppendLine( "On Error Resume Next" );
    sb.AppendLine( range );
    sb.AppendLine( "ActiveSheet.Shapes.AddChart.Select" );
    sb.AppendLine( "ActiveChart.ChartType = xlColumn" );
    sb.AppendLine( "ActiveChart.SetSourceData Source:=" + range );
    sb.AppendLine( "On Error GoTo 0" );
    sb.AppendLine( "End Sub" );
    return sb.ToString();
}

و برای استفاده از آن می‌توان مانند مثال زیر عمل کرد:

var excelApp = new Microsoft.Office.Interop.Excel.Application();
var fileName = @"C:\Users\Vahid\Desktop\VBA.xlsm";
var workBook = excelApp.Workbooks.Open( fileName );
var sheet = workBook.Sheets[1];

AddChartButton( workBook,
                sheet,
                sheet.Range["B1"],
                1231,
                1,
                10,
                1,
                2,
                @"C:\Users\Vahid\Desktop\BarChart.png");

excelApp.DisplayAlerts = false;
workBook.Close( true,
                fileName );

خروجی مثال بالا


نکته: در صورتیکه بعد از اجرای برنامه، خطای " programmatic access to visual basic project is not trusted"  رخ داد از این طریق می‌توانید مشکل را حل کنید.

File -> Options -> Trust Center -> Trust Center Settings -> Macro Settings -> Trust Access to the VBA Project object model


volatile

کلمه کلیدی volatile نشان می‌دهد که یک فیلد ممکن است توسط چندین thread به صورت همزمان تغییر کند. فیلدهایی که به عنوان volatile تعریف می‌شوند، شامل بهینه سازی کامپایلر برای دسترسی از طریق تنها یک thread قرار نمی‌گیرند و بروزرسانی مقدار فعلی این فیلد را در تمامی زمان‌ها، تضمین می‌کند.
class Program
{
    volatile bool _shouldPartyContinue = true;

    static void Main()
    {
        var firstDimension = new Program();
        var secondDimension = new Thread( firstDimension.StartPartyInAnotherDimension );
        secondDimension.Start( firstDimension );
        Thread.Sleep( 5000 );
        firstDimension._shouldPartyContinue = false;
        Console.WriteLine( "Party Finish" );
    }

    void StartPartyInAnotherDimension( object input )
    {
        var currentDimensionInput = (Program)input;
        Console.WriteLine( "let the party begin" );
        while ( currentDimensionInput._shouldPartyContinue ) {}
        Console.WriteLine( "Party ends: (" );
    }
}
نکته: اگر متغیر shouldPartyContinue به وسیله volatile علامت گذاری نشده بود، برنامه در حالت Release (که گزینه Optimize code تیک داشته باشد) هیچگاه به پایان نمی‌رسید.

::global

وقتی که یک عضو توسط موجودیت دیگری با همان نام مخفی شده باشد، با استفاده از کلمه کلیدی ::global (فضای نام سراسری) قابلیت دسترسی به آن امکان پذیر می‌شود.
class Program
{
    public static void Main(string[] args)
    {
        Console.WriteLine( Number );  // Error
        global::System.Console.WriteLine( "Console: " + Console ); //OK
    }

    public class System { }

    // Define a constant called ‘Console’ to cause more problems.
    const int Console = 7;
    const int Number = 67;
}
همانطور که در مثال بالا مشاهده می‌کنید، به دلیل وجود ثابت Console و کلاس System امکان دسترسی به متد WriteLine وجود ندارد، برای در دسترس قرار گرفتن آن باید از ::global استفاده کرد.

DebuggerDisplayAttribute

با استفاده از  DebuggerDisplayAttribute  می‌توانید نحوه نمایش یک فیلد یا یک کلاس را در پنجره متغیر دیباگر مشخص کنید.
[DebuggerDisplay( "{DebuggerDisplay}" )]
public class DebuggerDisplayTest
{
    public string FirstName { get; set; }

    public string LastName { get; set; }

    public int Age { get; set; }

    [DebuggerBrowsable( DebuggerBrowsableState.Never )]
    string DebuggerDisplay => $"{FirstName} {LastName} {Age} years old";
}
و بعد از استفاده‌ی از آن، خروجی زیر بدست می‌آید:

همچنین شما می‌توانید عبارات مختلفی را به صورت مستقیم در این attribute استفاده کنید.
[DebuggerDisplay( "Age {Age > 0 ? Age : 25}" )]
public class DebuggerDisplayTest
{
 //...
}
اگر مقدار پروپرتی Age بیشتر از 0 باشد، مقدار Age و در غیراینصورت 25 نشان داده می‌شود.
مطالب
استفاده از Web API در ASP.NET Web Forms
گرچه ASP.NET Web API بهمراه ASP.NET MVC بسته بندی شده و استفاده می‌شود، اما اضافه کردن آن به اپلیکیشن‌های ASP.NET Web Forms کار ساده ای است. در این مقاله مراحل لازم را بررسی می‌کنیم.

برای استفاده از Web API در یک اپلیکیشن ASP.NET Web Forms دو قدم اصلی باید برداشته شود:

  • اضافه کردن یک کنترلر Web API که از کلاس ApiController مشتق می‌شود.
  • اضافه کردن مسیرهای جدید به متد Application_Start.


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

ویژوال استودیو را اجرا کنید و پروژه جدیدی از نوع ASP.NET Web Forms Application ایجاد کنید.


کنترلر و مدل اپلیکیشن را ایجاد کنید

کلاس جدیدی با نام Product بسازید و خواص زیر را به آن اضافه کنید.

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public string Category { get; set; }
}
همانطور که مشاهده می‌کنید مدل مثال جاری نمایانگر یک محصول است. حال یک کنترلر Web API به پروژه اضافه کنید. کنترلر‌های Web API درخواست‌های HTTP را به اکشن متدها نگاشت می‌کنند. در پنجره Solution Explorer روی نام پروژه کلیک راست کنید و گزینه Add, New Item را انتخاب کنید.

در دیالوگ باز شده گزینه Web را از پانل سمت چپ کلیک کنید و نوع آیتم جدید را Web API Controller Class انتخاب نمایید. نام این کنترلر را به "ProductsController" تغییر دهید و OK کنید.

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

namespace WebForms
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Web.Http;

    public class ProductsController : ApiController
    {

        Product[] products = new Product[] 
        { 
            new Product { Id = 1, Name = "Tomato Soup", Category = "Groceries", Price = 1 }, 
            new Product { Id = 2, Name = "Yo-yo", Category = "Toys", Price = 3.75M }, 
            new Product { Id = 3, Name = "Hammer", Category = "Hardware", Price = 16.99M } 
        };

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

        public Product GetProductById(int id)
        {
            var product = products.FirstOrDefault((p) => p.Id == id);
            if (product == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            return product;
        }

        public IEnumerable<Product> GetProductsByCategory(string category)
        {
            return products.Where(
                (p) => string.Equals(p.Category, category,
                    StringComparison.OrdinalIgnoreCase));
        }
    }
}
کنترلر جاری لیستی از محصولات را بصورت استاتیک در حافظه محلی نگهداری می‌کند. متدهایی هم برای دریافت لیست محصولات تعریف شده اند.


اطلاعات مسیریابی را اضافه کنید

مرحله بعدی اضافه کردن اطلاعات مسیریابی (routing) است. در مثال جاری می‌خواهیم آدرس هایی مانند "api/products/" به کنترلر Web API نگاشت شوند. فایل Global.asax را باز کنید و عبارت زیر را به بالای آن اضافه نمایید.

using System.Web.Http;
حال کد زیر را به متد Application_Start اضافه کنید.
RouteTable.Routes.MapHttpRoute(
    name: "DefaultApi",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = System.Web.Http.RouteParameter.Optional }
    );

برای اطلاعات بیشتر درباره مسیریابی در Web API به این لینک مراجعه کنید.


دریافت اطلاعات بصورت آژاکسی در کلاینت

تا اینجا شما یک API دارید که کلاینت‌ها می‌توانند به آن دسترسی داشته باشند. حال یک صفحهHTML خواهیم ساخت که با استفاده از jQuery سرویس را فراخوانی می‌کند. صفحه Default.aspx را باز کنید و کدی که بصورت خودکار در قسمت Content تولید شده است را حذف کرده و کد زیر را به این قسمت اضافه کنید:

<%@ Page Title="Home Page" Language="C#" MasterPageFile="~/Site.Master" 
    AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="WebForms._Default" %>

<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
</asp:Content>

<asp:Content ID="BodyContent" runat="server" ContentPlaceHolderID="MainContent">
    <h2>Products</h2>
    <table>
    <thead>
        <tr><th>Name</th><th>Price</th></tr>
    </thead>
    <tbody id="products">
    </tbody>
    </table>
</asp:Content>
حال در قسمت HeaderContent کتابخانه jQuery را ارجاع دهید.
<asp:Content ID="HeaderContent" runat="server" ContentPlaceHolderID="HeadContent">
    <script src="Scripts/jquery-1.7.1.min.js" type="text/javascript"></script>
</asp:Content>

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

نکته: برای ارجاع دادن اسکریپت‌ها می‌توانید بسادگی فایل مورد نظر را با drag & drop به کد خود اضافه کنید.

زیر تگ jQuery اسکریپت زیر را اضافه کنید.

<script type="text/javascript">
    function getProducts() {
        $.getJSON("api/products",
            function (data) {
                $('#products').empty(); // Clear the table body.

                // Loop through the list of products.
                $.each(data, function (key, val) {
                    // Add a table row for the product.
                    var row = '<td>' + val.Name + '</td><td>' + val.Price + '</td>';
                    $('<tr/>', { text: row })  // Append the name.
                        .appendTo($('#products'));
                });
            });
        }

        $(document).ready(getProducts);
</script>

هنگامی که سند جاری (document) بارگذاری شد این اسکریپت یک درخواست آژاکسی به آدرس "api/products/" ارسال می‌کند. سرویس ما لیستی از محصولات را با فرمت JSON بر می‌گرداند، سپس این اسکریپت لیست دریافت شده را به جدول HTML اضافه می‌کند.

اگر اپلیکیشن را اجرا کنید باید با نمایی مانند تصویر زیر مواجه شوید: