مطالب
اجرای یک Script حاوی دستورات Go در سی شارپ
سلام ؛

سال نو مبارک ! امیدوارم سال بسیار خوبی در پیش داشته باشید :)

از زمانی که استفاده از ORM‌های Code First رایج شده ، اجرای اسکریپت‌های طولانی جهت ایجاد دیتابیس خیلی استفاده ندارد، اما حالت خاص همیشه پیش می‌آید.
مثلا قصد داریم پیش از آغاز برنامه پس از ایجاد دیتابیس توسط Entity Framework به یک سری جداول فیلدی با نوع داده‌ی Geometry اضافه کنیم. یا باید به دیتابیس یک سری Stored Procedure و View اضافه کرد.
Script‌ها ی Generate شده توسط SQL Server حاوی دستور Go هستند. ADO.NET اجرای Script که حاوی Go باشد را پشتیبانی نمی‌کند.
اما روش‌ها و ترفند‌های زیادی برای اجرای یک فایل Script طولانی حاوی دستور Go روی دیتابیس وجود دارد :
        private static string GetScript()
        {
            string path = AppDomain.CurrentDomain.BaseDirectory +
                          @"Scripts\script.sql";
            var file = new FileInfo(path);

            string script = file.OpenText().ReadToEnd();
            return script;
        }

        private static void ExecuteScript()
        {
            string script = GetScript();

            //split the script on "GO" commands
            var splitter = new[] {"\r\nGO\r\n"};
            string[] commandTexts = script.Split(splitter,
                                                 StringSplitOptions.RemoveEmptyEntries);
            foreach (string commandText in commandTexts)
            {
                using (var ctx = new DbContext())
                {
                    if (!string.IsNullOrEmpty(commandText))
                    {
                        ctx.Database.ExecuteSqlCommand(commandText);
                    }
                }
            }
        }

در اینجا به جای آنکه تلاش کنیم یک فایل را روی دیتابیس یک جا اجرا کنیم دستورات را جدا کرده و تک به تک اجرا می‌کنیم. 
بروزرسانی:
مطالب
ارتقاء به NH 3.2 - قسمت دوم

پیشتر مطلبی را در مورد 18 مقاله‌ای که اکثر حالت‌های Mapping موجود در NHibernate را خلاصه کرده بود، مطالعه کردید.
یک مورد هم در این مطلب به نظر در مقایسه با Fluent NHibernate درنظر گرفته نشده است و آن هم بحث AutoMapping است. Fluent NHibernate این قابلیت را دارد که بر اساس تعاریف کلاس‌های شما و روابط بین آن‌ها به صورت خودکار نگاشت‌ها را تشکیل دهید. یعنی خودش مباحث ارتباط‌های یک به چند و چند به چند و غیره را در پشت صحنه به صورت خودکار تولید کند؛ بدون حتی یک سطر کدنویسی اضافی. فقط حداکثر یک سری IAutoMappingOverride و همچنین تعدادی Convention نامگذاری را هم می‌توان جهت تنظیمات این سیستم تمام خودکار اعمال کرد. مثلا توسط IAutoMappingOverride، یکی از خاصیت‌های کلاس را به صورت Unique معرفی کرد و مابقی هم به صورت خودکار توسط قابلیت Autommaping نگاشت می‌شوند. یا توسط Convention نامگذاری سفارشی خود، به Fluent NHibernate اعلام می‌کنیم که من علاقمندم نام مثلا کلیدهای خارجی تشکیل شده بجای اعدادی منحصربفرد، از روش ویژه‌ای که تعیین می‌کنم، ساخته شوند. اینجا است که به نظر من کار با NHibernate حتی از Entity framework هم ساده‌تر است (یا ساده‌تر شده است).
قابلیت AutoMapping یاد شده، در سیستم جدید توکار Mapping by code هم وجود دارد. فقط چون جایی به صورت درست و درمان مستند نشده، همه دور خودشان می‌چرخند! به همین جهت مثالی را در این زمینه آماده کردم که موارد زیر را پوشش می‌دهد:
- نحوه اعمال تنظیمات بانک اطلاعاتی با کدنویسی در NH3,2
- نحوه یکپارچه سازی این روش جدید با کتابخانه NHibernate Validator
- استفاده از NHibernate Validator جهت تنظیم طول فیلدهای بانک اطلاعاتی تولیدی به صورت خودکار
- نحوه تعریف قراردادهای نامگذاری ویژه (مثلا نام جداول تولید شده به صورت خودکار،‌ برخلاف نام موجودیت‌ها، جمع باشد نه مفرد)
- اضافه کردن قابلیت تشخیص many-to-many به auto-mapping موجود در NH3,2 (برای تشخیص این مورد به کمک امکانات مهیای پیش فرض NH3,2، باید اندکی کدنویسی کرد)
- نحوه بازنویسی قراردادهای پیش فرض auto-mapping. مثلا اگر قرار باشد همه چیز خودکار شود اما یکی از فیلدها به صورت unique معرفی شود چکار باید کرد.
- نحوه ذخیره اطلاعات mapping کامپایل شده در فایل فایل و سپس بارگذاری خودکار آن در حین اجرای برنامه جهت بالا بردن سرعت بارگذاری اولیه برنامه.



