به این صورت مدلهای خود را تعریف کرده و طبق مقالهی قبلی، Controllerهای هر یک را پیاده سازی نمایید:
public class Supplier { [Key] public string Key {get; set; } public string Name { get; set; } } public class Category { public int ID { get; set; } public string Name { get; set; } public virtual ICollection<Product> Products { get; set; } } public class Product { public int ID { get; set; } public string Name { get; set; } public decimal Price { get; set; } [ForeignKey("Category")] public int CategoryId { get; set; } public Category Category { get; set; } [ForeignKey("Supplier")] public string SupplierId { get; set; } public virtual Supplier Supplier { get; set; } }
پکیج Microsoft.AspNet.OData به تازگی ورژن 6 آن به صورت رسمی منتشر شده و شامل تغییراتی نسبت به نسخهی قبلی آن است. اولین نکتهی حائز اهمیت، Config آن است که به صورت زیر تغییر کرده و باید Optionهای مورد نیاز، کانفیگ شوند. در این نسخه DI نیز به Odata اضافه شده است:
public static void Register(HttpConfiguration config) { ODataModelBuilder odataModelBuilder = new ODataConventionModelBuilder(); var product = odataModelBuilder.EntitySet<Product>("Products"); var category = odataModelBuilder.EntitySet<Category>("Categories"); var supplier = odataModelBuilder.EntitySet<Supplier>("Suppliers"); var edmModel = odataModelBuilder.GetEdmModel(); supplier.EntityType.Ignore(c => c.Name); config.Select(System.Web.OData.Query.QueryOptionSetting.Allowed); config.MaxTop(25); config.OrderBy(System.Web.OData.Query.QueryOptionSetting.Allowed); config.Count(System.Web.OData.Query.QueryOptionSetting.Allowed); config.Expand(System.Web.OData.Query.QueryOptionSetting.Allowed); config.Filter(System.Web.OData.Query.QueryOptionSetting.Allowed); config.Count(System.Web.OData.Query.QueryOptionSetting.Allowed); //config.MapODataServiceRoute("ODataRoute", "odata", edmModel); // کانفیگ به صورت معمولی config.MapODataServiceRoute("ODataRoute", "odata", builder => { builder.AddService(ServiceLifetime.Singleton, sp => edmModel); builder.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefault()); }); }
[EnableQuery] public IQueryable<Product> Get() { return new List<Product> { new Product { Id = 1, Name = "name 1", Price = 11, Category = new Category {Id =1, Name = "Cat1" } }, new Product { Id = 2, Name = "name 2", Price = 12, Category = new Category {Id =2, Name = "Cat2" } }, new Product { Id = 3, Name = "name 3", Price = 13, Category = new Category {Id =3, Name = "Cat3" } }, new Product { Id = 4, Name = "name 4", Price = 14, Category = new Category {Id =4, Name = "Cat4" } }, }.AsQueryable(); ; }
Description | Option |
بسط دادن موجودیت مرتبط | $expand |
فیلتر کردن نتیجه، بر اساس شرطهای Boolean ی | $filter |
فرمان به سرور که تعداد رکوردهای بازگشتی را نیز نمایش دهد(مناسب برای پیاده سازی server-side pagging ) | $count |
مرتب کردن نتیجهی بازگشتی | $orderby |
select زدن روی پراپرتیهای درخواستی | $select |
پرش کردن از اولین رکورد به اندازهی n عدد | $skip |
فقط بازگرداندن n رکورد اول | $top |
/odata/Products?$select=Id,Name
/odata/Products?$top=3&$skip=2
/odata/Products?$count=true
{@odata.context: "http://localhost:4516/odata/$metadata#Products", @odata.count: 4,…} @odata.context:"http://localhost:4516/odata/$metadata#Products" @odata.count:4 value:[{Id: 1, Name: "name 1", Price: 11, SupplierId: 0, CategoryId: 0},…] 0:{Id: 1, Name: "name 1", Price: 11, SupplierId: 0, CategoryId: 0} 1:{Id: 2, Name: "name 2", Price: 12, SupplierId: 0, CategoryId: 0} 2:{Id: 3, Name: "name 3", Price: 13, SupplierId: 0, CategoryId: 0} 3:{Id: 4, Name: "name 4", Price: 14, SupplierId: 0, CategoryId: 0}
/odata/Products?$filter=Id eq 1
/odata/Products?$filter=Id gt 1 and Id lt 3
/odata/Products?$orderby=Id desc
/odata/Products?$expand=Category
/odata/Products?$expand=Category,Supplier
/odata/Categories(1)?$expand=Products/Supplier
[EnableQuery(PageSize = 10)]
محدود کردن Query Options
[EnableQuery (AllowedQueryOptions= AllowedQueryOptions.Skip | AllowedQueryOptions.Top)]
[EnableQuery(AllowedLogicalOperators=AllowedLogicalOperators.Equal)]
var product = odataModelBuilder.EntitySet<Product>("Products"); product.EntityType.Ignore(e => e.Price);
[EnableQuery(AllowedOrderByProperties = "Id,Name")]
public System.Web.Http.IHttpActionResult GetName(int key) { Product product = Get().Single(c => c.Id == key); return Ok(product.Name); }
/odata/Products(1)/Name/$value
HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 DataServiceVersion: 3.0 Content-Length: 3 Ali
Attribute Convention هایی هم برای اعتبارسنجی پراپرتیها موجود است که نام آنها واضح تعریف شدهاند:
Description | Attribute |
اجازهی فیلتر زدن بر روی آن پراپرتی داده نخواهد شد | NotFilterable |
اجازهی مرتب کردن بر روی آن پراپرتی داده نخواهد شد | NotSortable |
اجازهی select زدن بر روی آن پراپرتی داده نمیشود | NotNavigable |
اجازهی شمارش دهی بر روی آن Collection داده نمیشود | NotCountable |
اجازهی بسط دادن آن Collection داده نمیشود | NotExpandable |
و همچنین [AutoExpand] به صورت اتوماتیک آن موجودیت مورد نظر را بسط میدهد.
بطور مثال کدهای زیر را در مدل خود میتوانید مشاهده نمائید:
public class Product { public int Id { get; set; } public string Name { get; set; } [NotFilterable, NotSortable] public decimal Price { get; set; } [ForeignKey(nameof(SupplierId))] [NotNavigable] public virtual Supplier Supplier { get; set; } public int SupplierId { get; set; } [ForeignKey(nameof(CategoryId))] public virtual Category Category { get; set; } public int CategoryId { get; set; } [NotExpandable] public virtual ICollection<TestEntity> TestEnities { get; set; } }
فرض کنید پراپرتی زیر را به مدل خود اضافه کرده اید
public DateTimeOffset CreatedOn { get; set; }
حال کوئری زیر را برای فیلتر زدن، بر روی آن در اختیار داریم:
/odata/Products?$filter=year(CreatedOn) eq 2016
در اینجا فقط Product هایی بازگردانده میشوند که در سال 2016 ثبت شدهاند:
/odata/Products?$filter=CreatedOn lt cast(2017-04-01T04:11:31%2B08:00,Edm.DateTimeOffset)
کوئری فوق تاریخ مورد نظر را Cast کرده و همهی Product هایی را که قبل از این تاریخ ثبت شدهاند، باز میگرداند.
Nested Filter In Expand
/odata/Categories?$expand=Products($filter=Id gt 1 and Id lt 5)
همهی Categoryها به علاوه بسط دادن Product هایشان، در صورتیکه Id آنها بیشتر از 1 باشد
و یا حتی بر روی موجودیت بسط داده شده، select زده شود:
/odata/Categories?$expand=Products($select=Id,Name)
Custom Attribute
ضمنا به سادگی میتوان اتریبیوت سفارشی نوشت:
public class MyEnableQueryAttribute : EnableQueryAttribute { public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions) { // Don't apply Skip and Top. var ignoreQueryOptions = AllowedQueryOptions.Skip | AllowedQueryOptions.Top; return queryOptions.ApplyTo(queryable, ignoreQueryOptions); } }
روی هر متدی از کنترلر خود که اتریبیوت [MyEnableQuery] را قرار دهید، دیگر قابلیت Skip, Top را نخواهد داشت.
Dependency Injection در آخرین نسخهی OData اضافه شده است. بطور پیشفرض OData بصورت case-sensitive رفتار میکند. برای تغییر دادن آن در نسخههای قدیمی، Extension Methodی به نام EnableCaseSensitive وجود داشت. اما در نسخهی جدید شما میتوانید پیاده سازی خاص خود را از هر کدام از بخشهای OData داشته باشید و با استفاده از تزریق وابستگی، آن را به config برنامهی خود اضافه کنید؛ برای مثال:
public class CaseInsensitiveResolver : ODataUriResolver { private bool _enableCaseInsensitive; public override bool EnableCaseInsensitive { get { return true; } set { _enableCaseInsensitive = value; } } }
اینجا پیاده سازی از ODataUriResolver انجام شده و متد EnableCaseInsensitive به صورت جدیدی override و در حالت default مقدار true را برمیگرداند.
حال به صورت زیر آن را میتوان به وابستگیهای config برنامه، اضافه نمود:
config.MapODataServiceRoute("ODataRoute", "odata", builder => { builder.AddService(ServiceLifetime.Singleton, sp => edmModel); builder.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefault()); builder.AddService<ODataUriResolver>(ServiceLifetime.Singleton, sp => new CaseInsensitiveResolver()); // how enable case sensitive });
در قسمت بعدی به Actionها و Functionها در OData میپردازیم.
SQL Injection چیست؟
SQL Injection در لغت به معنی تزریق کد SQL میباشد. در اصلاح یعنی تزریق دستوراتی به کد SQL تولیدی یک نرم افزار به نحوی که به جای عمل مورد انتظار برنامه نویس آن، کاری را که ما میخواهیم انجام دهد. مثلا به جای اینکه هنگام ورود به برنامه وقتی کاربر مشخصات کاربری خود را وارد میکند، مشخصات کاربری را به نحوی وارد کنیم که بتوانیم بعنوان مدیر سامانه و یا یک کاربر معمولی بدون داشتن کلمه عبور وارد سیستم شویم.
البته همیشه از این نوع حمله برای ورود به سیستم استفاده نمیشود. یعنی ممکن است هکر به عنوان یک کاربر عادی وارد سیستم شود ولی با به کاربردن دستورات خاص SQL در بخشهای مختلف، بتواند اطلاعاتی را حذف نماید.
خوب حالا این کار چگونه انجام میشود؟
فرض کنید برنامه نویسی کد چک نام کاربری را اینگونه نوشته باشد:
SqlCommand cmd=new SqlCommand ("select count(*) from login where user='"+userName+"' and pass='"+password+"'",con);
فکر نکنید خوب این نوع کد نویسی مربوط به زمان تیرکمون شاه است! همین امروز در نظارت از یک پروژه به این نکته برخورد کردم! دلیل نوشتن این مقاله هم همین کد بود.
خوب حالا مگر کد بالا چه مشکلی دارد؟ ;) اگر کاربر در نامه کاربری و کلمه عبور مقادیر معمولی وارد کند (مانند admin, salam123) کد sql تولید شده به شکل زیر خواهد بود:
select count(*) from login where user='admin' and pass='salam123'
خوب حالا اگر کاربر کمی با ورودیها بازی کند. به عنوان مثال فرض کنید به جای کلمه عبور تایپ کند
' or 1=1 --
نتیجه حاصله خواهد بود:
select count(*) from login where user='admin' and pass='' or 1=1 --'
با وارد کردن این دستور کاربر بدون داشتن کلمه عبور خواهد توانست وارد سیستم شود. موردی که توضیح دادم پایه مسئله بود. ما قصد آموزش هک نداریم ولی داشتن اطلاعات پایه لازم است. ممکن است فردی بگوید خوب ما قبل از تولید همچین کدی ' را از رشته کلمه عبور حذف میکنیم. خیلی خوب ولی اگر هکر از معادل unicode آن استفاده کرد چه؟ اگر و اگر و اگر...
راه حلهای متعددی برای این موضوع پیشنهاد شده است. ولی سادهترین و کارآمدترین راه، استفاده از پارامترها میباشد که علاوه بر حذف این خطر باعث ایجاد و ذخیره query plan در sql server میشود و اجرای این query را در آینده تسریع میکند.
بنابراین میتوان کد فوق را به صورت زیر بازنویسی کرد:
SqlCommand cmd=new SqlCommand ("select count(*) from login where user=@u and pass=@p",con); cmd.Parameters.Add("@u", SqlDbType.Varchar, 10).Value=TextLogin.Text.Trim(); cmd.Parameters.Add("@p", SqlDbType.Varchar,10).Value=TextPwd.Text.Trim();
OVER ( [ <PARTITION BY clause> ] [ <ORDER BY clause> ] [ <ROW or RANGE clause> ] ) <PARTITION BY clause> ::= PARTITION BY value_expression , ... [ n ] <ORDER BY clause> ::= ORDER BY order_by_expression [ COLLATE collation_name ] [ ASC | DESC ] [ ,...n ] <ROW or RANGE clause> ::= { ROWS | RANGE } <window frame extent> <window frame extent> ::= { <window frame preceding> | <window frame between> } <window frame between> ::= BETWEEN <window frame bound> AND <window frame bound> <window frame bound> ::= { <window frame preceding> | <window frame following> } <window frame preceding> ::= { UNBOUNDED PRECEDING | <unsigned_value_specification> PRECEDING | CURRENT ROW } <window frame following> ::= { UNBOUNDED FOLLOWING | <unsigned_value_specification> FOLLOWING | CURRENT ROW } <unsigned value specification> ::= { <unsigned integer literal> }
CREATE TABLE REVENUE ( [DepartmentID] int, [Revenue] int, [Year] int ); insert into REVENUE values (1,10030,1998),(2,20000,1998),(3,40000,1998), (1,20000,1999),(2,60000,1999),(3,50000,1999), (1,40000,2000),(2,40000,2000),(3,60000,2000), (1,30000,2001),(2,30000,2001),(3,70000,2001)
select *, avg(Revenue) OVER (PARTITION by DepartmentID) as AverageRevenue, sum(Revenue) OVER (PARTITION by DepartmentID) as TotalRevenue from REVENUE order by departmentID, year;
insert into REVENUE values(1,90000,2002),(2,20000,2002),(3,80000,2002), (1,10300,2003),(2,1000,2003), (3,90000,2003), (1,10000,2004),(2,10000,2004),(3,10000,2004), (1,20000,2005),(2,20000,2005),(3,20000,2005), (1,40000,2006),(2,30000,2006),(3,30000,2006), (1,70000,2007),(2,40000,2007),(3,40000,2007), (1,50000,2008),(2,50000,2008),(3,50000,2008), (1,20000,2009),(2,60000,2009),(3,60000,2009), (1,30000,2010),(2,70000,2010),(3,70000,2010), (1,80000,2011),(2,80000,2011),(3,80000,2011), (1,10000,2012),(2,90000,2012),(3,90000,2012)
select Year, DepartmentID, Revenue, sum(Revenue) OVER (PARTITION by DepartmentID ORDER BY [YEAR] ROWS BETWEEN 3 PRECEDING AND CURRENT ROW) as Prev3 From REVENUE order by departmentID, year;
ROWS BETWEEN 3 PRECEDING AND CURRENT ROW) as Prev3
select Year, DepartmentID, Revenue, sum(Revenue) OVER (PARTITION by DepartmentID ORDER BY [YEAR] ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING) as Next3 From REVENUE order by departmentID, year;
select Year, DepartmentID, Revenue, min(Revenue) OVER (PARTITION by DepartmentID ORDER BY [YEAR] ROWS UNBOUNDED PRECEDING) as MinRevenueToDate From REVENUE order by departmentID, year;
RANGE UNBOUNDED PRECEDING AND CURRENT ROW
CREATE TABLE Employees ( EmployeeId INT IDENTITY PRIMARY KEY, Name VARCHAR(50), HireDate DATE NOT NULL, Salary INT NOT NULL ) GO INSERT INTO Employees (Name, HireDate, Salary) VALUES ('Alice', '2011-01-01', 20000), ('Brent', '2011-01-15', 19000), ('Carlos', '2011-02-01', 22000), ('Donna', '2011-03-01', 25000), ('Evan', '2011-04-01', 18500) GO
SELECT Name, Salary, AVG(Salary) OVER(ORDER BY HireDate) AS avgSalary FROM Employees GO
SELECT Name, Salary, AVG(Salary) OVER(ORDER BY HireDate RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS avgSalary FROM Employees GO
SELECT Name, Salary, AVG(Salary) OVER(ORDER BY HireDate RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING ) AS avgSalary FROM Employees GO
برای مثال دو جدول شهرها و افراد را درنظر بگیرید. مقصود از تعریف جدول شهرها در اینجا، مشخص سازی محل تولد افراد است:
public class Person { public int Id { get; set; } public string Name { get; set; } [ForeignKey("BornInCityId")] public virtual City BornInCity { get; set; } public int BornInCityId { get; set; } } public class City { public int Id { get; set; } public string Name { get; set; } public virtual ICollection<Person> People { get; set; } }
public class MyContext : DbContext { public DbSet<City> Cities { get; set; } public DbSet<Person> People { get; set; } }
و همچنین تعدادی رکورد آغازین را نیز به جداول مرتبط اضافه میکنیم:
public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { var city1 = new City { Name = "city-1" }; var city2 = new City { Name = "city-2" }; context.Cities.Add(city1); context.Cities.Add(city2); var person1 = new Person { Name = "user-1", BornInCity = city1 }; var person2 = new Person { Name = "user-2", BornInCity = city1 }; context.People.Add(person1); context.People.Add(person2); base.Seed(context); } }
public static class Test { public static void RunTests() { Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>()); using (var context = new MyContext()) { var peopleAndCitiesList = from person in context.People join city in context.Cities on person.BornInCityId equals city.Id select new { PersonName = person.Name, CityName = city.Name }; foreach (var item in peopleAndCitiesList) { Console.WriteLine("{0}:{1}", item.PersonName, item.CityName); } } } }
SELECT [Extent1].[BornInCityId] AS [BornInCityId], [Extent1].[Name] AS [Name], [Extent2].[Name] AS [Name1] FROM [dbo].[People] AS [Extent1] INNER JOIN [dbo].[Cities] AS [Extent2] ON [Extent1].[BornInCityId] = [Extent2].[Id]
var peopleAndCitiesList = context.People .Select(person => new { PersonName = person.Name, CityName = person.BornInCity.Name });
مثال دوم:
میخواهیم لیست شهرها را بر اساس تعداد کاربر متناظر به صورت نزولی مرتب کنیم:
var citiesList = context.Cities.OrderByDescending(x => x.People.Count()); foreach (var item in citiesList) { Console.WriteLine("{0}", item.Name); }
SELECT [Project1].[Id] AS [Id], [Project1].[Name] AS [Name] FROM ( SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], (SELECT COUNT(1) AS [A1] FROM [dbo].[People] AS [Extent2] WHERE [Extent1].[Id] = [Extent2].[BornInCityId]) AS [C1] FROM [dbo].[Cities] AS [Extent1] ) AS [Project1] ORDER BY [Project1].[C1] DESC
مثال سوم:
در ادامه قصد داریم لیست شهرها را به همراه تعداد نفرات متناظر با آنها نمایش دهیم:
var peopleAndCitiesList = context.Cities .Select(city => new { InUseCount = city.People.Count(), CityName = city.Name }); foreach (var item in peopleAndCitiesList) { Console.WriteLine("{0}:{1}", item.CityName, item.InUseCount); }
خروجی SQL کوئری فوق به نحو ذیل است:
SELECT [Extent1].[Id] AS [Id], (SELECT COUNT(1) AS [A1] FROM [dbo].[People] AS [Extent2] WHERE [Extent1].[Id] = [Extent2].[BornInCityId]) AS [C1], [Extent1].[Name] AS [Name] FROM [dbo].[Cities] AS [Extent1]
DECLARE @tbl AS TABLE (f1 NVARCHAR(50) COLLATE Arabic_CI_AS NULL)
INSERT INTO @tbl(f1) VALUES(NCHAR(1740))
SELECT * FROM @tbl
با بالا رفتن تعداد اشیاء تعریف شده در SQL server ، نگهداری آنها نیز مشکلتر میشود. در این حالت تغییر کوچکی در یکی از اشیاء ممکن است باعث از کار افتادن قسمتی از سیستم شود. بنابراین قبل از هر گونه تغییری در یک شیء، ابتدا باید سایر اشیاء وابسته به آن را یافت و در نظر داشت ( dependencies ). برای این منظور ( impact analysis ) راهکارهای مختلفی در SQL server وجود دارد که در ادامه به آنها خواهیم پرداخت:
الف) استفاده از امکانات management studio (اس کیوال سرور 2005 به بعد)
سادهترین راه ممکن که گزارش مفصلی را نیز ارائه میدهد، کلیک بر روی یک شیء در management studio و انتخاب گزینه view dependencies است (شکل زیر).
در صفحه ظاهر شده میتوان اشیایی را که شیء مورد نظر به آنها وابسته است، مشاهده نمود یا برعکس (اشیایی که عملکرد آنها وابسته به شیء انتخابی است را نیز میتوان ملاحظه کرد).
ب) کوئری گرفتن از جداول سیستمی
امکانات قسمت قبل را با استفاده از اطلاعات جدول syscomments نیز میتوان شبیه سازی کرد. در این جدول اطلاعات تعاریف کلیه view ، trigger ، رویههای ذخیره شده و غیره نگهداری میشود. برای مثال فرض کنید قصد داریم در جدول Orders دیتابیس Northwind ، نام فیلد OrderDate را تغییر دهیم. قبل از اینکار بهتر است کوئری زیر را اجرا کنیم تا نام اشیاء وابسته را بدست آوریم:
SELECT NAME
FROM syscomments c
JOIN sysobjects o
ON c.id = o.id
WHERE TEXT LIKE '%OrderDate%'
AND TEXT LIKE '%Orders%'
این روش انعطاف پذیری بیشتری را نسبت به امکانات قسمت الف ، ارائه میدهد. برای نمونه فرض کنید میخواهید در یک دیتابیس کلیه اشیایی که عملیات delete را انجام میدهند پیدا کنید (جستجوی اشیاء حاوی یک عبارت خاص). در این مورد خواهیم داشت:
SELECT NAME
FROM syscomments c
JOIN Northwind.dbo.sysobjects o
ON c.id = o.id
WHERE TEXT LIKE '%delete%'
جدول سیستمی دیگری در اس کیوال سرور به نام sysdepends وجود دارد که اطلاعات وابستگیهای اشیاء در آنها نگهداری میشود. برای دسترسی به اطلاعات این جدول ، اس کیوال سرور رویه ذخیره شده سیستمی sp_depends را ارائه داده است. برای مثال فرض کنید میخواهیم لیست اشیایی را که به جدول Oredres دیتابیس Northwind وابسته هستند، پیدا کنیم. در این حالت داریم:
USE Northwind
EXEC sp_depends 'Orders'
د) استفاده از schema view
با استفاده از view سیستمی INFORMATION_SCHEMA.ROUTINES ، که از ترکیب جداول syscolumns و sysobjects ایجاد شده است نیز میتوان عملکرد sp_depends را شبیه سازی کرد اما جداول و view ها از گزارش آن حذف شدهاند.
SELECT routine_name,
routine_type
FROM INFORMATION_SCHEMA.ROUTINES
WHERE routine_definition LIKE '%Orders%'
ه) استفاده از برنامه SQL Dependency Tracker
نسخه آزمایشی برنامه ذکر شده را از این آدرس میتوان دریافت کرد.
همانطور که ملاحظه میکنید این جستجوها بر روی اطلاعات ذخیره شده در اس کیوال سرور صورت میگیرند و اگر در کدهای خود در خارج از اس کیوال سرور مخلوطی از عبارات اس کیوال را داشته باشید، نگهداری آنها بسیار مشکل خواهد بود. بنابراین تا حد ممکن باید عملیات مرتبط را در دیتابیس و توسط اشیاء اس کیوال سرور مانند رویههای ذخیره شده، view ها و امثال آنها انجام داد تا این جدا سازی بهخوبی صورت گرفته و در زمان نیاز به انجام تغییرات، ردگیری اشیاء وابسته بهسادگی صورت گیرد.
v1 = context.Customers.FromSql($"SELECT * FROM Customers WHERE City = {city}"); var sql = $"SELECT * FROM Customers WHERE City = {city}"; v2 = context.Customers.FromSql(sql);
context.Products.FromSqlRaw( "SELECT * FROM Products WHERE Name = {0}", product.Name); context.Products.FromSqlInterpolated( $"SELECT * FROM Products WHERE Name = {product.Name}");
current=1&rowCount=10&sort[sender]=asc&searchPhrase=&id=b0df282a-0d67-40e5-8558-c9e93b7befed
[AttributeUsage(AttributeTargets.Property,Inherited = true)] public class RequestBodyField:Attribute { public string Field; public RequestBodyField(string field) { this.Field = field; } }
public class EmployeesRequestBody { [RequestBodyField("current")] public int CurrentPage { get; set; } [RequestBodyField("rowcount")] public int RowCount { get; set; } [RequestBodyField("searchPhrase")] public string SearchPhrase { get; set; } [RequestBodyField("sort")] public NameValueCollection SortDictionary { get; set; } }
public T GetFromQueryString<T>() where T : new() { var obj = new T(); var queryString = HttpContext.Current.Request.QueryString; var queries = HttpUtility.ParseQueryString(queryString.ToString()); var properties = typeof(T).GetProperties(); foreach (var property in properties) { foreach (Attribute attribute in property.GetCustomAttributes(true)) { var requestBodyField = attribute as RequestBodyField; if (requestBodyField == null) continue; //get value of query string var valueAsString = queries[requestBodyField.Field]; var converter = TypeDescriptor.GetConverter(property.PropertyType); var value = converter.ConvertFrom(valueAsString); if (value == null) continue; property.SetValue(obj, value, null); } } return obj; }
HttpContext.Current.Request.QueryString
سپس در مرحلهی بعدی با استفاده از Reflection پراپرتیهایی را که دارای attribute تعریف شده هستند، پیدا میکنیم.
var converter = TypeDescriptor.GetConverter(property.PropertyType); var value = converter.ConvertFrom(valueAsString);
سپس در صورتی که مقدار صحیح دریافت شود و برابر null نباشد، مقدار را در پراپرتی مربوطه جا میدهیم.
نکتهای که در اینجا نیاز به تلاش بیشتر دارد، کلید sort در کوئری استرینگ است. با نگاهی دقیقتر متوجه میشوید که خود کلید دو مقدار دارد که یکی از مقادیرش با کلید ترکیب شده است. این حالت روش ارسال آرایهها با نام کلیدی متفاوت در کوئری استرینگ است. این حالت ارسال باعث میشود که گرید بتواند حالت multi sort را نیز پیاده سازی کند.
پس برای دریافت این نوع مقادیر کمی کد به آن اضافه میکنیم. برای دریافت مقادیر آرایهای کد زیر را به سیستم اضافه میکنیم:
if (valueAsString == null) { var keys = from key in queries.AllKeys where key.StartsWith(requestBodyField.Field) select key; var collection = new NameValueCollection(); foreach (var key in keys) { var openBraketIndex = key.IndexOf("[", StringComparison.Ordinal); var closeBraketIndex = key.IndexOf("]", StringComparison.Ordinal); if (openBraketIndex < 0 || closeBraketIndex < 0) throw new Exception("query string is corrupted."); openBraketIndex++; //get key in [...] var fieldName = key.Substring(openBraketIndex, closeBraketIndex - openBraketIndex); collection.Add(fieldName, queries[key] ); } property.SetValue(obj, collection, null); continue; }
public T GetFromQueryString<T>() where T : new() { var obj = new T(); var properties = typeof(T).GetProperties(); var queryString = HttpContext.Current.Request.QueryString; var queries = HttpUtility.ParseQueryString(queryString.ToString()); foreach (var property in properties) { foreach (Attribute attribute in property.GetCustomAttributes(true)) { var requestBodyField = attribute as RequestBodyField; if (requestBodyField == null) continue; //get value of query string var valueAsString = queries[requestBodyField.Field]; if (valueAsString == null) { var keys = from key in queries.AllKeys where key.StartsWith(requestBodyField.Field) select key; var collection = new NameValueCollection(); foreach (var key in keys) { var openBraketIndex = key.IndexOf("[", StringComparison.Ordinal); var closeBraketIndex = key.IndexOf("]", StringComparison.Ordinal); if (openBraketIndex < 0 || closeBraketIndex < 0) throw new Exception("query string is corrupted."); openBraketIndex++; //get key in [...] var fieldName = key.Substring(openBraketIndex, closeBraketIndex - openBraketIndex); collection.Add(fieldName, queries[key]); } property.SetValue(obj, collection, null); continue; } var converter = TypeDescriptor.GetConverter(property.PropertyType); var value = converter.ConvertFrom(valueAsString); if (value == null) continue; property.SetValue(obj, value, null); } } return obj; }
حال به صورت زیر این متد را صدا میزنیم:
public virtual ActionResult GetEmployees() { var request = new Requests().GetFromQueryString<EmployeesRequestBody>(); }
using System; using System.Linq; using System.Windows.Forms; namespace WindowsFormsApplication2 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { // این قسمت برای ورود اطلاعات به بانک است و با کمک کتابخانه پرژن دات نت تاریخ شمسی را به میلادی تبدیل و ذخیره میکنم using(var db = new h7Entities()) { var t = new test { ResponseDate = PersianDateTime.Parse(textBox1.Text).ToDateTime() }; db.test.Add(t); db.SaveChanges(); } } private void Form1_Load(object sender, EventArgs e) { // این قسمت هم فقط اطلاعات واکشی شده را در گرید نمایش میدهد . بدون هیچ شرطی ، یک سلکت ساده . . فقط از پرژن دانت نت برای تبدیل میلادی به شمسی کمک میگیرم using (var db = new h7Entities()) { dataGridView1.DataSource =(from t in db.test select new { Id= t.Id, time = new PersianDateTime(DateTime.Parse(t.ResponseDate.ToString())).ToString(PersianDateTimeFormat.DateShortTime) }).ToList(); } } } }
کتابخانه PersianDateTime را از نیوگت دریافت کردم .
ولی چیزی در گرید نمایش نمیدهد .
مدل برنامه هم :
public partial class test { public int Id { get; set; } public Nullable<System.DateTime> ResponseDate { get; set; } }
سوال دیگه اینکه وقتی تبدیلی انجام نمیشود ، خروجی زیر را دارم :
حالا چطور از فیلدی که تاریخ را نمایش میدهد فقط آن را تبدیل به شمسی و نمایش دهد ؟ شبیه این 1365/02/02 ؟
تشکر