EF Code First #7
سعید جان میدونم که اینکار میشه- مطمئنا شماره ملیشون رو uniqe کردم!
ما برای انتخاب کلید اصلی دو حالت داریم -
1- استفاده از کلیدهای طبیعی مثل شماره پرسنلی
2- استفاده از کلیدهای جانشین مثل یک فیلد identity - این حالت موقعی استفاده میشه که کلید طبیعی نداشته باشیم
مفاهیم برنامه نویسی ـ مروری بر کلاس و شیء
کتاب رایگان NancyFX Succinctly
In NancyFX Succinctly, you'll learn what NancyFX is all about, where it came from, and what it can do for you as a .NET developer. Author Peter Shaw will explain why NancyFX is more than just another web framework, and teach you some of the many tricks that make it as easy as possible for you to create stunning web-enabled applications on the .NET platform.
چندی پیش در سایت جاری چند مقاله خوب توسط یکی از دوستان درباره Qunit منتشر شد. Qunit یک ابزار قدرتمند و مناسب برای تست کدهای جاوااسکریپت است و در اثبات صحت این گفته همین کافیست که بدانیم برای تست کدهای نوشته شده در پروژههای متن بازی هم چون Backbone.Js و JQuery از این فریم ورک استفاده شده است. اما به احتمال قوی در ذهن شما این سوال مطرح شده است که خب! در صورت آشنایی با Qunit چه نیاز به یادگیری Jasmine یا خدای نکرده Mocha و FuncUnit است؟ هدف صرفا معرفی یک ابزار غیر برای تست کد است نه مقایسه و نتیجه گیری برای تعیین میزان برتری این ابزارها. اصولا مهمترین دلیل برای انتخاب، علاوه بر امکانات و انعطاف پذیری، فاکتور راحتی و آسان بودن در هنگام استفاده است که به صورت مستقیم به شما و تیم توسعه نرم افزار بستگی دارد.
اما به عنوان توسعه دهنده نرم افزار که قرار است از این ابزار استفاده کنیم بهتر است با تفاوتها و شباهتهای مهم این دو فریم ورک آشنا باشیم:
»Jasmine یک فریم ورک تست کدهای جاوا اسکریپ بر مبنای Behavior-Driven Development است در حالی که Qunit بر مبنای Test-Driven Development است و همین مسئله مهمترین تفاوت بین این دو فریم ورک میباشد.
»اگر قصد دارید که از Qunit نیز به روش BDD استفاده نمایید باید از ترکیب Pavlov به همراه Qunit استفاده کنید.
»Jasmine از مباحث مربوط به Spies و Mocking به خوبی پشتیبانی میکند ولی این امکان به صورت توکار در Qunit فراهم نیست. برای اینکه بتوانیم این مفاهیم را در Qunit پیاده سازی کنیم باید از فریم ورکهای دیگر نظیر SinonJS به همراه Qunit استفاده کنیم.
»هر دو فریم ورک بالا به سادگی و راحتی کار معروف هستند
»تمام موارد مربوط به الگوهای Matching در هر دو فریم ورک به خوبی تعبیه شده است
» هر دو فریم ورک بالا از مباحث مربوط به Asynchronous Testing برای تست کدهای Ajax ای به خوبی پشتیبانی میکنند.
بررسی چند مفهوم
قبل از شروع، بهتر است که با چند مفهوم کلی و در عین حال مهم این فریم ورک آشنا شویم
describe('JavaScript addition operator', function () { it('adds two numbers together', function () { expect(1 + 2).toEqual(3); }); });
در تابع it کد بالا شما میتوانید کدهای مربوط بدنه توابع تست خود را بنویسید. برای پیاده سازی Assert در توابع تست مفهوم expectationها وجود دارد. در واقع expect برای بررسی مقادیر حقیقی با مقادیر مورد انتظار مورد استفاده قرار میگیرد و شامل مقادیر true یا false خواهد بود.
برای Setup و Teardown توابع تست خود باید از توابع beforeEach و afterEach که بدین منظور تعبیه شده اند استفاده کنید.
describe("A spec (with setup and tear-down)", function() { var foo; beforeEach(function() { foo = 0; foo += 1; }); afterEach(function() { foo = 0; }); it("is just a function, so it can contain any code", function() { expect(foo).toEqual(1); }); it("can have more than one expectation", function() { expect(foo).toEqual(1); expect(true).toEqual(true); }); });
اگر در کد تست خود قصد دارید که یک تابع describe یا it را غیر فعال کنید کافیست یک x به ابتدای آنها اضافه کنید و دیگر نیاز به هیچ کار اضافه دیگری برای comment کردن کد نیست.
xdescribe("A spec", function() { var foo; beforeEach(function() { foo = 0; foo += 1; }); xit("is just a function, so it can contain any code", function() { expect(foo).toEqual(1); }); });
درادامه قصد پیاده سازی یک مثال را با استفاده از Jasmine و RequireJs در پروژه Asp.Net MVC دارم.
- فولدر lib شامل فایلها کدهای Jasmine برای setup و tear down و spice و تست کدهای شما میباشد.
- فایل specRunner.html به واقع یک فایل برای نمایش فایلهای تست و همچنین نمایش نتیجه تست است.
- فولدر spec نیز شامل کدهای Jasmine برای کمک به نوشتن تست میباشد.
در این مثال قصد داریم فایلهای player.js و song.js که به عنوان نمونه به همراه این فریم ورک قرار دارد را در قالب یک پروژه MVC به همراه RequireJs، تست نماییم. در نتیجه این فایلها را از فولدر src انتخاب نمایید و آنها را در قسمت Scripts پروژه اصلی خود کپی کنید(ابتدا بک پوشه به نام App بسازید و فایلها را در آن قرار دهید)
برای استفاده از requireJs باید دستور define را در ابتدا این فایلها اضافه نماییم. در نتیجه فایلهای Player.js و Song.js را باز کنید و تغییرات زیر را در ابتدای این فایلها اعمال نمایید.
Song.js
define(function () { function Song() { } Song.prototype.persistFavoriteStatus = function (value) { // something complicated throw new Error("not yet implemented"); }; });
define(function () { function Player() { } Player.prototype.play = function (song) { this.currentlyPlayingSong = song; this.isPlaying = true; }; Player.prototype.pause = function () { this.isPlaying = false; }; Player.prototype.resume = function () { if (this.isPlaying) { throw new Error("song is already playing"); } this.isPlaying = true; }; Player.prototype.makeFavorite = function () { this.currentlyPlayingSong.persistFavoriteStatus(true); }; });
baseUrl در پیکر بندی requireJs به مسیر فایلهای پروژه که در پروژه اصلی MVC قرار دارد اشاره میکند. paths برای تعیین مسیر فایلهای تست که در پوشه spec در پروژه تست قرار دارد اشاره میکند. اگر دقت کرده باشید به دلیل اینگه تگهای script مربوط به لود فایلهای SpecHelper.js و PlayerSpec.js به صورت comment در آمده اند در نتیجه این فایلها لود نخواهند شد و خروجی مورد نظر مشاهده نمیشود. در این جا باید از مکانیزم AMD موجود در RequireJs استفاده نماییم و فایلهای مربوطه را لود کنیم. برای این کار نیاز به اضافه کردن دستور require در ابتدای تگ script به صورت زیر در این فایل است. در نتیجه فایلهای PlayerSpec و SpecHelper نیز توسط RequireJs لود خواهند شد.
نیاز به یک تغییر کوچک دیگر نیز وجود دارد. فایل PlayerSpec را باز نمایید و وابستگی فایلهای آن را تعیین نمایید. از آن جا که این فایل برای تست فایلهای Player , Song ایجاد شده است در نتیجه باید از define برای تعیین این وابستگیها استفاده نماییم.
یادآوری:
»دستور describe در فایل بالا برای تعریف تابع تست است. همان طور که میبینید بک نام به آن داده میشود به همراه بدنه تابع تست.
»دستور beforeEach برای آماده سازی مواردی است که قصد داریم در تست مورد استفاده قرار گیرند. همانند متدهای Setup در UnitTest.
» دستور expect نیز معادل Assert در UnitTest است و برای بررسی صحت عملکرد تست نوشته میشود.
اگر
فایل SpecRunner.html را دوباره در مرورگر خود باز نمایید تصویر زیر را
مشاهده خواهید کرد که به عنوان موفقیت آمیز بودن پیکر بندی پروژه و تستهای آن میباشد.
function testScope() { var myTest = true; if (true) { var myTest = "I am changed!" } alert(myTest); } testScope(); // will alert "I am changed!"
function testScope() { var myTest = true; if (true) { var myTest = "I am changed!" } alert(myTest); } testScope(); // will alert "I am changed!" alert(myTest); // will throw a reference error, because it doesn't exist outside of the function
<script type="text/javascript"> // a globally-scoped variable var a = 1; // global scope function one() { alert(a); } // local scope function two(a) { alert(a); } // local scope again function three() { var a = 3; alert(a); } // Intermediate: no such thing as block scope in javascript function four() { if (true) { var a = 4; } alert(a); // alerts '4', not the global value of '1' } // Intermediate: object properties function Five() { this.a = 5; } // Advanced: closure var six = function () { var foo = 6; return function () { // javascript "closure" means I have access to foo in here, // because it is defined in the function in which I was defined. alert(foo); } }() // Advanced: prototype-based scope resolution function Seven() { this.a = 7; } // [object].prototype.property loses to [object].property in the lookup chain Seven.prototype.a = -1; // won't get reached, because 'a' is set in the constructor above. Seven.prototype.b = 8; // Will get reached, even though 'b' is NOT set in the constructor. // These will print 1-8 one(); two(2); three(); four(); alert(new Five().a); six(); alert(new Seven().a); alert(new Seven().b); </Script>
var obj = { value: 0, increment: function() { this.value+=1; } }; obj.increment(); //Method invocation
<script type="text/javascript"> var value = 500; //Global variable var obj = { value: 0, increment: function() { this.value++; var innerFunction = function() { alert(this.value); } innerFunction(); //Function invocation pattern } } obj.increment(); //Method invocation pattern <script type="text/javascript"> Result : 500
<script type="text/javascript"> var value = 500; //Global variable var obj = { value: 0, increment: function() { var that = this; that.value++; var innerFunction = function() { alert(that.value); } innerFunction(); //Function invocation pattern } } obj.increment(); <script type="text/javascript"> Result : 1
var Dog = function(name) { //this == brand new object ({}); this.name = name; this.age = (Math.random() * 5) + 1; }; var myDog = new Dog('Spike'); //myDog.name == 'Spike' //myDog.age == 2 var yourDog = new Dog('Spot'); //yourDog.name == 'Spot' //yourDog.age == 4
var createCallBack = function(init) { //First function return new function() { //Second function by Constructor Invocation var that = this; this.message = init; return function() { //Third function alert(that.message); } } } window.addEventListener('load', createCallBack("First Message")); window.addEventListener('load', createCallBack("Second Message"));
myFunction.apply(thisContext, arrArgs); myFunction.call(thisContext, arg1, arg2, arg3, ..., argN);
var contextObject = { testContext: 10 } var otherContextObject = { testContext: "Hello World!" } var testContext = 15; // Global variable function testFunction() { alert(this.testContext); } testFunction(); // This will alert 15 testFunction.call(contextObject); // Will alert 10 testFunction.apply(otherContextObject); // Will alert "Hello World”
var o = { i : 0, F : function() { var a = function() { this.i = 42; }; a(); document.write(this.i); } }; o.F(); Result :0
var p = { i : 0, F : function() { var a = function() { this.i = 42; }; a.apply(this); document.write(this.i); } }; p.F(); Result :42
var q = { i: 0, F: function F() { var that = this; var a = function () { that.i = 42; } a(); document.write(this.i); } } q.F();
مثال 12: محاسبه کنید در سال 2012 و به ازای هر ماه مجزای آن، چه تعداد slots رزرو شدهاند؛ قسمت دوم.
این مثال را در قسمت قبل (مثال 6 آن) نیز بررسی کردیم. در اینجا میخواهیم در گزارش نهایی تولید شده، پس از اتمام ردیفهای یک ماه به ازای یک امکان خاص، جمع کل آن نیز درج شود و همچنین در پایان تمام ردیفها، جمع کل نهایی ذکر شود؛ چیزی شبیه به تصویر زیر که در آن 910، جمع کل slots ماه 8 است و 9191، جمع کل سال.
روش پیشنهادی حل این مساله استفاده از مفهومی به نام «GROUP BY ROLLUP» است:
SELECT facid, DATEPART(month, [StartTime]) AS month, sum(slots) AS slots FROM bookings WHERE starttime >= '2012-01-01' AND starttime < '2013-01-01' GROUP BY ROLLUP(facid, DATEPART(month, [StartTime])) ORDER BY facid, month;
ابتدا جمع slots را گروه بندی شده بر اساس هر ماه سال محاسبه میکنیم. این قسمت توسط LINQ to Entities قابل انجام است؛ همان مثال 6 قسمت قبل است.
سپس این اطلاعات که اکنون در سمت کلاینت (یعنی برنامهی ما) در حافظه موجود هستند، نیاز دارند به ازای هر گروه، یک جمع کل (sub total) و به ازای کل سال نیز یک جمع کل (grand total یا total) پیدا کنند.
ROLLUP(facid, month) اطلاعات تجمعی سلسه مراتبی پارامترهای ارسالی به آن را تولید میکند. یعنی (facid, month), (facid) و (). پیاده سازی LINQ to Objects این تابع را در اینجا میتوانید مشاهده کنید: Utils\GroupingExtensions.cs
بنابراین راه حل این مساله به صورت زیر خواهد بود:
var date1 = new DateTime(2012, 01, 01); var date2 = new DateTime(2013, 01, 01); var facilities = context.Bookings .Where(booking => booking.StartTime >= date1 && booking.StartTime < date2) .GroupBy(booking => new { booking.FacId, booking.StartTime.Month }) .Select(group => new { group.Key.FacId, group.Key.Month, TotalSlots = group.Sum(booking => booking.Slots) }) .OrderBy(result => result.FacId) .ThenBy(result => result.Month) .ToList() //This is new .GroupByWithRollup( item => item.FacId, item => item.Month, (primaryGrouping, secondaryGrouping) => new { FacId = primaryGrouping.Key, Month = secondaryGrouping.Key, TotalSlots = secondaryGrouping.Sum(item => item.TotalSlots) }, item => new { FacId = item.Key, Month = -1, TotalSlots = item.SubTotal(subItem => subItem.TotalSlots) }, items => new { FacId = -1, Month = -1, TotalSlots = items.GrandTotal(subItem => subItem.TotalSlots) });
در اینجا سلولهایی که اطلاعاتی ندارند، با منهای یک مشخص شدهاند؛ در گزارش اصلی با null مقدار دهی شده بودند.
مثال 13: به ازای نام هر کدام از امکانات موجود، جمع کل تعداد ساعات رزرو شدهی آنها را محاسبه کنید.
هر slot تنها نیم ساعت است و گزارش نهایی باید به همراه ستونهای facid, name, Total Hours باشد؛ مرتب شده بر اساس facid.
var items = context.Bookings .GroupBy(booking => new { booking.FacId, booking.Facility.Name }) .Select(group => new { group.Key.FacId, group.Key.Name, TotalHours = group.Sum(booking => booking.Slots) / 2M }) .OrderBy(result => result.FacId) .ToList();
مثال 14: گزارشی را از اولین رزرو کاربران پس از September 1st 2012، تهیه کنید.
این گزارش باید به همراه ستونهای surname, firstname, memid, starttime باشد؛ مرتب شده بر اساس memid.
var date1 = new DateTime(2012, 09, 01); var items = context.Bookings .Where(booking => booking.StartTime >= date1) .GroupBy(booking => new { booking.Member.Surname, booking.Member.FirstName, booking.Member.MemId }) .Select(group => new { group.Key.Surname, group.Key.FirstName, group.Key.MemId, StartTime = group.Min(booking => booking.StartTime) }) .OrderBy(result => result.MemId) .ToList();
مثال 15: گزارشی را از کاربران تهیه کنید که هر ردیف آن، به همراه تعداد کل کاربران باشد.
این گزارش باید به همراه ستونهای count, firstname, surname باشد؛ مرتب شده بر اساس joindate.
var members = context.Members .OrderBy(member => member.JoinDate) .Select(member => new { Count = context.Members.Count(), member.FirstName, member.Surname }) .ToList();
SELECT COUNT(*) FROM [Members] AS [m]; SELECT [m].[FirstName], [m].[Surname], @__Count_0 AS [Count] FROM [Members] AS [m] ORDER BY [m].[JoinDate];
باید بخاطر داشت که ID کاربران پشت سرهم نیست و همچنین این گزارش باید به همراه ستونهای row_number, firstname, surname باشد؛ مرتب شده بر اساس joindate.
هدف اصلی از این مثال، کار با مفهوم window functionها و تابع row_number است:
SELECT row_number() OVER (ORDER BY joindate) AS row_number, firstname, surname FROM members ORDER BY joindate;
var members = context.Members .OrderBy(member => member.JoinDate) .Select(member => new { member.FirstName, member.Surname }) .ToList() /* SELECT [m].[FirstName], [m].[Surname] FROM [Members] AS [m] ORDER BY [m].[JoinDate] */ // Now using LINQ to Objects .Select((member, index) => new { RowNumber = index + 1, member.FirstName, member.Surname }) .ToList();
مثال 17: کدامیک از امکانات موجود، بیشترین slots رزرو شده را دارد؟ قسمت دوم.
این مورد همان مثال 11 قسمت قبل است که پاسخ آنرا یافتیم (و از تکرار مجدد آن صرفنظر میکنیم) و هدف اصلی آن رسیدن به کوئری window function دار زیر است که تنها از طریق اجرای یک raw sql در EF-Core قابل اجرا است:
SELECT facid, total FROM (SELECT facid, sum(slots) AS total, rank() OVER (ORDER BY sum(slots) DESC) AS rank FROM bookings GROUP BY facid) AS ranked WHERE rank = 1;
مثال 18: به کاربران بر اساس تعداد ساعات رزرو آنها، امتیاز دهی (رتبه بندی) کنید.
این گزارش باید به همراه ستونهای firstname, surname, hours, rank باشد؛ مرتب شده بر اساس rank, surname.
هدف اصلی از این مثال، رسیدن به کوئری rank دار زیر است:
SELECT mems.firstname, mems.surname, ((sum(bks.slots) + 10) / 20) * 10 AS hours, rank() OVER (ORDER BY ((sum(bks.slots) + 10) / 20) * 10 DESC) AS rank FROM bookings AS bks INNER JOIN members AS mems ON bks.memid = mems.memid GROUP BY mems.firstname, mems.surname ORDER BY rank, mems.surname, mems.firstname;
var itemsQuery = context.Bookings .GroupBy(booking => new { booking.Member.FirstName, booking.Member.Surname }) .Select(group => new { group.Key.FirstName, group.Key.Surname, Hours = (group.Sum(booking => booking.Slots) + 10) / 20 * 10 }) .OrderByDescending(result => result.Hours) .ThenBy(result => result.Surname) .ThenBy(result => result.FirstName); var rankedItems = itemsQuery.Select(thisItem => new { thisItem.FirstName, thisItem.Surname, thisItem.Hours, Rank = itemsQuery.Count(mainItem => mainItem.Hours > thisItem.Hours) + 1 }) .ToList();
با این خروجی SQL نهایی:
مثال 19: سه امکانی را لیست کنید که بالاترین میزان فروش را داشتهاند.
این گزارش باید به همراه ستونهای name, rank باشد؛ مرتب شده بر اساس rank.
روش محاسبهی این گزارش با مثال قبلی یکی است (البته اینبار رتبه بندی بر اساس TotalRevenue است) و فقط در انتهای آن یک Where(result => result.Rank <= 3) را بیشتر دارد:
var facilitiesQuery = context.Bookings.Select(booking => new { booking.Facility.Name, Revenue = booking.MemId == 0 ? booking.Slots * booking.Facility.GuestCost : booking.Slots * booking.Facility.MemberCost }) .GroupBy(b => b.Name) .Select(group => new { Name = group.Key, TotalRevenue = group.Sum(b => b.Revenue) }) .OrderBy(result => result.TotalRevenue); var rankedFacilities = facilitiesQuery.Select(thisItem => new { thisItem.Name, thisItem.TotalRevenue, Rank = facilitiesQuery.Count(mainItem => mainItem.TotalRevenue > thisItem.TotalRevenue) + 1 }) .Where(result => result.Rank <= 3) .OrderBy(result => result.Rank) .ToList();
مثال 20: امکانات موجود را بر اساس میزان فروشی که دارند به گروههایی با تعداد مساوی high, average, low تقسیم بندی کنید.
این گزارش باید به همراه ستونهای name, revenue باشد؛ مرتب شده بر اساس revenue, name.
هدف اصلی از این گزارش کار با تابع ntile است که اطلاعات را بر اساس پارامتر ارسالی به آن تاجای ممکن به گروههای مساوی تقسیم میکند:
SELECT name, CASE WHEN class = 1 THEN 'high' WHEN class = 2 THEN 'average' ELSE 'low' END AS revenue FROM (SELECT facs.name AS name, ntile(3) OVER (ORDER BY sum(CASE WHEN memid = 0 THEN slots * facs.guestcost ELSE slots * membercost END) DESC) AS class FROM bookings AS bks INNER JOIN facilities AS facs ON bks.facid = facs.facid GROUP BY facs.name) AS subq ORDER BY class, name;
var facilities = context.Bookings.Select(booking => new { booking.Facility.Name, Revenue = booking.MemId == 0 ? booking.Slots * booking.Facility.GuestCost : booking.Slots * booking.Facility.MemberCost }) .GroupBy(b => b.Name) .Select(group => new { Name = group.Key, TotalRevenue = group.Sum(b => b.Revenue) }) .OrderByDescending(result => result.TotalRevenue) .ToList();
SELECT [f].[Name], SUM(CASE WHEN [b].[MemId] = 0 THEN CAST ([b].[Slots] AS DECIMAL (18, 6)) * [f].[GuestCost] ELSE CAST ([b].[Slots] AS DECIMAL (18, 6)) * [f].[MemberCost] END) AS [TotalRevenue] FROM [Bookings] AS [b] INNER JOIN [Facilities] AS [f] ON [b].[FacId] = [f].[FacId] GROUP BY [f].[Name] ORDER BY SUM(CASE WHEN [b].[MemId] = 0 THEN CAST ([b].[Slots] AS DECIMAL (18, 6)) * [f].[GuestCost] ELSE CAST ([b].[Slots] AS DECIMAL (18, 6)) * [f].[MemberCost] END) DESC;
var n = 3; var tiledFacilities = facilities.Select((item, index) => new { Item = item, Index = (index / n) + 1 }) .GroupBy(x => x.Index) .Select(g => g.Select(z => new { z.Item.Name, z.Item.TotalRevenue, Tile = g.Key, GroupName = g.Key == 1 ? "High" : (g.Key == 2 ? "Average" : "Low") }) .OrderBy(x => x.GroupName) .ThenBy(x => x.Name) ) .ToList(); var flatTiledFacilities = tiledFacilities.SelectMany(group => group) .Select(tile => new { tile.Name, Revenue = tile.GroupName }) .ToList();
مثال 21: چندماه طول میکشد تا هر کدام از امکانات موجود بر اساس فروشی که دارند، هزینهی مالکیت ابتدایی خود را کسب کنند.
این گزارش باید به همراه ستونهای name, months باشد؛ مرتب شده بر اساس name.
var facilities = context.Bookings.Select(booking => new { booking.Facility.Name, booking.Facility.InitialOutlay, booking.Facility.MonthlyMaintenance, Revenue = booking.MemId == 0 ? booking.Slots * booking.Facility.GuestCost : booking.Slots * booking.Facility.MemberCost }) .GroupBy(b => new { b.Name, b.InitialOutlay, b.MonthlyMaintenance }) .Select(group => new { group.Key.Name, RepayTime = group.Key.InitialOutlay / ((group.Sum(b => b.Revenue) / 3) - group.Key.MonthlyMaintenance) }) .OrderBy(result => result.Name) .ToList();
مثال 22: گزارش میانگین متحرک فروش کل هر کدام از روزهای August 2012 را برای یک بازهی 15 روزهی قبل، محاسبه کنید.
این گزارش باید به همراه ستونهای date, revenue باشد؛ مرتب شده بر اساس date. در این گزارش روزهای ماه 8 میلادی ردیف شده و به ازای هر ردیف، میانگین فروش 15 روز قبل از آن تاریخ، نمایش داده میشود. به همین جهت به آن میانگین متحرک نیز میگویند.
هدف اصلی از این گزارش، استفاده از توابع avg(revdata.rev) over است. اما چون نمیتوان از آنها در LINQ to Entities استفاده کرد، از روش دیگری که شامل جوین یک جدول با خودش است، استفاده میکنیم:
var startDate = new DateTime(2012, 08, 1); var endDate = new DateTime(2012, 08, 31); var period = 14; var dailyRevenueQuery = context.Bookings .Select(booking => new { StartDate = booking.StartTime.Date, // How to group by date (or TruncateTime) in EF-Core Revenue = booking.MemId == 0 ? booking.Slots * booking.Facility.GuestCost : booking.Slots * booking.Facility.MemberCost }) .GroupBy(b => b.StartDate) .Select(group => new { Date = group.Key, TotalRevenue = group.Sum(b => b.Revenue) });
اکنون که میزان کل فروش روزها را داریم، میخواهیم میانگین فروش 15 روز قبل شروع شدهی از از ابتدای ماه 8، تا انتهای آنرا محاسبه کنیم. برای اینکار نیاز است کوئری فوق را یکبار دیگر با خودش جوین کنیم تا از یک سر آن تاریخ هر روز و از طرف دیگر، میانگین 15 روز قبل، تولید شود:
var movingAvgs = dailyRevenueQuery .Select(dr1 => new { dr1.Date, MovingAvg = dailyRevenueQuery .Where(dr2 => dr2.Date <= dr1.Date && dr2.Date >= dr1.Date.AddDays(-period)) .Average(dr2 => dr2.TotalRevenue) }) .Where(result => result.Date >= startDate && result.Date <= endDate) .OrderBy(result => result.Date) .ToList();
کدهای کامل این قسمت را در اینجا میتوانید مشاهده کنید.
public AddUserStatus Add(User user) { if (ExistsByEmail(user.Email)) return AddUserStatus.EmailExist; if (ExistsByUserName(user.UserName)) return AddUserStatus.UserNameExist; _users.Add(user); return AddUserStatus.AddingUserSuccessfully; }
using System.Linq; using System.Web.Mvc; using Iris.Datalayer.Context; namespace Iris.Web.Controllers { public class MigrationController : Controller { public ActionResult RemoveDuplicateUsers() { var db = new IrisDbContext(); var lstDuplicateUserGroup = db.Users .GroupBy(u => u.UserName) .Where(g => g.Count() > 1) .ToList(); foreach (var duplicateUserGroup in lstDuplicateUserGroup) { foreach (var user in duplicateUserGroup.Skip(1).Where(user => user.UserMetaData != null)) { db.UserMetaDatas.Remove(user.UserMetaData); } db.Users.RemoveRange(duplicateUserGroup.Skip(1)); } db.SaveChanges(); return new EmptyResult(); } } }
مقایسه ساختار جداول دیتابیس کاربران IRIS با ASP.NET Identity
ساختار جداول ASP.NET Identity به شکل زیر است:
ساختار جداول سیستم کنونی هم بدین شکل است:
همان طور که مشخص است در هر دو سیستم، بین ساختار جداول و رابطهی بین آنها شباهتها و تفاوت هایی وجود دارد. سیستم Identity دو جدول بیشتر از IRIS دارد و برای جداولی که در سیستم کنونی وجود ندارند نیاز به انجام کاری نیست و به هنگام پیاده سازی Identity، این جداول به صورت خودکار به دیتابیس اضافه خواهند شد.
دو جدول مشترک در این دو سیستم، جداول Users و Roles هستندکه نحوهی ارتباطشان با یکدیگر متفاوت است. در Iris بین User و Role رابطهی یک به چند وجود دارد ولی در Identity، رابطهی بین این دو جدول چند به چند است و جدول واسط بین آنها نیز UserRoles نام دارد.
از آن جایی که من قصد دارم در سیستم جدید هم رابطهی بین کاربر و نقش چند به چند باشد، به پیش فرضهای Identity کاری ندارم. به رابطهی کنونی یک به چند کاربر و نقشش نیز دست نمیگذارم تا در انتها با یک کوئری از دیتابیس، اطلاعات نقشهای کاربران را به جدول جدیدش منتقل کنم.
جدولی که در هر دو سیستم مشترک است و هستهی آنها را تشکیل میدهد، جدول Users است. اگر دقت کنید میبینید که این جدول در هر دو سیستم، دارای یک سری فیلد مشترک است که دقیقا هم نام هستند مثل Id، UserName و Email؛ پس این فیلدها از نظر کاربرد در هر دو سیستم یکسان هستند و مشکلی ایجاد نمیکنند.
یک سری فیلد هم در جدول User در سیستم IRIS هست که در Identity نیست و بلعکس. با این فیلدها نیز کاری نداریم چون در هر دو سیستم کار مخصوص به خود را انجام میدهند و تداخلی در کار یکدیگر ایجاد نمیکنند.
اما فیلدی که برای ذخیره سازی پسورد در هر دو سیستم استفاده میشود دارای نامهای متفاوتی است. در Iris این فیلد Password نام دارد و در Identity نامش PasswordHash است.
برای اینکه در سیستم کنونی، نام فیلد Password جدول User را به PasswordHash تغییر دهیم قدمهای زیر را بر میداریم:
وارد پروژهی DomainClasses شده و کلاس User را باز کنید. سپس نام خاصیت Password را به PasswordHash تغییر دهید. پس از این تغییر بلافاصله یک گزینه زیر آن نمایان میشود که میخواهد در تمام جاهایی که از این نام استفاده شده است را به نام جدید تغییر دهد؛ آن را انتخاب کرده تا همه جا Password به PasswordHash تغییر کند.
برای این که این تغییر نام بر روی دیتابیس نیز اعمال شود باید از Migration استفاده کرد. در اینجا من از مهاجرت دستی که بر اساس کد هست استفاده میکنم تا هم بتوانم کدهای مهاجرت را پیش از اعمال بررسی و هم تاریخچهای از تغییرات را ثبت کنم.
برای این کار، Package Manager Console را باز کرده و از نوار بالایی آن، پروژه پیش فرض را بر روی DataLayer قرار دهید. سپس در کنسول، دستور زیر را وارد کنید:
Add-Migration Rename_PasswordToPasswordHash_User
اگر وارد پوشه Migrations پروژه DataLayer خود شوید، باید کلاسی با نامی شبیه به 201510090808056_Rename_PasswordToPasswordHash_User ببینید. اگر آن را باز کنید کدهای زیر را خواهید دید:
public partial class Rename_PasswordToPasswordHash_User : DbMigration { public override void Up() { AddColumn("dbo.Users", "PasswordHash", c => c.String(nullable: false, maxLength: 200)); DropColumn("dbo.Users", "Password"); } public override void Down() { AddColumn("dbo.Users", "Password", c => c.String(nullable: false, maxLength: 200)); DropColumn("dbo.Users", "PasswordHash"); } }
بدیهی هست که این کدها عمل حذف ستون Password را انجام میدهند که سبب از دست رفتن اطلاعات میشود. کدهای فوق را به شکل زیر ویرایش کنید تا تنها سبب تغییر نام ستون Password به PasswordHash شود.
public partial class Rename_PasswordToPasswordHash_User : DbMigration { public override void Up() { RenameColumn("dbo.Users", "Password", "PasswordHash"); } public override void Down() { RenameColumn("dbo.Users", "PasswordHash", "Password"); } }
سپس باز در کنسول دستور Update-Database را وارد کنید تا تغییرات بر روی دیتابیس اعمال شود.
دلیل اینکه این قسمت را مفصل بیان کردم این بود که میخواستم در مهاجرت از سیستم اعتبارسنجی خودتان به ASP.NET Identity دید بهتری داشته باشید.
تا به این جای کار فقط پایگاه داده سیستم کنونی را برای مهاجرت آماده کردیم و هنوز ASP.NET Identity را وارد پروژه نکردیم. در بخشهای بعدی Identity را نصب کرده و تغییرات لازم را هم انجام میدهیم.