مطالب
خلاصه اشتراک‌های روز سه شنبه 3 آبان 1390
مطالب
انتشار VS2010

یکی دو روزی هست که VS2010 منتشر شده و در این مطلب خلاصه‌ای از لینک‌های مفید مرتبط را جمع آوری کرده‌ام که در ادامه ملاحظه خواهید کرد:

دریافت فایل ISO اصلی
این فایل ISO منتشر شده، نسخه‌ی نهایی است و تنها تفاوت آن با نگارش اصلی منتشر شده برای دارندگان اکانت‌های MSDN ، عدم درج سریال در فایل setup.sdb آن است. تصاویر زیر متعلق است به دارندگان اکانت‌های MSDN و همانطور که مشاهده می‌کنید، محل وارد کردن سریالی که یافته‌اید، در فایل setup.sdb است. پس از آن نگارش Trial به صورت کامل نصب خواهد شد.








خلاصه‌ای از تازه‌ها و موارد مرتبط با VS2010 و دات نت 4

راستی! ReSharper 5.0 هم در همین زمان منتشر شده است.

مسیرراه‌ها
توسعه برنامه های Cross Platform با Xamarin Forms & Bit Framework
لطفا علاوه بر مطالب، نظرات نیز مطالعه شوند.

قسمت اول چرا Xamarin Forms؟
قسمت دوم نصب و راه اندازی
قسمت سوم اجرا و دیباگ پروژه مثال روی UWP و راه اندازی Simulator و Edit & Continue برای C# - XAML
قسمت چهارم اجرا و دیباگ پروژه مثال روی Android + راه اندازی Emulator و Device + نکات بهبود Performance در Android
قسمت پنجم اجرا و دیباگ پروژه مثال روی iOS + راه اندازی Virtual Mac OS + تست بر روی Emulator و Device
قسمت ششم بررسی کلیات پروژه مثال و بررسی ساختار Page - Layout - Control
قسمت هفتم بررسی جزئیات Layout شامل Unit - Padding -Margin و ...
قسمت هشتم بررسی نقش View Model در MVVM و طرز کار Navigation
قسمت نهم Binding & Commanding & x:DataType
قسمت دهم Triggers & Value Converters
قسمت یازدهم List View & Syncfusion Controls
قسمت دوازدهم چند زبانه سازی و Right To Left
قسمت سیزدهم بهبود Performance با Compressed Layout و FF Image Loading
قسمت چهاردهم مدیریت خطاها و استفاده از App Center برای لاگ کردن آنها
قسمت پانزدهم زدن کدهای Platform Specific و Custom Renderers
قسمت هجدهم ارتباط با Rest Api
قسمت نوزدهم نوشتن Component و Partial View
قسمت بیستم Entity Framework Core & Sqlite
قسمت بیست و یکم آشنایی با پروژه ToDoStand
قسمت بیست و دوم ارتباط با پروژه ای که سمت سرور آن با Bit توسعه داده شده است و مزیت‌های کار با Bit در سمت سرور
قسمت بیست و سوم آشنایی با پروژه ToDoLine
مطالب
استفاده از 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 تفکیک نمایید.

