آیا با ازدیاد تعداد اکشنها و کنترلرها برای ذخیره سازی Claim ها در یک پروژه بزرگ، مشکل حجیم شدن کوکی یا توکن را نخواهیم داشت؟
مطالب
ایجاد فیلتر برای هدایت همهی درخواستها به صفحهی «در حال بهروزرسانی» در برنامههای ASP.NET MVC
مواقع زیادی اتفاق میافتد که باید کار خاصی، بر روی سایتی که publish شده انجام بدیم. مثلا قرار هست یک بهروزرسانی انجام بدیم یا هر عملیات خاصی دیگری. در این موارد اگر بخواهیم کاربر نتواند وارد سایت شود، اما سایت برای خودمان باز باشد تا بتوانیم عملیاتی را انجام دهیم، فیلتری را مینویسیم که همهی کاربران غیر از خودمان را به صفحهی HTML سادهای که قبلا طراحی کردهایم، منتقل کند. مثلا در این صفحه نوشته شده «در حال بروز رسانی هستیم ...»
برای اینکار یک فیلتر جدید را مینویسیم؛ به صورت زیر:
public sealed class DownForMaintenanceAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { var path = System.Web.Hosting.HostingEnvironment.MapPath("~/down.html"); var ip = WebConfigurationManager.AppSettings["MaintenanceIp"]; if (System.IO.File.Exists(path) && HttpContext.Current.Request.UserHostAddress != ip) { filterContext.HttpContext.Response.Clear(); filterContext.HttpContext.Response.Redirect("~/down.html"); return; } base.OnActionExecuting(filterContext); } }
ابتدا فایل HTML، در پوشهی پروژه جستجو میشود. همچنین آدرس ip ای که میتواند سایت را مشاهده کند، از web.config خوانده میشود. اگر چنین فایلی مثلا (down.html) در پروژه موجود بود، تمام درخواستهایی را که به سمت سایت میاید، به این صفحه هدایت میکنیم؛ غیر از درخواستهایی که از ip ذکر شده به سمت سایت میآیند (یعنی ip خودمان).
کد web.config
<add key="MaintenanceIp" value="x.x.x.x" />
filters.Add(new DownForMaintenanceAttribute());
با آمدن ORMها به دنیای برنامه نویسی، کار برنامه نویسی نسبت به قبل سادهتر و راحتتر شد. عدم استفاده کوئریهای دستی، پشتیبانی از چند دیتابیس و از همه مهمتر و اصلیترین هدف این ابزار "تنها درگیری با اشیا و مدل شیء گرایی" کار را پیش از پیش آسانتر نمود.
در این بین به راحتی میتوان چندین نمونه از این ORMها را نام برد مثل IBatis , Hibernate ,Nhibernate و EF که از معروفترین آنها هستند.
من در حال حاضر قصد شروع یک پروژه اندرویدی را دارم و دوست دارم بجای استفادهی از Sqlitehelper، از یک ORM مناسب بهره ببرم که چند سوال برای من پیش میآید. آیا ORM ای برای آن تهیه شده است؟ اگر آری چندتا و کدامیک از آنها بهتر هستند؟ شاید در اولین مورد کتابخانهی Hibernate جاوا را نام ببرید؛ ولی توجه به این نکته ضروری است که ما در مورد پلتفرم موبایل و محدودیتهای آن صحبت میکنیم. یک کتابخانه همانند Hibernate مطمئنا برای یک برنامه اندروید چه از نظر حجم نهایی برنامه و چه از نظر حجم بزرگش در اجرا، مشکل زا خواهد بود و وجود وابستگیهای متعدد و دارا بودن بسیاری از قابلیتهایی که اصلا در بانکهای اطلاعاتی موبایل قابل اجرا نیست، باعث میشود این فریمورک انتخاب خوبی برای یک برنامه اندروید نباشد.
معیارهای انتخاب یک فریم ورک مناسب برای موبایل:
OrmLight
این فریمورک مختص اندروید طراحی نشده ولی سبک بودن آن موجب شدهاست که بسیاری از برنامه نویسان از آن در برنامههای اندرویدی استفاده کنند. این فریم ورک جهت اتصالات JDBC و Spring و اندروید طراحی شده است.
نحوه معرفی جداول در این فریمورک به صورت زیر است:
در این بین به راحتی میتوان چندین نمونه از این ORMها را نام برد مثل IBatis , Hibernate ,Nhibernate و EF که از معروفترین آنها هستند.
من در حال حاضر قصد شروع یک پروژه اندرویدی را دارم و دوست دارم بجای استفادهی از Sqlitehelper، از یک ORM مناسب بهره ببرم که چند سوال برای من پیش میآید. آیا ORM ای برای آن تهیه شده است؟ اگر آری چندتا و کدامیک از آنها بهتر هستند؟ شاید در اولین مورد کتابخانهی Hibernate جاوا را نام ببرید؛ ولی توجه به این نکته ضروری است که ما در مورد پلتفرم موبایل و محدودیتهای آن صحبت میکنیم. یک کتابخانه همانند Hibernate مطمئنا برای یک برنامه اندروید چه از نظر حجم نهایی برنامه و چه از نظر حجم بزرگش در اجرا، مشکل زا خواهد بود و وجود وابستگیهای متعدد و دارا بودن بسیاری از قابلیتهایی که اصلا در بانکهای اطلاعاتی موبایل قابل اجرا نیست، باعث میشود این فریمورک انتخاب خوبی برای یک برنامه اندروید نباشد.
معیارهای انتخاب یک فریم ورک مناسب برای موبایل:
- سبک بودن: مهمترین مورد سبک بودن آن است؛ چه از لحاظ اجرای برنامه و چه از لحاظ حجم نهایی برنامه
- سریع بودن: مطمئنا ORMهای طراحی شدهی موجود، از سرعت خیلی بدی برخوردار نخواهند بود؛ اگر سر زبان هم افتاده باشند. ولی باز هم انتخاب سریع بودن یک ORM، مورد علاقهی بسیاری از ماهاست.
- یادگیری آسان و کانفیگ راحت تر.
OrmLight
این فریمورک مختص اندروید طراحی نشده ولی سبک بودن آن موجب شدهاست که بسیاری از برنامه نویسان از آن در برنامههای اندرویدی استفاده کنند. این فریم ورک جهت اتصالات JDBC و Spring و اندروید طراحی شده است.
نحوه معرفی جداول در این فریمورک به صورت زیر است:
@DatabaseTable(tableName = "users") public class User { @DatabaseField(id = true) private String username; @DatabaseField private String password; public User() { // ORMLite needs a no-arg constructor } public User(String username, String password) { this.username = username; this.password = password; } // Implementing getter and setter methods public String getUserame() { return this.username; } public void setName(String username) { this.username = username; } public String getPassword() { return this.password; } public void setPassword(String password) { this.password = password; } }
سورس این فریمورک را میتوان در گیت هاب یافت و مستندات آن در این آدرس قرار دارند.
SugarORM
این فریمورک مختص اندروید طراحی شده است. یادگیری آن بسیار آسان است و به راحتی به یاد میماند. همچنین جداول مورد نیاز را به طور خودکار خواهد ساخت. روابط یک به یک و یک به چند را پشتیبانی میکند و عملیات CURD را با سه متد Save,Delete و Find که البته FindById هم جزء آن است، پیاده سازی میکند.
برای استفاده از این فریمورک نیاز است ابتدا متادیتاهای زیر را به فایل manifest اضافه کنید:
SugarORM
این فریمورک مختص اندروید طراحی شده است. یادگیری آن بسیار آسان است و به راحتی به یاد میماند. همچنین جداول مورد نیاز را به طور خودکار خواهد ساخت. روابط یک به یک و یک به چند را پشتیبانی میکند و عملیات CURD را با سه متد Save,Delete و Find که البته FindById هم جزء آن است، پیاده سازی میکند.
برای استفاده از این فریمورک نیاز است ابتدا متادیتاهای زیر را به فایل manifest اضافه کنید:
<meta-data android:name="DATABASE" android:value="my_database.db" /> <meta-data android:name="VERSION" android:value="1" /> <meta-data android:name="QUERY_LOG" android:value="true" /> <meta-data android:name="DOMAIN_PACKAGE_NAME" android:value="com.my-domain" />
برای تبدیل یک کلاس به جدول هم از کلاس این فریم ورک ارث بری میکنیم:
public class User extends SugarRecord<User> { String username; String password; int age; @Ignore String bio; //this will be ignored by SugarORM public User() { } public User(String username, String password,int age){ this.username = username; this.password = password; this.age = age; } }
باقی عملیات آن از قبیل اضافه کردن یک رکورد جدید یا حذف رکورد(ها) به صورت زیر است:
User johndoe = new User(getContext(),"john.doe","secret",19); johndoe.save(); //ذخیره کاربر جدید در دیتابیس //حذف تمامی کاربرانی که سنشان 19 سال است List<User> nineteens = User.find(User.class,"age = ?",new int[]{19}); foreach(user in nineteens) { user.delete(); }
GreenDAO
موقعیکه بحث کارآیی و سرعت پیش میآید نام GreenDAO هست که میدرخشد. طبق گفتهی سایت رسمی آن این فریمورک میتواند در ثانیه چند هزار موجودیت را اضافه و به روزرسانی و بارگیری نماید. این لیست حاوی برنامههایی است که از این فریمورک استفاده میکنند. جدول زیر مقایسهای است بین این کتابخانه و OrmLight که نشان میدهد 4.5 برابر سریعتر از OrmLight عمل میکند.
موقعیکه بحث کارآیی و سرعت پیش میآید نام GreenDAO هست که میدرخشد. طبق گفتهی سایت رسمی آن این فریمورک میتواند در ثانیه چند هزار موجودیت را اضافه و به روزرسانی و بارگیری نماید. این لیست حاوی برنامههایی است که از این فریمورک استفاده میکنند. جدول زیر مقایسهای است بین این کتابخانه و OrmLight که نشان میدهد 4.5 برابر سریعتر از OrmLight عمل میکند.
غیر از اینها در زمینهی حجم هم حرفهایی برای گفتن دارد. حجم این کتابخانه کمتر از 100 کیلوبایت است که در اندازهی APK اثر چندانی نخواهد داشت.
آموزش راه اندازی آن در اندروید استادیو، سورس آن در گیت هاب و مستندات رسمی آن.
Active Android
این کتابخانه از دو طریق فایل JAR و به شیوه maven قابل استفاده است که میتوانید روش استفادهی از آن را در این لینک ببینید و سورس اصلی آن هم در این آدرس قرار دارد. بعد از اینکه کتابخانه را به پروژه اضافه کردید، دو متادیتای زیر را که به ترتیب نام دیتابیس و ورژن آن هستند، به manifest اضافه کنید:
آموزش راه اندازی آن در اندروید استادیو، سورس آن در گیت هاب و مستندات رسمی آن.
Active Android
این کتابخانه از دو طریق فایل JAR و به شیوه maven قابل استفاده است که میتوانید روش استفادهی از آن را در این لینک ببینید و سورس اصلی آن هم در این آدرس قرار دارد. بعد از اینکه کتابخانه را به پروژه اضافه کردید، دو متادیتای زیر را که به ترتیب نام دیتابیس و ورژن آن هستند، به manifest اضافه کنید:
<meta-data android:name="AA_DB_NAME" android:value="my_database.db" /> <meta-data android:name="AA_DB_VERSION" android:value="1" />
public class MyActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ActiveAndroid.initialize(this); //ادامه برنامه } }
@Table(name = "User") public class User extends Model { @Column(name = "username") public String username; @Column(name = "password") public String password; public User() { super(); } public User(String username,String password) { super(); this.username = username; this.password = password; } }
ORMDroid
از آن دست کتابخانههایی است که سادگی و کم حجم بودن شعار آنان است و سعی دارند تا حد ممکن همه چیز را خودکار کرده و کمترین کانفیگ را نیاز داشته باشد. حجم فعلی آن حدود 20 کیلوبایت بوده و نمیخواهند از 30 کیلوبایت تجاوز کند.
برای استفادهی از آن ابتدا دو خط زیر را جهت معرفی تنظیمات به manifest اضافه کنید:
<meta-data android:name="ormdroid.database.name" android:value="your_database_name" /> <meta-data android:name="ormdroid.database.visibility" android:value="PRIVATE||WORLD_READABLE||WORLD_WRITEABLE" />
ORMDroidApplication.initialize(someContext);
public class Person extends Entity { public int id; public String name; public String telephone; } //==================== Person p = Entity.query(Person.class).where("id=1").execute(); p.telephone = "555-1234"; p.save(); // یا Person person = Entity.query(Person.class).where(eql("id", id)).execute(); p.telephone = "555-1234"; p.save();
کد بالا دقیقا یادآوری به EF هست ولی حیف که از Linq پشتیبانی نمیشود.
سورس آن در گیت هاب
در اینجا سعی کردیم تعدادی از کتابخانههای محبوب را معرفی کنیم ولی تعداد آن به همین جا ختم نمیشود. ORMهای دیگری نظیر AndRom و سایر ORM هایی که در این لیست معرفی شده اند وجود دارند.
نکته نهایی اینکه خوب میشود دوستانی که از این ORMهای مختص اندروید استفاده کرده اند؛ نظراتشان را در مورد آنها بیان کنند و مزایا و معایب آنها را بیان کنند.
سورس آن در گیت هاب
در اینجا سعی کردیم تعدادی از کتابخانههای محبوب را معرفی کنیم ولی تعداد آن به همین جا ختم نمیشود. ORMهای دیگری نظیر AndRom و سایر ORM هایی که در این لیست معرفی شده اند وجود دارند.
نکته نهایی اینکه خوب میشود دوستانی که از این ORMهای مختص اندروید استفاده کرده اند؛ نظراتشان را در مورد آنها بیان کنند و مزایا و معایب آنها را بیان کنند.
یکی دیگر از ویجتهای Kendo UI یک HTML Editor کامل است به همراه امکانات ارسال فایل، تصویر و ... پشتیبانی از راست به چپ. در ادامه قصد داریم نحوهی مدیریت نمایش لیست فایلها، افزودن و حذف آنها را از طریق این ادیتور بررسی کنیم.
تنظیمات ابتدایی Kendo UI Editor
در ذیل کدهای سمت کاربر فعال سازی مقدماتی Kendo UI را مشاهده میکنید. در قسمت tools آن، لیست امکانات و نوار ابزار مهیای آن درج شدهاند.
دو مورد insertImage و insertFile آن نیاز به تنظیمات سمت کاربر و سرور بیشتری دارند.
در اینجا نحوهی تنظیم مسیرهای مختلف ارسال فایل و تصویر Kendo UI Editor را ملاحظه میکنید.
منهای قسمت thumbnailUrl، عملکرد قسمتهای مختلف افزودن فایل و تصویر این ادیتور یکسان هستند. به همین جهت میتوان برای مثال کنترلی مانند KendoEditorFilesController را ایجاد و سپس در کنترلر KendoEditorImagesController از آن ارث بری کرد و متد دریافت و نمایش بند انگشتی تصاویر را افزود. به این ترتیب دیگر نیازی به تکرار کدهای مشترک بین این دو قسمت نخواهد بود.
نمایش لیست پوشهها و تصویر در ابتدای باز شدن صفحهی درج تصویر
با کلیک بر روی دکمهی نمایش لیست تصاویر، صفحه دیالوگی مانند شکل زیر ظاهر خواهد شد:
تنظیمات خواندن این فایلها، از قسمت read مربوط به imageBrowser دریافت میشود که cache آن نیز به false تنظیم شدهاست تا در این بین مرورگر اطلاعات را کش نکند. این مورد در حین حذف فایلها و پوشهها مهم است. زیرا اگر cache:false تنظیم نشده باشد، حذف یک فایل یا پوشه در سمت کاربر تاثیری نخواهد داشت.
در ادامه نیاز است اکشن متد GetFilesList را به نحو ذیل در سمت سرور تهیه کرد:
در اینجا کدهای کلاس پایه KendoEditorFilesController را مشاهده میکنید. به این جهت فیلد FilesFolder آن protected تعریف شدهاست تا در کلاسی که از آن ارث بری میکند نیز قابل دسترسی باشد. سپس لیست فایلها و پوشههای path دریافتی با فرمت لیستی از KendoFile تهیه شده و با فرمت JSON بازگشت داده میشوند. ساختار KendoFile را در ذیل مشاهده میکنید:
- در اینجا Type میتواند از نوع فایل با مقدار f و یا از نوع پوشه با مقدار d باشد.
- علت استفاده از CamelCasePropertyNamesContractResolver در حین بازگشت JSON نهایی، تبدیل خواص دات نتی، به نامهای سازگار با JavaScript است. برای مثال به صورت خودکار Name را تبدیل به name میکند.
- پارامتر path در ابتدای کار خالی است. اما کاربر میتواند در بین پوشههای باز شدهی توسط مرورگر تصاویر Kendo UI حرکت کند. به همین جهت مقدار آن باید هربار بررسی شده و بر این اساس لیست فایلها و پوشههای جاری بازگشت داده شوند.
مدیریت حذف تصاویر و پوشهها
همانطور که در شکل فوق نیز مشخص است، با انتخاب یک پوشه یا فایل، دکمهای با آیکن ضربدر جهت فراهم آوردن امکان حذف، ظاهر میشود. این دکمه متصل است به قسمت destroy تنظیمات ادیتور:
این تنظیمات سمت کاربر را باید به نحو ذیل در سمت سرور مدیریت کرد:
- استفاده از Path.GetFileName جهت دریافت نام فایلها در اینجا بسیار مهم است. زیرا اگر این تمیز سازی امنیتی صورت نگیرد، ممکن است با کمی تغییر در آن، فایل web.config برنامه، دریافت یا حذف شود.
- پارامتر name دریافتی مساوی است با نام فایل انتخاب شده و path مشخص میکند که در کدام پوشه قرار داریم.
- چون در اینجا امکان حذف یک پوشه یا فایل وجود دارد، حتما نیاز است بررسی کنیم، مسیر دریافتی پوشهاست یا فایل و سپس بر این اساس جهت حذف آنها اقدام صورت گیرد.
مدیریت ایجاد یک پوشهی جدید
تنظیمات قسمت create مرورگر تصاویر، مرتبط است به زمانیکه کاربر با کلیک بر روی دکمهی +، درخواست ایجاد یک پوشهی جدید را کردهاست:
کدهای اکشن متد متناظر با این عمل را در ذیل مشاهده میکنید:
- در اینجا نیز name مساوی نام پوشهی درخواستی است و path به مسیر تو در توی پوشهی جاری اشاره میکند.
- پس از ایجاد پوشه، باید نام آنرا با فرمت KendoFile به صورت JSON بازگشت داد. همچنین در اینجا Type را نیز باید به d (پوشه) تنظیم کرد.
مدیریت قسمت ارسال فایل و تصویر
زمانیکه کاربر بر روی دکمهی upload file یا بارگذاری تصاویر در اینجا کلیک میکند، اطلاعات فایل آپلودی به مسیر uploadUrl ارسال میگردد.
دو تنظیم دیگر thumbnailUrl و imageUrl، برای نمایش بند انگشتی و نمایش کامل تصویر کاربرد دارند.
در ادامه کدهای مدیریت سمت سرور قسمت آپلود این ادیتور را مشاهده میکنید:
- در اینجا path مشخص میکند که در کدام پوشهی تو در تو قرار داریم و file نیز حاوی محتوای ارسالی به سرور است.
- پس از ذخیره سازی اطلاعات فایل، نیاز است اطلاعات فایل نهایی را با فرمت KendoFile به صورت JSON بازگشت دهیم.
ارث بری از KendoEditorFilesController جهت تکمیل قسمت مدیریت تصاویر
تا اینجا کدهایی را که ملاحظه کردید، برای هر دو قسمت ارسال تصویر و فایل کاربرد دارند. قسمت ارسال تصاویر برای تکمیل نیاز به متد دریافت تصاویر به صورت بند انگشتی نیز دارد که به صورت ذیل قابل تعریف است و چون از کلاس پایه KendoEditorFilesController ارث بری کردهاست، این کنترلر به صورت خودکار حاوی اکشن متدهای کلاس پایه نیز خواهد بود.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید.
تنظیمات ابتدایی Kendo UI Editor
در ذیل کدهای سمت کاربر فعال سازی مقدماتی Kendo UI را مشاهده میکنید. در قسمت tools آن، لیست امکانات و نوار ابزار مهیای آن درج شدهاند.
دو مورد insertImage و insertFile آن نیاز به تنظیمات سمت کاربر و سرور بیشتری دارند.
<!--نحوهی راست به چپ سازی --> <div class="k-rtl"> <textarea id="editor" rows="10" cols="30" style="height: 440px"></textarea> </div> @section JavaScript { <script type="text/javascript"> $(function () { $("#editor").kendoEditor({ tools: [ "bold", "italic", "underline", "strikethrough", "justifyLeft", "justifyCenter", "justifyRight", "justifyFull", "insertUnorderedList", "insertOrderedList", "indent", "outdent", "createLink", "unlink", "insertImage", "insertFile", "subscript", "superscript", "createTable", "addRowAbove", "addRowBelow", "addColumnLeft", "addColumnRight", "deleteRow", "deleteColumn", "viewHtml", "formatting", "cleanFormatting", "fontName", "fontSize", "foreColor", "backColor", "print" ], imageBrowser: { messages: { dropFilesHere: "فایلهای خود را به اینجا کشیده و رها کنید" }, transport: { read: { url: "@Url.Action("GetFilesList", "KendoEditorImages")", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET', cache: false }, destroy: { url: "@Url.Action("DestroyFile", "KendoEditorImages")", type: "POST" }, create: { url: "@Url.Action("CreateFolder", "KendoEditorImages")", type: "POST" }, thumbnailUrl: "@Url.Action("GetThumbnail", "KendoEditorImages")", uploadUrl: "@Url.Action("UploadFile", "KendoEditorImages")", imageUrl: "@Url.Action("GetFile", "KendoEditorImages")?path={0}" } }, fileBrowser: { messages: { dropFilesHere: "فایلهای خود را به اینجا کشیده و رها کنید" }, transport: { read: { url: "@Url.Action("GetFilesList", "KendoEditorFiles")", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET', cache: false }, destroy: { url: "@Url.Action("DestroyFile", "KendoEditorFiles")", type: "POST" }, create: { url: "@Url.Action("CreateFolder", "KendoEditorFiles")", type: "POST" }, uploadUrl: "@Url.Action("UploadFile", "KendoEditorFiles")", fileUrl: "@Url.Action("GetFile", "KendoEditorFiles")?path={0}" } } }); }); </script> }
منهای قسمت thumbnailUrl، عملکرد قسمتهای مختلف افزودن فایل و تصویر این ادیتور یکسان هستند. به همین جهت میتوان برای مثال کنترلی مانند KendoEditorFilesController را ایجاد و سپس در کنترلر KendoEditorImagesController از آن ارث بری کرد و متد دریافت و نمایش بند انگشتی تصاویر را افزود. به این ترتیب دیگر نیازی به تکرار کدهای مشترک بین این دو قسمت نخواهد بود.
نمایش لیست پوشهها و تصویر در ابتدای باز شدن صفحهی درج تصویر
با کلیک بر روی دکمهی نمایش لیست تصاویر، صفحه دیالوگی مانند شکل زیر ظاهر خواهد شد:
تنظیمات خواندن این فایلها، از قسمت read مربوط به imageBrowser دریافت میشود که cache آن نیز به false تنظیم شدهاست تا در این بین مرورگر اطلاعات را کش نکند. این مورد در حین حذف فایلها و پوشهها مهم است. زیرا اگر cache:false تنظیم نشده باشد، حذف یک فایل یا پوشه در سمت کاربر تاثیری نخواهد داشت.
imageBrowser: { transport: { read: { url: "@Url.Action("GetFilesList", "KendoEditorImages")", dataType: "json", contentType: 'application/json; charset=utf-8', type: 'GET', cache: false } } },
namespace KendoUI13.Controllers { public class KendoEditorFilesController : Controller { //مسیر پوشه فایلها protected string FilesFolder = "~/files"; protected string KendoFileType = "f"; protected string KendoDirType = "d"; [HttpGet] public ActionResult GetFilesList(string path) { path = GetSafeDirPath(path); var imagesList = new DirectoryInfo(path) .GetFiles() .Select(fileInfo => new KendoFile { Name = fileInfo.Name, Size = fileInfo.Length, Type = KendoFileType }).ToList(); var foldersList = new DirectoryInfo(path) .GetDirectories() .Select(directoryInfo => new KendoFile { Name = directoryInfo.Name, Type = KendoDirType }).ToList(); return new ContentResult { Content = JsonConvert.SerializeObject(imagesList.Union(foldersList), new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }), ContentType = "application/json", ContentEncoding = Encoding.UTF8 }; } protected string GetSafeDirPath(string path) { // path = مسیر زیر پوشهی وارد شده if (string.IsNullOrWhiteSpace(path)) { return Server.MapPath(FilesFolder); } //تمیز سازی امنیتی path = Path.GetDirectoryName(path); path = Path.Combine(Server.MapPath(FilesFolder), path); return path; } } }
namespace KendoUI13.Models { public class KendoFile { public string Name { set; get; } public string Type { set; get; } public long Size { set; get; } } }
- علت استفاده از CamelCasePropertyNamesContractResolver در حین بازگشت JSON نهایی، تبدیل خواص دات نتی، به نامهای سازگار با JavaScript است. برای مثال به صورت خودکار Name را تبدیل به name میکند.
- پارامتر path در ابتدای کار خالی است. اما کاربر میتواند در بین پوشههای باز شدهی توسط مرورگر تصاویر Kendo UI حرکت کند. به همین جهت مقدار آن باید هربار بررسی شده و بر این اساس لیست فایلها و پوشههای جاری بازگشت داده شوند.
مدیریت حذف تصاویر و پوشهها
همانطور که در شکل فوق نیز مشخص است، با انتخاب یک پوشه یا فایل، دکمهای با آیکن ضربدر جهت فراهم آوردن امکان حذف، ظاهر میشود. این دکمه متصل است به قسمت destroy تنظیمات ادیتور:
imageBrowser: { transport: { destroy: { url: "@Url.Action("DestroyFile", "KendoEditorImages")", type: "POST" } } },
namespace KendoUI13.Controllers { public class KendoEditorFilesController : Controller { //مسیر پوشه فایلها protected string FilesFolder = "~/files"; protected string KendoFileType = "f"; protected string KendoDirType = "d"; [HttpPost] public ActionResult DestroyFile(string name, string path) { //تمیز سازی امنیتی name = Path.GetFileName(name); path = GetSafeDirPath(path); var pathToDelete = Path.Combine(path, name); var attr = System.IO.File.GetAttributes(pathToDelete); if ((attr & FileAttributes.Directory) == FileAttributes.Directory) { Directory.Delete(pathToDelete, recursive: true); } else { System.IO.File.Delete(pathToDelete); } return Json(new object[0]); } } }
- پارامتر name دریافتی مساوی است با نام فایل انتخاب شده و path مشخص میکند که در کدام پوشه قرار داریم.
- چون در اینجا امکان حذف یک پوشه یا فایل وجود دارد، حتما نیاز است بررسی کنیم، مسیر دریافتی پوشهاست یا فایل و سپس بر این اساس جهت حذف آنها اقدام صورت گیرد.
مدیریت ایجاد یک پوشهی جدید
تنظیمات قسمت create مرورگر تصاویر، مرتبط است به زمانیکه کاربر با کلیک بر روی دکمهی +، درخواست ایجاد یک پوشهی جدید را کردهاست:
imageBrowser: { transport: { create: { url: "@Url.Action("CreateFolder", "KendoEditorImages")", type: "POST" } } },
namespace KendoUI13.Controllers { public class KendoEditorFilesController : Controller { //مسیر پوشه فایلها protected string FilesFolder = "~/files"; protected string KendoFileType = "f"; protected string KendoDirType = "d"; [HttpPost] public ActionResult CreateFolder(string name, string path) { //تمیز سازی امنیتی name = Path.GetFileName(name); path = GetSafeDirPath(path); var dirToCreate = Path.Combine(path, name); Directory.CreateDirectory(dirToCreate); return KendoFile(new KendoFile { Name = name, Type = KendoDirType }); } protected ActionResult KendoFile(KendoFile file) { return new ContentResult { Content = JsonConvert.SerializeObject(file, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver() }), ContentType = "application/json", ContentEncoding = Encoding.UTF8 }; } } }
- پس از ایجاد پوشه، باید نام آنرا با فرمت KendoFile به صورت JSON بازگشت داد. همچنین در اینجا Type را نیز باید به d (پوشه) تنظیم کرد.
مدیریت قسمت ارسال فایل و تصویر
زمانیکه کاربر بر روی دکمهی upload file یا بارگذاری تصاویر در اینجا کلیک میکند، اطلاعات فایل آپلودی به مسیر uploadUrl ارسال میگردد.
imageBrowser: { transport: { thumbnailUrl: "@Url.Action("GetThumbnail", "KendoEditorImages")", uploadUrl: "@Url.Action("UploadFile", "KendoEditorImages")", imageUrl: "@Url.Action("GetFile", "KendoEditorImages")?path={0}" } },
در ادامه کدهای مدیریت سمت سرور قسمت آپلود این ادیتور را مشاهده میکنید:
namespace KendoUI13.Controllers { public class KendoEditorFilesController : Controller { //مسیر پوشه فایلها protected string FilesFolder = "~/files"; protected string KendoFileType = "f"; protected string KendoDirType = "d"; [HttpPost] public ActionResult UploadFile(HttpPostedFileBase file, string path) { //تمیز سازی امنیتی var name = Path.GetFileName(file.FileName); path = GetSafeDirPath(path); var pathToSave = Path.Combine(path, name); file.SaveAs(pathToSave); return KendoFile(new KendoFile { Name = name, Size = file.ContentLength, Type = KendoFileType }); } } }
- پس از ذخیره سازی اطلاعات فایل، نیاز است اطلاعات فایل نهایی را با فرمت KendoFile به صورت JSON بازگشت دهیم.
ارث بری از KendoEditorFilesController جهت تکمیل قسمت مدیریت تصاویر
تا اینجا کدهایی را که ملاحظه کردید، برای هر دو قسمت ارسال تصویر و فایل کاربرد دارند. قسمت ارسال تصاویر برای تکمیل نیاز به متد دریافت تصاویر به صورت بند انگشتی نیز دارد که به صورت ذیل قابل تعریف است و چون از کلاس پایه KendoEditorFilesController ارث بری کردهاست، این کنترلر به صورت خودکار حاوی اکشن متدهای کلاس پایه نیز خواهد بود.
using System.Web.Mvc; namespace KendoUI13.Controllers { public class KendoEditorImagesController : KendoEditorFilesController { public KendoEditorImagesController() { // بازنویسی مسیر پوشهی فایلها FilesFolder = "~/images"; } [HttpGet] [OutputCache(Duration = 3600, VaryByParam = "path")] public ActionResult GetThumbnail(string path) { //todo: create thumb/ resize image path = GetSafeFileAndDirPath(path); return File(path, "image/png"); } } }
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید.
نظرات مطالب
دستیابی به HttpContext در Blazor Server
بنا بر گفته یکی از دوستان که از این روش استفاده کرده بود و نتیجه خوبی گرفته بود.من این نوع متغیر رو بدون استفاده از تزریق وابستگی در کلاس کانتکس new دستی کردم و تو این مدت باهاش کار کردیم به درستی جواب داده.
تو این حالت میشه به صورت مستقیم هم از کلاس Product، آبجکت تعریف کرد. پس در این حالت کاهش وابستگی دقیقا به چه صورتی هست؟ چه فرقی هست بین ()<AbstractFactory.CreateObject<Product و ()new Product؟
در قسمت قبل دیدیم که انجام کارهای همزمان، با Objectهایی که به اصطلاح Thread Safe نیستند (مانند DbContext) خروجی چندان جالبی ندارد و برای مثال اگر در یک Service یک DbContext را Inject کنیم (مثلا با Constructor injection) و از آن در متدی استفاده کنیم که آن متد یا با TPL یا RX و ... به صورت چندتایی و همزمان اجرا شود، DbContext به مشکل میخورد؛ یعنی نمیتوان یک وهله از DbContext را بین چند Thread همزمان پردازش موازی، به اشتراک گذاشت.
در کدهای فرضی مثالهای قسمت قبل، متدی داشتیم با نام DoSomethingWithCustomer که مثلا همان متدی بود که قرار است همزمان اجرا شود. یکی از سادهترین کارهایی که برای رفع این مشکل میتوان انجام داد، نوشتن چنین کدی است:
public async Task DoSomethingWithCustomer(Customer customer) { using var dbContext = new AppDbContext(); // ... }
این روش ابدا توصیه نمیشود؛ برای اینکه Dependency Injection این روزها مسائل خیلی زیادی را مدیریت میکند. مثلا DbContext در EF Core وقتی با Dependency Injection ساخته شود، Logging اش هم فعال میشود و یا مثلا اگر از متد زیر
services.AddDbContextPool<AppDbContext>();
و یا مثلا برای HttpClient فقط در همین سایت نزدیک به یک دوجین مقاله توضیح دادهاند که چرا new کردن و Dispose کردن HTTP Client مناسب نیست و بهتر است برای Register کردن HttpClient، از services.AddHttpClient استفاده و از IHttpClientFactory و سایر روشها برای Resolve کردن HttpClient استفاده کنیم که اینها نیز توسط Dependency Injection قابل استفاده هستند. از مسائلی مانند Polly و ... نیز میگذریم.
راه حل ایجاد یک Context جدید، تقریبا در تمامی کتابخانههای Dependency Injection دیده شدهاست و آن ساختن یک Child Scope است. در ادامه با Microsoft.Extensions.DependencyInjection یک پیاده سازی آنرا خواهیم داشت؛ ولی مشابه این روش در سایر کتابخانهها همچون Autofac نیز شدنی است.
برای شروع System.IServiceProvider را inject کنید. سپس کد قبلی را به این شکل بنویسید:
(نیاز به ;using Microsoft.Extensions.DependencyInjection در بالای فایل کد است)
public async Task DoSomethingWithCustomer(Customer customer) { using var scope = _serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>(); var httpClient = scope.ServiceProvider.GetRequiredService<IHttpClientFactory>().CreateClient(); // ... }
همچنین میتوانید برای داشتن کدی بهتر، یک interface و class را ایجاد کنید و logic مربوطه را در آن سرویس قرار دهید و در آن سرویس با constructor یا property injection از DbContext و HttpClient و سایر سرویسها استفاده کنید و در نهایت آن interface/class را رجیستر کنید و در DoSomethingWithCustomer به کمک child scope، یک object از آن سرویس بسازید و متدش را فراخوانی کنید. برای مثال اگر هدف ساختن Excel تاریخچه خریدهای مشتری است، داریم:
public interface IOrderHistoryService { Task BuildCustomerHistory(); } public class OrderHistoryService : IOrderHistoryService { private AppDbContext _dbContext; public OrderHistoryService(AppDbContext dbContext) { _dbContext = dbContext; } public async Task BuildCustomerHistory() { // ... } }
کارهای پس زمینه اهمیتی برای IIS ندارن: انجام کارهای پس زمینه در ASP.NET 4.5.2
حداقل کاری که میتونی انجام بدی این هست که خودت یک وظیفهی Ping درست کنی. یک Task درست کن که هر 30 ثانیه یکبار صفحه اول سایت رو واکشی کنه. مثل کاری که RSS Readerها انجام میدن. اینطوری برنامهات همیشه زنده میمونه؛ مگر اینکه یک نفر کل سرور رو ریاستارت کنه. یا یکی از حالتهای ریاستارت برنامههای ASP.NET رخ بده.
یکی از مزایای کار با ORMها، امکان تعویض نوع بانک اطلاعاتی برنامه، بدون نیازی به تغییری در کدهای برنامه است. برای مثال فرض کنید میخواهید با تغییر رشتهی اتصالی برنامه، یکبار از بانک اطلاعاتی SQL Server و بار دیگر از بانک اطلاعاتی کاملا متفاوتی مانند SQLite استفاده کنید. در این مطلب نکات استفادهی از چندین نوع بانک اطلاعاتی متفاوت را در برنامههای مبتنی بر EF Core بررسی خواهیم کرد.
هر بانک اطلاعاتی باید Migration و Context خاص خودش را داشته باشد
تامین کنندهی بانکهای اطلاعاتی مختلف، عموما تنظیمات خاص خودشان را داشته و همچنین دستورات SQL متفاوتی را نیز تولید میکنند. به همین جهت نمیتوان از یک تک Context، هم برای SQLite و هم SQL Server استفاده کرد. به علاوه قصد داریم اطلاعات Migrations هر کدام را نیز در یک اسمبلی جداگانه قرار دهیم. در یک چنین حالتی EF نمیپذیرد که Context تولید کنندهی Migration، در اسمبلی دیگری قرار داشته باشد و باید حتما در همان اسمبلی Migration قرار گیرد. بنابراین ساختار پوشه بندی مثال جاری به صورت زیر خواهد بود:
- در پوشهی EFCoreMultipleDb.DataLayer فقط اینترفیس IUnitOfWork را قرار میدهیم. از این جهت که وقتی قرار شد در برنامه چندین Context تعریف شوند، لایهی سرویس برنامه قرار نیست بداند در حال حاضر با کدام Context کار میکند. به همین جهت است که تغییر بانک اطلاعاتی برنامه، تغییری را در کدهای اصلی آن ایجاد نخواهد کرد.
- در پوشهی EFCoreMultipleDb.DataLayer.SQLite کدهای Context و همچنین IDesignTimeDbContextFactory مخصوص SQLite را قرار میدهیم.
- در پوشهی EFCoreMultipleDb.DataLayer.SQLServer کدهای Context و همچنین IDesignTimeDbContextFactory مخصوص SQL Server را قرار میدهیم.
برای نمونه ابتدای Context مخصوص SQLite چنین شکلی را دارد:
و IDesignTimeDbContextFactory مخصوص آن که برای Migrations از آن استفاده میشود، به صورت زیر تهیه خواهد شد:
هدف از این فایل، ساده سازی کار تولید اطلاعات Migrations برای EF Core است. به این صورت ساخت new SQLiteDbContext توسط ما صورت خواهد گرفت و دیگر EF Core درگیر جزئیات وهله سازی آن نمیشود.
تنظیمات رشتههای اتصالی بانکهای اطلاعاتی مختلف
در اینجا محتویات فایل appsettings.json را که در آن تنظیمات رشتههای اتصالی دو بانک SQL Server LocalDB و همچنین SQLite در آن ذکر شدهاند، مشاهده میکنید:
همین رشتهی اتصالی است که در SQLiteDbContextFactory مورد استفاده قرار میگیرد.
یک کلید InUseKey را هم در اینجا تعریف کردهایم تا مشخص باشد در ابتدای کار برنامه، کلید کدام رشتهی اتصالی مورد استفاده قرار گیرد. برای مثال در اینجا کلید رشتهی اتصالی SQLite تنظیم شدهاست.
در این تنظیمات یک DataDirectory را نیز مشاهده میکنید. مقدار آن در فایل Startup.cs برنامه به صورت زیر بر اساس پوشهی جاری تعیین میشود و در نهایت به wwwroot\app_data اشاره خواهد کرد:
دستورات تولید Migrations و به روز رسانی بانک اطلاعاتی
چون تعداد Contextهای برنامه بیش از یک مورد شدهاست، دستورات متداولی را که تاکنون برای تولید Migrations و یا به روز رسانی ساختار بانک اطلاعاتی اجرا میکردید، با پیام خطایی که این مساله را گوشزد میکند، متوقف خواهند شد. راه حل آن ذکر صریح Context مدنظر است:
برای تولید Migrations، از طریق خط فرمان، به پوشهی اسمبلی مدنظر وارد شده و دستور زیر را اجرا کنید:
در اینجا ذکر startup-project و همچنین context برای پروژههایی که context آنها خارج از startup-project است و همچنین بیش از یک context دارند، ضروریاست. بدیهی است این دستورات را باید یکبار در پوشهی EFCoreMultipleDb.DataLayer.SQLite و یکبار در پوشهی EFCoreMultipleDb.DataLayer.SQLServer اجرا کنید.
دو سطر اول آن، زمان اجرای دستورات را به عنوان نام فایلها تولید میکنند.
پس از تولید Migrations، اکنون نوبت به تولید بانک اطلاعاتی و یا به روز رسانی بانک اطلاعاتی موجود است:
در این مورد نیز ذکر startup-project و همچنین context مدنظر ضروری است.
بدیهی است این رویه را پس از هربار تغییراتی در موجودیتهای برنامه و یا تنظیمات آنها در Contextهای متناظر، نیاز است مجددا اجرا کنید. البته اجرای اولین دستور اجباری است؛ اما میتوان دومین دستور را به صورت زیر نیز اجرا کرد:
متد applyPendingMigrations، کار وهله سازی IUnitOfWork را انجام میدهد. سپس متد Migrate آنرا اجرا میکند، تا تمام Migrations تولید شده، اما اعمال نشدهی به بانک اطلاعاتی، به صورت خودکار به آن اعمال شوند. متد Migrate نیز به صورت زیر تعریف میشود:
مرحلهی آخر: انتخاب بانک اطلاعاتی در برنامهی آغازین
پس از این تنظیمات، قسمتی که کار تعریف IUnitOfWork و همچنین DbContext جاری برنامه را انجام میدهد، به صورت زیر پیاده سازی میشود:
در اینجا ابتدا مقدار InUseKey از فایل تنظیمات برنامه دریافت میشود. بر اساس مقدار آن، رشتهی اتصالی مدنظر دریافت شده و سپس یکی از دو حالت SQLite و یا SQLServer انتخاب میشوند. برای مثال اگر Sqlite انتخاب شده باشد، IUnitOfWork به SQLiteDbContext تنظیم میشود. به این ترتیب لایهی سرویس برنامه که با IUnitOfWork کار میکند، به صورت خودکار وهلهای از SQLiteDbContext را دریافت خواهد کرد.
آزمایش برنامه
ابتدا کدهای کامل این مطلب را از اینجا دریافت کنید: EFCoreMultipleDb.zip
سپس آنرا اجرا نمائید. چنین تصویری را مشاهده خواهید کرد:
اکنون برنامه را بسته و سپس فایل appsettings.json را جهت تغییر مقدار InUseKey به کلید SqlServerConnection ویرایش کنید:
اینبار اگر مجددا برنامه را اجرا کنید، چنین خروجی قابل مشاهدهاست:
مقدار username، در contextهای هر کدام از این بانکهای اطلاعاتی، با مقدار متفاوتی به عنوان اطلاعات اولیهی آن ثبت شدهاست. سرویسی هم که اطلاعات آنرا تامین میکند، به صورت زیر تعریف شدهاست:
همانطور که مشاهده میکنید، با تغییر context برنامه، هیچ نیازی به تغییر کدهای UsersService نیست؛ چون اساسا این سرویس نمیداند که IUnitOfWork چگونه تامین میشود.
هر بانک اطلاعاتی باید Migration و Context خاص خودش را داشته باشد
تامین کنندهی بانکهای اطلاعاتی مختلف، عموما تنظیمات خاص خودشان را داشته و همچنین دستورات SQL متفاوتی را نیز تولید میکنند. به همین جهت نمیتوان از یک تک Context، هم برای SQLite و هم SQL Server استفاده کرد. به علاوه قصد داریم اطلاعات Migrations هر کدام را نیز در یک اسمبلی جداگانه قرار دهیم. در یک چنین حالتی EF نمیپذیرد که Context تولید کنندهی Migration، در اسمبلی دیگری قرار داشته باشد و باید حتما در همان اسمبلی Migration قرار گیرد. بنابراین ساختار پوشه بندی مثال جاری به صورت زیر خواهد بود:
- در پوشهی EFCoreMultipleDb.DataLayer فقط اینترفیس IUnitOfWork را قرار میدهیم. از این جهت که وقتی قرار شد در برنامه چندین Context تعریف شوند، لایهی سرویس برنامه قرار نیست بداند در حال حاضر با کدام Context کار میکند. به همین جهت است که تغییر بانک اطلاعاتی برنامه، تغییری را در کدهای اصلی آن ایجاد نخواهد کرد.
- در پوشهی EFCoreMultipleDb.DataLayer.SQLite کدهای Context و همچنین IDesignTimeDbContextFactory مخصوص SQLite را قرار میدهیم.
- در پوشهی EFCoreMultipleDb.DataLayer.SQLServer کدهای Context و همچنین IDesignTimeDbContextFactory مخصوص SQL Server را قرار میدهیم.
برای نمونه ابتدای Context مخصوص SQLite چنین شکلی را دارد:
public class SQLiteDbContext : DbContext, IUnitOfWork { public SQLiteDbContext(DbContextOptions options) : base(options) { } public virtual DbSet<User> Users { set; get; }
namespace EFCoreMultipleDb.DataLayer.SQLite.Context { public class SQLiteDbContextFactory : IDesignTimeDbContextFactory<SQLiteDbContext> { public SQLiteDbContext CreateDbContext(string[] args) { var basePath = Directory.GetCurrentDirectory(); Console.WriteLine($"Using `{basePath}` as the BasePath"); var configuration = new ConfigurationBuilder() .SetBasePath(basePath) .AddJsonFile("appsettings.json") .Build(); var builder = new DbContextOptionsBuilder<SQLiteDbContext>(); var connectionString = configuration.GetConnectionString("SqliteConnection") .Replace("|DataDirectory|", Path.Combine(basePath, "wwwroot", "app_data")); builder.UseSqlite(connectionString); return new SQLiteDbContext(builder.Options); } } }
تنظیمات رشتههای اتصالی بانکهای اطلاعاتی مختلف
در اینجا محتویات فایل appsettings.json را که در آن تنظیمات رشتههای اتصالی دو بانک SQL Server LocalDB و همچنین SQLite در آن ذکر شدهاند، مشاهده میکنید:
{ "Logging": { "LogLevel": { "Default": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "SqlServerConnection": "Data Source=(LocalDB)\\MSSQLLocalDB;Initial Catalog=ASPNETCoreSqlDB;AttachDbFilename=|DataDirectory|\\ASPNETCoreSqlDB.mdf;Integrated Security=True;MultipleActiveResultSets=True;", "SqliteConnection": "Data Source=|DataDirectory|\\ASPNETCoreSqliteDB.sqlite", "InUseKey": "SqliteConnection" } }
یک کلید InUseKey را هم در اینجا تعریف کردهایم تا مشخص باشد در ابتدای کار برنامه، کلید کدام رشتهی اتصالی مورد استفاده قرار گیرد. برای مثال در اینجا کلید رشتهی اتصالی SQLite تنظیم شدهاست.
در این تنظیمات یک DataDirectory را نیز مشاهده میکنید. مقدار آن در فایل Startup.cs برنامه به صورت زیر بر اساس پوشهی جاری تعیین میشود و در نهایت به wwwroot\app_data اشاره خواهد کرد:
var connectionStringKey = Configuration.GetConnectionString("InUseKey"); var connectionString = Configuration.GetConnectionString(connectionStringKey) .Replace("|DataDirectory|", Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "app_data"));
دستورات تولید Migrations و به روز رسانی بانک اطلاعاتی
چون تعداد Contextهای برنامه بیش از یک مورد شدهاست، دستورات متداولی را که تاکنون برای تولید Migrations و یا به روز رسانی ساختار بانک اطلاعاتی اجرا میکردید، با پیام خطایی که این مساله را گوشزد میکند، متوقف خواهند شد. راه حل آن ذکر صریح Context مدنظر است:
برای تولید Migrations، از طریق خط فرمان، به پوشهی اسمبلی مدنظر وارد شده و دستور زیر را اجرا کنید:
For /f "tokens=2-4 delims=/ " %%a in ('date /t') do (set mydate=%%c_%%a_%%b) For /f "tokens=1-2 delims=/:" %%a in ("%TIME: =0%") do (set mytime=%%a%%b) dotnet build dotnet ef migrations --startup-project ../EFCoreMultipleDb.Web/ add V%mydate%_%mytime% --context SQLiteDbContext
دو سطر اول آن، زمان اجرای دستورات را به عنوان نام فایلها تولید میکنند.
پس از تولید Migrations، اکنون نوبت به تولید بانک اطلاعاتی و یا به روز رسانی بانک اطلاعاتی موجود است:
dotnet build dotnet ef --startup-project ../EFCoreMultipleDb.Web/ database update --context SQLServerDbContext
بدیهی است این رویه را پس از هربار تغییراتی در موجودیتهای برنامه و یا تنظیمات آنها در Contextهای متناظر، نیاز است مجددا اجرا کنید. البته اجرای اولین دستور اجباری است؛ اما میتوان دومین دستور را به صورت زیر نیز اجرا کرد:
namespace EFCoreMultipleDb.Web { public class Startup { public void Configure(IApplicationBuilder app, IHostingEnvironment env) { applyPendingMigrations(app); // ... } private static void applyPendingMigrations(IApplicationBuilder app) { var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>(); using (var scope = scopeFactory.CreateScope()) { var uow = scope.ServiceProvider.GetService<IUnitOfWork>(); uow.Migrate(); } } } }
namespace EFCoreMultipleDb.DataLayer.SQLite.Context { public class SQLiteDbContext : DbContext, IUnitOfWork { // ... public void Migrate() { this.Database.Migrate(); } } }
مرحلهی آخر: انتخاب بانک اطلاعاتی در برنامهی آغازین
پس از این تنظیمات، قسمتی که کار تعریف IUnitOfWork و همچنین DbContext جاری برنامه را انجام میدهد، به صورت زیر پیاده سازی میشود:
namespace EFCoreMultipleDb.Web { public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddScoped<IUsersService, UsersService>(); var connectionStringKey = Configuration.GetConnectionString("InUseKey"); var connectionString = Configuration.GetConnectionString(connectionStringKey) .Replace("|DataDirectory|", Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "app_data")); switch (connectionStringKey) { case "SqlServerConnection": services.AddScoped<IUnitOfWork, SQLServerDbContext>(); services.AddDbContext<SQLServerDbContext>(options => { options.UseSqlServer( connectionString, dbOptions => { var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds; dbOptions.CommandTimeout(minutes); dbOptions.EnableRetryOnFailure(); }); }); break; case "SqliteConnection": services.AddScoped<IUnitOfWork, SQLiteDbContext>(); services.AddDbContext<SQLiteDbContext>(options => { options.UseSqlite( connectionString, dbOptions => { var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds; dbOptions.CommandTimeout(minutes); }); }); break; default: throw new NotImplementedException($"`{connectionStringKey}` is not defined in `appsettings.json` file."); } services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); }
آزمایش برنامه
ابتدا کدهای کامل این مطلب را از اینجا دریافت کنید: EFCoreMultipleDb.zip
سپس آنرا اجرا نمائید. چنین تصویری را مشاهده خواهید کرد:
اکنون برنامه را بسته و سپس فایل appsettings.json را جهت تغییر مقدار InUseKey به کلید SqlServerConnection ویرایش کنید:
{ "ConnectionStrings": { // … "InUseKey": "SqlServerConnection" } }
مقدار username، در contextهای هر کدام از این بانکهای اطلاعاتی، با مقدار متفاوتی به عنوان اطلاعات اولیهی آن ثبت شدهاست. سرویسی هم که اطلاعات آنرا تامین میکند، به صورت زیر تعریف شدهاست:
namespace EFCoreMultipleDb.Services { public interface IUsersService { Task<User> FindUserAsync(int userId); } public class UsersService : IUsersService { private readonly IUnitOfWork _uow; private readonly DbSet<User> _users; public UsersService(IUnitOfWork uow) { _uow = uow; _users = _uow.Set<User>(); } public Task<User> FindUserAsync(int userId) { return _users.FindAsync(userId); } } }
در گزارشات Crosstab، ردیفهای یک گزارش، تبدیل به ستونهای آن میشوند؛ به همین جهت به آنها Pivot tables هم میگویند.
برای مثال فرض کنید که قصد دارید گزارش تعداد ساعت کارکرد را به ازای هر پروژه در طول چند ماه تعیین کنید. گزارش متداول از این نوع اطلاعات، یک لیست بلند بالای بیمفهوم است. این گزارش تشکیل شده از صدها رکورد به ازای کارکنان مختلف در پروژههای مختلف و ... هیچ ارزش آماری خاصی ندارد. یک گزارش بدوی است. زمانیکه این گزارش را تبدیل به حالت crosstab میکنیم، اولین ستون فقط یک شماره پروژه خواهد بود و ستونهای بعدی، مثلا نام ماهها و مقادیر آنها هم جمع کارکرد افراد بر روی یک پروژه مشخص.
مثال اول) تهیه گزارش Crosstab جمع هزینههای واحدهای مختلف به تفکیک ماه
کلاس هزینههای زیر را در نظر بگیرید که به کمک آن میتوان به ازای هر واحد یا دپارتمان در تاریخهای متفاوت، هزینهای را مشخص ساخت:
using System;
namespace Pivot.Sample1
{
public class Expense
{
public DateTime Date { set; get; }
public string Department { set; get; }
public decimal Expenses { set; get; }
}
}
با توجه به این کلاس، یک منبع داده آزمایشی جهت تهیه گزارشات، میتواند به صورت زیر باشد:
using System;
using System.Collections.Generic;
namespace Pivot.Sample1
{
public class ExpenseDataSource
{
public static IList<Expense> ExpensesDataSource()
{
return new List<Expense>
{
new Expense { Date = new DateTime(2011,11,1), Department = "Computer", Expenses = 100 },
new Expense { Date = new DateTime(2011,11,1), Department = "Math", Expenses = 200 },
new Expense { Date = new DateTime(2011,11,1), Department = "Physics", Expenses = 150 },
new Expense { Date = new DateTime(2011,10,1), Department = "Computer", Expenses = 75 },
new Expense { Date = new DateTime(2011,10,1), Department = "Math", Expenses = 150 },
new Expense { Date = new DateTime(2011,10,1), Department = "Physics", Expenses = 130 },
new Expense { Date = new DateTime(2011,9,1), Department = "Computer", Expenses = 90 },
new Expense { Date = new DateTime(2011,9,1), Department = "Math", Expenses = 95 },
new Expense { Date = new DateTime(2011,9,1), Department = "Physics", Expenses = 100 }
};
}
}
}
و اگر این لیست را به همین شکلی که هست نمایش دهیم، خروجی زیر را خواهیم داشت:
که ... خروجی مطلوبی نیست. در اینجا ما فقط 9 رکورد داریم؛ اما در عمل به ازای هر روز، یک رکورد میتواند وجود داشته باشد و این لیست طولانی، هیچ ارزش آماری خاصی ندارد. میخواهیم سرستونهای گزارش ما مطابق جدول زیر باشند:
یعنی اگر سه ماه را در نظر بگیریم با هر تعداد رکورد، فقط سه ردیف به ازای هر ماه باید حاصل شود و ستونهای دیگر هم نام بخشها یا واحدهای موجود باشند.
برای رسیدن به این خروجی Crosstab، میتوان کوئری LINQ زیر را به کمک امکانات گروه بندی اطلاعات آن تهیه کرد:
using System.Collections;
using System.Linq;
namespace Pivot.Sample1
{
public class PivotTable
{
public static IList ExpensesCrossTab()
{
return ExpenseDataSource
.ExpensesDataSource()
.GroupBy(t =>
new
{
Year = t.Date.Year,
Month = t.Date.Month
})
.Select(myGroup =>
new
{
//Year = myGroup.Key.Year,
Month = myGroup.Key.Month,
ComputerDepartment = myGroup.Where(x => x.Department == "Computer").Sum(x => x.Expenses),
MathDepartment = myGroup.Where(x => x.Department == "Math").Sum(x => x.Expenses),
PhysicsDepartment = myGroup.Where(x => x.Department == "Physics").Sum(x => x.Expenses)
})
.ToList();
}
}
}
که اینبار خروجی زیر را تولید میکند.
اگر علاقمند باشید که مثال فوق را در برنامهی LINQPad آزمایش کنید، این فایل را دریافت نموده و در آن برنامه باز نمائید.
مثال دوم) تهیه لیست Crosstab حضور و غیاب افراد در طول یک هفته
کلاس StudentStat را جهت ثبت اطلاعات حضور یک دانشجو، میتوان به شکل زیر تعریف کرد:
using System;
namespace Pivot.Sample2
{
public class StudentStat
{
public int Id { set; get; }
public string Name { set; get; }
public DateTime Date { set; get; }
public bool IsPresent { set; get; }
}
}
و بر همین اساس یک منبع داده فرضی جهت انجام گزارشات میتواند به نحو زیر تهیه شود:
using System;
using System.Collections.Generic;
namespace Pivot.Sample2
{
public class StudentsStatDataSource
{
public static IList<StudentStat> CreateMonthlyReportDataSource()
{
var result = new List<StudentStat>();
var rnd = new Random();
for (int day = 1; day < 6; day++)
{
for (int student = 1; student < 6; student++)
{
result.Add(new StudentStat
{
Id = student,
Date = new DateTime(2011, 11, day),
IsPresent = rnd.Next(-1, 1) == 0 ? true : false,
Name = "student " + student
});
}
}
return result;
}
}
}
خروجی این گزارش هم در این حالت ساده با 5 دانشجو و فقط 5 روز، 25 رکورد خواهد بود:
که ... این هم آنچنان از لحاظ آماری مطلوب و مفهوم نیست. میخواهیم سطرهای این گزارش همانند لیست واقعی حضورغیاب، فقط از نام افراد تشکیل شود و همچنین ستونها مثلا شماره یا نام روزهای یک هفته یا ماه باشند. مثلا به شکل زیر:
برای رسیدن به این خروجی Crosstab، مثلا میتوان از کوئری LINQ زیر کمک گرفت که بر اساس شماره دانشجویی اطلاعات را گروه بندی کرده است:
using System.Collections;
using System.Linq;
namespace Pivot.Sample2
{
public class PivotTable
{
public static IList StudentsStatCrossTab()
{
return StudentsStatDataSource
.CreateWeeklyReportDataSource()
.GroupBy(x =>
new
{
x.Id
})
.Select(myGroup =>
new
{
myGroup.Key.Id,
Name = myGroup.First().Name,
Day1IsPresent = myGroup.Where(x => x.Date.Day == 1).First().IsPresent,
Day2IsPresent = myGroup.Where(x => x.Date.Day == 2).First().IsPresent,
Day3IsPresent = myGroup.Where(x => x.Date.Day == 3).First().IsPresent,
Day4IsPresent = myGroup.Where(x => x.Date.Day == 4).First().IsPresent,
Day5IsPresent = myGroup.Where(x => x.Date.Day == 5).First().IsPresent,
PresentsCount = myGroup.Where(x => x.IsPresent).Count(),
AbsentsCount = myGroup.Where(x => !x.IsPresent).Count()
})
.ToList();
}
}
}
و این کوئری خروجی زیر را تولید میکند که از هر لحاظ نسبت به لیست قبلی مفهومتر است:
فایل LINQPad این مثال را میتوانید از اینجا دریافت کنید.