نظرات مطالب
Contact me
متاسفانه این دلیل عمده‌ای است که کمتر کتاب با کیفیت کامپیوتری فارسی رو می‌تونید پیدا کنید که حاصل تحقیق برای مثال 6 ماه (یا بیشتر) یک شخص باشد. عمدتا کتاب‌های برنامه نویسی در سه سوت و خودآموز در 5 روز و غیره که به نظر قسمتی از آن ترجمه‌ی ماشینی است و قسمتی هم ادیت دستی برای تولید انبوه سریعتر.
تا زمانی که نشود به عنوان یک شغل به آن نگاه کرد و به صورت اقساطی بعد از 6 ماه از تاریخ انتشار کتاب آن هم به میزان 9 تا 10 درصد پشت جلد را به شما بدهند ... کیفیت بازار نشر تخصصی ما به همین صورت خواهد بود.
مطالب
سری بررسی SQL Smell در EF Core - استفاده از مدل Entity Attribute Value - بخش اول
یکی از چالش‌های دیتابیس‌های رابطه‌ایی، ذخیره‌سازی داده‌هایی با ساختار داینامیک است. در حالت عادی، یک جدول مجموعه‌ایی از موجودیت‌ها است. هر موجودیت نیز شامل یکسری ویژگی‌های (Attributes) مشخص می‌باشد. اما شرایطی را در نظر بگیرید که تعداد این ویژگی‌ها به صورت مشخص و ثابتی نباشد؛ یعنی برای هر موجودیت، ویژگی‌های متفاوتی داشته باشیم. یک روش پیاده‌سازی اینچنین سناریوهایی، استفاده از مدلی با نام Entity Attribute Value است. در این روش ستون‌های داینامیک را درون یک جدول جنریک تعریف خواهیم کرد. به عنوان مثال برای ذخیره‌سازی اطلاعات اشخاص، در حالت نرمال، یک جدول با ساختار مشخصی خواهیم داشت: 
create table Employees
(
   Id int auto_increment
   primary key,
   FirstName text null,
   LastName text null,
   DateOfBirth timestamp not null
);
تعریف جدول فوق نیز در Entity Framework به اینصورت خواهد بود:
public class Employee
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTimeOffset DateOfBirth { get; set; }
}

public class MyDbContext : DbContext
{
    public DbSet<Employee> Employees { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        options.UseMySQL(_configuration.GetConnectionString("DataConnection"));
    }
}


اما در مدل EAV، خواص داینامیک را به درون جدول دومی منتقل خواهیم کرد:

create table EmployeeEav
(
   Id int auto_increment
   primary key
);

create table EmployeeAttributes
(
  Id int auto_increment
  primary key,
  EmployeeId int not null,
  AttributeName text null,
  AttributeValue text null,
  constraint FK_EmployeeAttributes_EmployeeEav_EmployeeId
  foreign key (EmployeeId) references EmployeeEav (Id)
  on delete cascade
);

create index IX_EmployeeAttributes_EmployeeId
on EmployeeAttributes (EmployeeId);

تعریف جداول فوق نیز در Entity Framework به اینصورت خواهند بود:

public class EmployeeEav
{
    public int Id { get; set; }
    public virtual ICollection<EmployeeAttribute> Attributes { get; set; }
}

public class EmployeeAttribute
{
    public int Id { get; set; }
    public virtual EmployeeEav Employee { get; set; }
    public int EmployeeId { get; set; }
    public string AttributeName { get; set; }
    public string AttributeValue { get; set; }
}

public class MyDbContext : DbContext
{

    public DbSet<EmployeeEav> EmployeeEav { get; set; }
    public DbSet<EmployeeAttribute> EmployeeAttributes { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        options.UseMySQL(_configuration.GetConnectionString("DataConnection"));
    }
}



درون این جدول دوم، سه فیلد اصلی داریم: یکی به عنوان Entity که در اینجا یک ارجاع را به جدول EmployeeEav دارد. یک فیلد به عنوان Attribute که برای تعیین نام ویژگی داینامیک استفاده می‌شود و در نهایت یک Value که برای ذخیره‌سازی مقدار ویژگی مورد استفاده قرار میگیرد. بنابراین به این نوع طراحی، Entity Attribute Value گفته می‌شود. مزیت اصلی این روش، انعطاف زیاد آن است در واقع می‌توانیم N تعداد ویژگی را برای Entity موردنظرمان داشته باشیم. اما این روش یک SQL Smell است و اشکالات زیادی را به همراه دارد:

  • کوئری گرفتن در این روش سخت است
یکی از مشکلات اصلی این روش این است امکان کوئری گرفتن از جدول ویژگی‌ها را سخت میکند. در واقع این روش به store everything, query nothing معروف است. مثلاً فرض کنید می‌خواهیم لیست کارمندانی را که تاریخ تولدشان ۲۵ سال پیش است، واکشی کنیم. در حالت عادی با تعداد ستون ثابت می‌توانیم به راحتی اینکار را انجام دهیم:
SELECT `e`.`Id`, `e`.`DateOfBirth`, `e`.`FirstName`, `e`.`LastName`
FROM `Employees` AS `e`
WHERE `e`.`DateOfBirth` > @__endDate_0
کوئری LINQ کد فوق اینچنین شکلی خواهد داشت:
var endDate = DateTimeOffset.Now.AddYears(Convert.ToInt32(-25));
var normalTypes = dbContext.Employees.Where(x => x.DateOfBirth > endDate).ToList();
اما در مدل EAV نوشتن کوئری فوق خیلی سخت‌تر خواهد بود: 
SELECT MAX(CASE AttributeName
               WHEN 'FirstName'
                   THEN AttributeValue
    END)        AS FirstName,
       MAX(CASE AttributeName
               WHEN 'LastName'
                   THEN AttributeValue
           END) AS LastName,
       MAX(CASE AttributeName
               WHEN 'DateOfBirth'
                   THEN AttributeValue
           END) AS DateOfBirth
FROM efcoresample.EmployeeAttributes
WHERE EmployeeId IN (SELECT EmployeeId
                     FROM efcoresample.EmployeeAttributes
                     WHERE AttributeName = 'DateOfBirth'
                       AND AttributeValue > DATE_SUB(CURRENT_DATE(), INTERVAL 25 YEAR))
  AND AttributeName IN ('FirstName', 'LastName', 'DateOfBirth')
GROUP BY EmployeeId;
همچنین کوئری LINQ آن نیز به همان اندازه سخت میباشد: 
string[] columnNames = {"FirstName", "LastName", "DateOfBirth"};
var employees = dbContext.EmployeeAttributes
    .Where(x => 
                dbContext.EmployeeAttributes
                    .Where(i => i.AttributeName == "DateOfBirth")
                    .Select(eId => eId.EmployeeId).Contains(x.EmployeeId) &&
                columnNames.Contains(x.AttributeName))
    .GroupBy(x => x.EmployeeId)
    .Select(g => new
    {
        FirstName = g.Max(f => f.AttributeName == "FirstName" ? f.AttributeValue : ""),
        LastName = g.Max(f => f.AttributeName == "LastName"? f.AttributeValue : ""),
        DateOfBirth = g.Max(f => f.AttributeName == "DateOfBirth"? f.AttributeValue : ""),
        Id = g.Key
    })
    .ToList()
    .Where(x => DateTime.ParseExact(x.DateOfBirth, "yyyy-MM-dd", CultureInfo.InvariantCulture) > DateTime.Now.AddYears(-25));

  • امکان تعریف فیلدهای اجباری را نخواهیم داشت
در حالت نرمال و ساختاریافته، برای هرکدام از فیلدها می‌توانیم الزامی و یا اختیاری بودن آنها را به راحتی با NOT NULL تعیین کنیم. اما در مدل EAV این امکان را نخواهیم داشت. 

  • امکان تعیین نوع ستون‌ها را نخواهیم داشت
در حالت نرمال به راحتی می‌توانیم نوع فیلد موردنظر را تعیین کنیم. اما در مدل EAV به دلیل ماهیت داینامیک ستون‌ها، این امکان را نداریم. ستون AttributeValue همزمان ممکن است تاریخ، عددی، اعشاری و… باشد در نتیجه چون از ورودی مطمئن نیستیم، مجبوریم تایپ آن را به رشته تنظیم کنیم. 

  • امکان تعریف کلیدهای خارجی را نخواهیم داشت
در مدل EAV نمی‌توانیم صحت دیتا را تضمین کنیم؛ زیرا امکان تعریف کلید خارجی را نخواهیم داشت.

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