حجم تقریبی آپدیت از نسخه قبلی (16.11.3) حدود 1.11G میشه.
- Windows 11 SDK support.
- Adds Xcode 13.0 support.
- Add AMD64 math functions to ARM64X CRT.
- Updates to the ARM64 and ARM64EC interfaces between the binary and the POGO instrumentation runtime.
- Fixed several problems with IntelliSense responsiveness and correctness affecting C++20 concepts, ranges, and abbreviated function templates.
- Fixed a false positive in local lifetime checks.
- Corrected an issue where arrays allocated with a constant of size > 32bits could allocate less memory than requested.
- Ensures that ATL string initialization occurs during static variable initialization, in the default AppDomain.
- Fixed a bug in C++ Concurrency::parallel_for_each that was crashing the calling process due to integer overflow.
- Fixed a bug in the STL's iterator debugging machinery that could cause crashes in multithreaded programs using STL containers.
- We have fixed a fatal internal compiler error caused by unnamed structs whose fields are referenced from SAL annotations.
- Fixes a rare crash when analyzing templated code that uses __uuidof.
- Fixed an issue that caused C++ static analysis results to sometimes not display correctly in the FixIt action.
- Fixed opening .uitest extension files in Coded UI project
- Fire component change events for non-component objects also in WinForms .NET designer
- Fix for crash on deleting ContextMenuStrip control in Windows Forms .NET designer.
- Guard against crashes when the Windows Forms designer reloads when dragging.
- Fix for intermittent VS crash while interacting with WinForms .NET designer during solution or project rebuild.
- Fixed a bug causing .NET 5 projects to be reported as out of date when they should have been up to date, causing slower builds.
- Automatically disable asset-indexing for large scale Unity projects.
- This release fixes an issue with deploying certain Windows Application Packaging projects where deployment is unnecessarily copying unmodified files.
همه ما به نحوی در پروژههای خود مجبور به تبدیل انوع داده شده ایم و یک نوع از داده یا Object رو به نوع دیگری از داده یا Object تبدیل کرده ایم. در این پست دو روش دیگر برای تبدیل انواع دادهها بررسی میکنیم. برای شروع دو کلاس زیر رو در نظر بگیرید.
#1کلاس Book
#2کلاس NoteBook
این دو کلاس هیچ ارتباطی با هم ندارند در نتیجه امکان تبدیل این دو نوع وجود ندارد یعنی اجرای هر دو دستور زیر باعث ایجاد خطای کامپایلری میشود.
برای حل این مشکل و تبدیل این دو نوع از Objectها میتونیم از دو نوع ImplicitCasting و Explicit Casting استفاده کنیم.
#Explicit Casting
در Explicit یک Operator به صورت Explicit تعریف میکنیم که ورودی اون از نوع خود کلاس book و خروجی اون از نوع مورد دلخواه است. Converter مورد نظر رو در بدنه این Operator مینویسیم. حالا به راحتی دستور زیر کامپایل میشود.
تنها تفاوت این روش با روش قبلی، در نوع تعریف operator است. بعد از تعریف نوع استفاده به صورت زیر خواهد بود.
در این روش نیاز به ذکر نوع Object برای Cast نیست و Object مورد نظر به راحتی به نوع داده قبل از اپراتور = تبدیل میشود.
#1کلاس Book
public class Book { public int Code { get; set; } public string Title { get; set; } public string Category { get; set; } }
public class NoteBook { public int Code { get; set; } public string Title { get; set; } }
static void Main( string[] args ) { Book book = new Book() { Code = 1, Title = "Book1", Category = "Default" }; NoteBook noteBook = new NoteBook(); noteBook = (NoteBook)book;//Compile error noteBook = book as NoteBook;//Compile error }
#Explicit Casting
public class Book { public int Code { get; set; } public string Title { get; set; } public string Category { get; set; } public static explicit operator NoteBook( Book book ) { return new NoteBook() { Code = book.Code, Title = book.Title }; } }
static void Main( string[] args ) { Book book = new Book() { Code = 1, Title = "Book1", Category = "Default" }; NoteBook noteBook = new NoteBook(); noteBook = (NoteBook)book;//Correct }
در بالا مشاهده میکنید که حتما باید به طور صریح عملیات Cast رو انجام دهیددر غیر این صورت همچنان خطا خواهید داشت. اما میتوان این مراحل رو هم نادیده گرفت و تبدیل رو به صورت Implicit انجام داد.
#Implicit Casting
public class Book { public int Code { get; set; } public string Title { get; set; } public string Category { get; set; } public static implicit operator NoteBook( Book book ) { return new NoteBook() { Code = book.Code, Title = book.Title }; } }
static void Main( string[] args ) { Book book = new Book() { Code = 1, Title = "Book1", Category = "Default" }; NoteBook noteBook = new NoteBook(); noteBook = book;//Correct }
مستندات رسمی
Tuples with C# 7.0
Deconstruction In C# 7.0
C#7: Tuples
C#7 - Taking a second look at Tuples
Tackling Tuples: Understanding the New C# 7 Value Type
Tuples and Generics in C#7
C# Tuples. How tuples are related to ValueTuple
C# Tuples. Why mutable structs
C# 7.0 : Tuples to the Extreme
Tuples with C# 7.0
Deconstruction In C# 7.0
C#7: Tuples
C#7 - Taking a second look at Tuples
Tackling Tuples: Understanding the New C# 7 Value Type
Tuples and Generics in C#7
C# Tuples. How tuples are related to ValueTuple
C# Tuples. Why mutable structs
C# 7.0 : Tuples to the Extreme
پیش از ارائه نهایی مطلب، تمام کدهای آن با VS 2017 RTM آزمایش و بررسی شوند.
سیشارپ نیز مانند بسیاری از زبانهای شیءگرای دیگر، امکان فیلتر کردن استثناءها را بر اساس نوع آنها، دارا است. برای مثال:
در اینجا میتوان بر اساس نوع استثنای مدنظر، چندین catch را نوشت و مدیریت کرد. اما گاهی از اوقات شاید بهتر باشد بجای مدیریت کلی یک نوع از استثناءها، فقط نوعی خاص را صرفا بر اساس شرایطی مشخص، مدیریت کرد. این قابلیت، تحت عنوان Exception Filtering به C# 6 اضافه شدهاست و شکل کلی آن به صورت ذیل است:
در این حالت ابتدا نوع استثناء بررسی میشود و سپس شرطی که در قسمت when ذکر شدهاست. اگر هر دو با هم برقرار بودند، آنگاه این استثنای خاص مدیریت خواهد شد؛ در غیر اینصورت، از مدیریت این نوع استثناء صرفنظر میگردد. این قابلیت، از ابتدای ارائهی CLR وجود داشتهاست، اما C#6 تازه شروع به استفادهی از آن کردهاست (و VB.NET از چند نگارش قبل).
علاوه بر این در اینجا میتوان چندین بدنهی catch مجزا را به ازای یک نوع استثنای مشخص به همراه whenهای متفاوتی نیز تعریف کرد و از این لحاظ محدودیتی وجود ندارد. فقط در این حالت باید به تقدم و تاخرها دقت داشت. برای نمونه در مثال ذیل، ترکیب چندین شرط متفاوت را بر اساس یک نوع مشخص استثناء، مشاهده میکنید. در اینجا اگر برای نمونه شرط ذکر شدهی در قسمت when مربوط به catch اولی صادق باشد، همینجا کار خاتمه مییابد و سایر catchها بررسی نمیشوند:
مورد آخر، حالت catch all را دارد و در صورت شکست دو catch قبلی اجرا میشود. اما باید دقت داشت که اگر این catch all بدون شرط و بدون قسمت when را در ابتدا ذکر کنیم، دیگر کار به بررسی سایر catchهای این نوع استثنای خاص نخواهد رسید:
در مثال فوق هیچگاه دو catch تعریف شدهی پس از catch all اولی اجرا نمیشوند.
لاگ کردن استثناءها در C# 6 بدون مدیریت آنها
به مثال ذیل دقت کنید:
در قسمت when میتوان هر متدی که true یا false را برگرداند، فراخوانی کرد. در این مثال، متدی تعریف شدهاست که false بر میگرداند. یعنی این استثناء کلی از نوع Exception هرچند به ظاهر دارای قسمت when است و مدیریت شدهاست، اما چون خروجی متد Log قسمت when آن مساوی false است، مدیریت نخواهد شد. یعنی در اینجا میتوان بدون مدیریت یک استثناء، اطلاعات کامل آنرا لاگ کرد!
تفاوت C# 6 - Exception Filtering با if/else نوشتن در بدنهی catch چیست؟
تا اینجا به این نتیجه رسیدیم که کدهای if/else دار داخل بدنهی catch کدهای قدیمی را مانند کد ذیل:
میتوان به شکل جدید C# 6 به همراه when نوشت و تبدیل کرد:
اما باید دقت داشت که تفاوت مهم قطعه کد دوم، در مباحث Stack unwinding است. در مثال اولی که if/else داخل بدنهی catch نوشته شدهاست، اطلاعات local محل فراخوانی متدی را که سبب بروز استثناء شدهاست، از دست خواهیم داد؛ اما در مثال دوم خیر.
به این معنا که exception filters سبب Stack unwinding نمیشوند. با هربار ورود به بدنهی catch، اصطلاحا عملیات Stack unwinding صورت میگیرد. یعنی اطلاعات stack مربوط به متدهای پیش از فراخوانی متدی که سبب بروز استثناء شدهاست، از بین میروند. به این ترتیب تشخیص مقادیر متغیرهایی که سبب بروز این استثناء شدهاند نیز میسر نخواهد بود و دیگر نمیتوان با قطعیت عنوان کرد که چه مقادیری و چه اطلاعاتی سبب بروز این مشکل شدهاند. اما در حالت exception filters در قسمت when آن هنوز وارد بدنهی catch نشدهایم. در اینجا دسترسی کاملی به اطلاعات stack جاری و مقادیر متغیرهای محلی که سبب بروز این استثناء شدهاند وجود دارد.
تفاوت stack با stack trace چیست؟ stack قطعهای از حافظهاست که اطلاعاتی در مورد نحوهی فراخوانی متدها، آدرس بازگشتی آنها، آرگومان و همچنین متغیرهای محلی آنها را دارا است. اما stack trace تنها یک رشتهاست و بیانگر نام متدهایی است که هم اکنون بر روی stack قرار دارند. احتمالا پیشتر خوانده بودید که فراخوانی throw داخل بدنهی catch سبب حفظ stack trace میشود و اگر throw ex صورت گیرد، این اطلاعات از دست میروند و بازنویسی میشوند. اما در C# 6 امکان حفظ کل اطلاعات stack به همراه exception filtering میسر شدهاست.
try { // some code to check ... } catch (InvalidOperationException ex) { // do your handling for invalid operation ... } catch (IOException ex) { // do your handling for IO error ... }
catch (SomeException ex) when (someConditionIsMet) { // Your handler logic }
علاوه بر این در اینجا میتوان چندین بدنهی catch مجزا را به ازای یک نوع استثنای مشخص به همراه whenهای متفاوتی نیز تعریف کرد و از این لحاظ محدودیتی وجود ندارد. فقط در این حالت باید به تقدم و تاخرها دقت داشت. برای نمونه در مثال ذیل، ترکیب چندین شرط متفاوت را بر اساس یک نوع مشخص استثناء، مشاهده میکنید. در اینجا اگر برای نمونه شرط ذکر شدهی در قسمت when مربوط به catch اولی صادق باشد، همینجا کار خاتمه مییابد و سایر catchها بررسی نمیشوند:
catch (SomeDependencyException ex) when (condition1 && condition2) { } catch (SomeDependencyException ex) when (condition1) { } catch (SomeDependencyException ex) { }
catch (SomeDependencyException ex) { } catch (SomeDependencyException ex) when (condition1 && condition2) { } catch (SomeDependencyException ex) when (condition1) { }
لاگ کردن استثناءها در C# 6 بدون مدیریت آنها
به مثال ذیل دقت کنید:
try { DoSomethingThatMightFail(s); } catch (Exception ex) when (Log(ex, "An error occurred")) { // this catch block will never be reached } ... static bool Log(Exception ex, string message, params object[] args) { Debug.Print(message, args); return false; }
تفاوت C# 6 - Exception Filtering با if/else نوشتن در بدنهی catch چیست؟
تا اینجا به این نتیجه رسیدیم که کدهای if/else دار داخل بدنهی catch کدهای قدیمی را مانند کد ذیل:
try { var request = WebRequest.Create("http://www.google.coom/"); var response = request.GetResponse(); } catch (WebException we) { if (we.Status == WebExceptionStatus.NameResolutionFailure) { //handle DNS error return; } if (we.Status == WebExceptionStatus.ConnectFailure) { //handle connection error return; } throw; }
try { var request = WebRequest.Create("http://www.google.coom/"); var response = request.GetResponse(); } catch (WebException we) when (we.Status == WebExceptionStatus.NameResolutionFailure) { //Handle NameResolutionFailure Separately } catch (WebException we) when (we.Status == WebExceptionStatus.ConnectFailure) { //Handle ConnectFailure Separately }
به این معنا که exception filters سبب Stack unwinding نمیشوند. با هربار ورود به بدنهی catch، اصطلاحا عملیات Stack unwinding صورت میگیرد. یعنی اطلاعات stack مربوط به متدهای پیش از فراخوانی متدی که سبب بروز استثناء شدهاست، از بین میروند. به این ترتیب تشخیص مقادیر متغیرهایی که سبب بروز این استثناء شدهاند نیز میسر نخواهد بود و دیگر نمیتوان با قطعیت عنوان کرد که چه مقادیری و چه اطلاعاتی سبب بروز این مشکل شدهاند. اما در حالت exception filters در قسمت when آن هنوز وارد بدنهی catch نشدهایم. در اینجا دسترسی کاملی به اطلاعات stack جاری و مقادیر متغیرهای محلی که سبب بروز این استثناء شدهاند وجود دارد.
تفاوت stack با stack trace چیست؟ stack قطعهای از حافظهاست که اطلاعاتی در مورد نحوهی فراخوانی متدها، آدرس بازگشتی آنها، آرگومان و همچنین متغیرهای محلی آنها را دارا است. اما stack trace تنها یک رشتهاست و بیانگر نام متدهایی است که هم اکنون بر روی stack قرار دارند. احتمالا پیشتر خوانده بودید که فراخوانی throw داخل بدنهی catch سبب حفظ stack trace میشود و اگر throw ex صورت گیرد، این اطلاعات از دست میروند و بازنویسی میشوند. اما در C# 6 امکان حفظ کل اطلاعات stack به همراه exception filtering میسر شدهاست.
این دو متد را در نظر بگیرید:
در اولی با استفاده از using، شیء context به صورت خودکار dispose خواهد شد؛ اما در دومی از using استفاده نشدهاست.
سؤال: در یک برنامهی بزرگ چطور میتوان لیست Contextهای Dispose نشده را یافت؟
در EF 6 با تعریف یک IDbConnectionInterceptor سفارشی میتوان به متدهای باز، بسته و dispose شدن یک Connection دسترسی یافت. اگر Context ایی dispose نشده باشد، اتصال آن نیز dispose نخواهد شد.
همانطور که ملاحظه میکنید، با پیاده سازی IDbConnectionInterceptor، به سه متد Closed، Opened و Disposed یک DbConnection میتوان دسترسی یافت.
مشکل مهم! در زمان فراخوانی متد Disposed، دقیقا کدام DbConnection باز شده، رها شدهاست؟
پاسخ به این سؤال را در مطلب «ایجاد خواص الحاقی» میتوانید مطالعه کنید. با استفاده از یک ConditionalWeakTable به هر کدام از اشیاء DbConnection یک Id را انتساب خواهیم داد و پس از آن به سادگی میتوان وضعیت این Id را ردگیری کرد.
برای این منظور، لیستی از ConnectionInfo را تشکیل خواهیم داد:
در اینجا ConnectionId را به کمک ConditionalWeakTable محاسبه میکنیم.
StackTrace توسط نکتهی مطلب «کدام سلسله متدها، متد جاری را فراخوانی کردهاند؟ » تهیه میشود.
Status نیز وضعیت جاری اتصال است که بر اساس متدهای فراخوانی شده در پیاده سازی IDbConnectionInterceptor مشخص میگردد.
در پایان کار برنامه فقط باید یک گزارش تهیه کنیم از لیست ConnectionInfoهایی که Status آنها مساوی Disposed نیست. این موارد با توجه به مشخص بودن Stack trace هر کدام، دقیقا محل متدی را که در آن context مورد استفاده dispose نشدهاست، مشخص میکنند.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
EFNonDisposedContext.zip
private static void disposedContext() { using (var context = new MyContext()) { Debug.WriteLine("Posts count: " + context.BlogPosts.Count()); } } private static void nonDisposedContext() { var context = new MyContext(); Debug.WriteLine("Posts count: " + context.BlogPosts.Count()); }
سؤال: در یک برنامهی بزرگ چطور میتوان لیست Contextهای Dispose نشده را یافت؟
در EF 6 با تعریف یک IDbConnectionInterceptor سفارشی میتوان به متدهای باز، بسته و dispose شدن یک Connection دسترسی یافت. اگر Context ایی dispose نشده باشد، اتصال آن نیز dispose نخواهد شد.
using System.Data; using System.Data.Common; using System.Data.Entity.Infrastructure.Interception; namespace EFNonDisposedContext.Core { public class DatabaseInterceptor : IDbConnectionInterceptor { public void Closed(DbConnection connection, DbConnectionInterceptionContext interceptionContext) { Connections.AddOrUpdate(connection, ConnectionStatus.Closed); } public void Disposed(DbConnection connection, DbConnectionInterceptionContext interceptionContext) { Connections.AddOrUpdate(connection, ConnectionStatus.Disposed); } public void Opened(DbConnection connection, DbConnectionInterceptionContext interceptionContext) { Connections.AddOrUpdate(connection, ConnectionStatus.Opened); } // the rest of the IDbConnectionInterceptor methods ... } }
مشکل مهم! در زمان فراخوانی متد Disposed، دقیقا کدام DbConnection باز شده، رها شدهاست؟
پاسخ به این سؤال را در مطلب «ایجاد خواص الحاقی» میتوانید مطالعه کنید. با استفاده از یک ConditionalWeakTable به هر کدام از اشیاء DbConnection یک Id را انتساب خواهیم داد و پس از آن به سادگی میتوان وضعیت این Id را ردگیری کرد.
برای این منظور، لیستی از ConnectionInfo را تشکیل خواهیم داد:
public enum ConnectionStatus { None, Opened, Closed, Disposed } public class ConnectionInfo { public string ConnectionId { set; get; } public string StackTrace { set; get; } public ConnectionStatus Status { set; get; } public override string ToString() { return string.Format("{0}:{1} [{2}]",ConnectionId, Status, StackTrace); } }
StackTrace توسط نکتهی مطلب «کدام سلسله متدها، متد جاری را فراخوانی کردهاند؟ » تهیه میشود.
Status نیز وضعیت جاری اتصال است که بر اساس متدهای فراخوانی شده در پیاده سازی IDbConnectionInterceptor مشخص میگردد.
در پایان کار برنامه فقط باید یک گزارش تهیه کنیم از لیست ConnectionInfoهایی که Status آنها مساوی Disposed نیست. این موارد با توجه به مشخص بودن Stack trace هر کدام، دقیقا محل متدی را که در آن context مورد استفاده dispose نشدهاست، مشخص میکنند.
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید
EFNonDisposedContext.zip
اشتراکها
گوگل کروم نسخه ۷۰ با امکانات جدید
استفاده از OData تنها به عملیات CRUD معطوف نمیشود و در عمل شما این قابلیت را دارید که متدهای سفارشی و کاملا مجزایی را از همدیگر در سرویسهای خود داشته باشید.
حال نیاز داریم که آن را به EDM اضافه نماییم. در کانفیگ OData، نوع Conventional را استفاده کرده و ابتدا Namespaceی را تعریف کرده و سپس Action خود را تعریف مینماییم؛ به صورت زیر:
در اینجا یک متد اکشن را به نام Rate و پارامتری را از نوع integer به نام Rating تعریف مینماییم.
توجه کنید که نام متد نوشته شده، همنام اکشنی است که قبلا تعریف کردهایم. کار ODataActionParameters گرفتن پارامترهای ارسالی از سمت کلاینت میباشد. دقت کنید که نام پارامتر را نیز تعیین کرده بودیم.
توجه داشته باشید که ProductService همان Nampespaceی است که در کانفیگ تعیین کرده بودیم.
اضافه کردن متد Function
در اینجا یک متد function را به نام MostExpensive، بدون پارامتر و با نوع بازگشتی double، تعریف نمودهایم.
نکته:
و یا نوع بازگشتی، لیستی از EntitySetها باشد:
و یا نوع بازگشتی بطور مثال لیستی از stringها باشد:
برای فراخوانی این متد میتوان از آدرس زیر استفاده نمود:
حال فقط کافیست که متد آن را در کنترلر مربوطه پیاده سازی نماییم:
اگر مقالههای قبلی را دنبال کرده باشید، این قسمت برای شما آشنا خواهد بود.
اضافه کردن Unbound Function
توجه کنید اینجا ما متد را به صورت مستقیم از ODataModelBuilder تهیه نمودهایم؛ به جای Entity type یا collection مربوطه. این تنظیم به model builder میگوید که متدی unbound میباشد.
اهمیتی ندارد که این متد را در چه Controllerی پیاده سازی نمایید. ذکر [ODataRoute] نیز برای تعریف URl این function میباشد.
برای فراخوانی آن نیز از درخواست زیر استفاده مینماییم:
یک مثال از نوع response درخواست فوق
شاید سؤالی برایتان پیش بیاید که آیا برای تعریف هر متد، این همه مراحل کانفیگ لازم است؟! در واقع باید عرض کنم، این نوع استفاده از OData، ابتداییترین نوع طراحی آن میباشد و قطعا در یک برنامهی واقعی این همه کد نویسی برای نوشتن فقط یک متد، شاید منطقی به نظر نمیرسد. از آنجاییکه این مقاله فقط جنبهی آموزشی خیلی ساده از این پروتکل را دارد، فعلا به همین اندازه بسنده میکنیم. اما در مقالههای بعدی راهحلهایی برای بینهایت ساده کردن کانفیگ OData را شرح خواهیم داد.
هرچند در بعضی از سناریوها نیازی به استفادهی بیشتر از CRUD مربوط به آن entity وجود ندارد، اما در اکثر موارد نیاز به رفتاری دارید که به راحتی با استفاده از CRUD معمولی قابلیت پیاده سازی را ندارد. در اینگونه موارد Actionها و Functionها هستند که به راحتی با استفاده از آنها، قابلیت طراحی ماژولهای سفارشی فراهم شده است.
Actions و Functions در عمل رفتاری شبیه به هم را دارند؛ اما تفاوتهای آنها در این است که:
1) Actionها برای درخواستهای از نوع post هستند؛ اما به عکس Functionها از نوع get میباشند.
2) Actionها اثرات جانبی دارند اما Functionها خیر.
3) Functionها حتما باید خروجی داشته باشند؛ اما این الزام برای Actionها وجود ندارد.
4) هر دو امکان داشتن هر تعداد پارامتری را دارند (یا بدون پارامتر).
اضافه کردن Action
فرض کنید مدل زیر را در اختیار داریم
namespace ProductService.Models { public class ProductRating { public int ID { get; set; } public int Rating { get; set; } public int ProductID { get; set; } public virtual Product Product { get; set; } } }
ODataModelBuilder builder = new ODataConventionModelBuilder(); builder.EntitySet<Product>("Products"); builder.Namespace = "ProductService"; builder.EntityType<Product>() .Action("Rate") .Parameter<int>("Rating");
اضافه کردن متد اکشن در کنترلر
[HttpPost] public async Task<IHttpActionResult> Rate([FromODataUri] int key, ODataActionParameters parameters) { if (!ModelState.IsValid) { return BadRequest(); } int rating = (int)parameters["Rating"]; db.Ratings.Add(new ProductRating { ProductID = key, Rating = rating }); await db.SaveChangesAsync(); return StatusCode(HttpStatusCode.NoContent); }
ذکر [HttpPost] برای تعیین کردن post بودن این متد است. برای فراخوانی این اکشن از سمت کلاینت، درخواستی را از نوع post، بدین شکل ارسال مینماییم:
POST http://localhost/Products(1)/ProductService.Rate HTTP/1.1 Content-Type: application/json Content-Length: 12 {"Rating":5}
و البته برای فراخوانی این درخواست توسط یک کلاینت C#ی، اینگونه رفتار میکنیم (در مقالههای آتی از C# Odata client برای فراخوانی درخواستها به صورت strongly typed استفاده خواهیم نمود)
HttpClient client = new HttpClient(); var response = client.PostAsync(postUrl, new StringContent(JsonConvert.SerializeObject(new { Rating = 5 }), Encoding.UTF8, "application/json")).Result;
اضافه کردن متد Function
برای اضافه کردن متد فانکشن نیز ابتدا باید آن را در کانفیگ OData معرفی نماییم؛ به صورت زیر:
ODataModelBuilder builder = new ODataConventionModelBuilder(); builder.EntitySet<Product>("Products"); builder.EntitySet<Supplier>("Suppliers"); builder.Namespace = "ProductService"; builder.EntityType<Product>().Collection .Function("MostExpensive") .Returns<double>();
نکته:
برای اینکه نوع بازگشتی از نوع EntitySet باشد:
ReturnsFromEntitySet<Product>("Products")
ReturnsCollectionFromEntitySet<Product>("Products");
ReturnsCollection<string>();
برای فراخوانی این متد میتوان از آدرس زیر استفاده نمود:
GET http://localhost/Products/ProductService.MostExpensive
حال فقط کافیست که متد آن را در کنترلر مربوطه پیاده سازی نماییم:
public class ProductsController : ODataController { [HttpGet] public IHttpActionResult MostExpensive() { var product = db.Products.Max(x => x.Price); return Ok(product); } // Other controller methods not shown. }
نوع response بازگشتی درخواست فوق چیزی شبیه به این خواهد بود:
HTTP/1.1 200 OK Content-Type: application/json; odata.metadata=minimal; odata.streaming=true OData-Version: 4.0 Date: Sat, 28 Jun 2016 00:44:07 GMT Content-Length: 85 { "@odata.context":"http://localhost:38479/$metadata#Edm.Decimal","value":50.00 }
اضافه کردن Unbound Function
در مثال قبلی یک function bound نوشتیم که مربوط به یک EntitySet خاص بود. اما اکنون میخواهیم یک متد Unbound تعریف نماییم، به صورت زیر:
ODataModelBuilder builder = new ODataConventionModelBuilder(); builder.EntitySet<Product>("Products"); builder.Function("GetSalesTaxRate") .Returns<double>() .Parameter<int>("PostalCode");
متد آن را نیز اینگونه پیاده سازی مینماییم:
[HttpGet] [ODataRoute("GetSalesTaxRate(PostalCode={postalCode})")] public IHttpActionResult GetSalesTaxRate([FromODataUri] int postalCode) { double rate = 5.6; // Use a fake number for the sample. return Ok(rate); }
برای فراخوانی آن نیز از درخواست زیر استفاده مینماییم:
GET http://localhost/GetSalesTaxRate(PostalCode=10) HTTP/1.1
HTTP/1.1 200 OK Content-Type: application/json; odata.metadata=minimal; odata.streaming=true OData-Version: 4.0 Date: Sat, 28 Jun 2016 01:05:32 GMT Content-Length: 82 { "@odata.context":"http://localhost:38479/$metadata#Edm.Double","value":5.6 }
شاید سؤالی برایتان پیش بیاید که آیا برای تعریف هر متد، این همه مراحل کانفیگ لازم است؟! در واقع باید عرض کنم، این نوع استفاده از OData، ابتداییترین نوع طراحی آن میباشد و قطعا در یک برنامهی واقعی این همه کد نویسی برای نوشتن فقط یک متد، شاید منطقی به نظر نمیرسد. از آنجاییکه این مقاله فقط جنبهی آموزشی خیلی ساده از این پروتکل را دارد، فعلا به همین اندازه بسنده میکنیم. اما در مقالههای بعدی راهحلهایی برای بینهایت ساده کردن کانفیگ OData را شرح خواهیم داد.
در مطلب «کنترل نرخ ورود اطلاعات در برنامههای Angular» جزئیات پیاده سازی جستجوی همزمان با تایپ کاربر، بررسی شدند. در اینجا میخواهیم از اطلاعات آن مطلب جهت پیاده سازی یک AutoComplete جستجوی نام کاربران که اطلاعات آن از سرور تامین میشوند، استفاده کنیم:
استفاده از کامپوننت AutoComplete کتابخانهی Angular Material
کتابخانهی Angular Material به همراه یک کامپوننت Auto Complete نیز هست. در اینجا قصد داریم آنرا در یک صفحهی دیالوگ جدید نمایش دهیم و با انتخاب کاربری از لیست توصیههای آن و کلیک بر روی دکمهی نمایش آن کاربر، جزئیات کاربر یافت شده را نمایش دهیم.
به همین جهت ابتدا کامپوننت جدید search-auto-complete را به صورت زیر به مجموعهی کامپوننتهای تعریف شده اضافه میکنیم:
همچنین چون قصد داریم آنرا درون یک popup نمایش دهیم، نیاز است به ماژول contact-manager\contact-manager.module.ts مراجعه کرده و آنرا به لیست entryComponents نیز اضافه کنیم:
در ادامه برای نمایش این کامپوننت به صورت popup، دکمهی جدید جستجو را به toolbar اضافه میکنیم:
برای این منظور به فایل toolbar\toolbar.component.html مراجعه کرده و دکمهی جستجو را پیش از دکمهی نمایش منو، قرار میدهیم:
با این کدها برای مدیریت متد openSearchDialog در فایل toolbar\toolbar.component.ts
در اینجا توسط سرویس MatDialog، کامپوننت SearchAutoCompleteComponent به صورت پویا بارگذاری شده و به صورت یک popup نمایش داده میشود. سپس مشترک رخداد بسته شدن آن شده و بر اساس اطلاعات کاربری که توسط آن بازگشت داده میشود، سبب هدایت صفحهی جاری به صفحهی جزئیات این کاربر یافت شده، خواهیم شد.
کنترلر جستجوی سمت سرور و سرویس سمت کلاینت استفاده کنندهی از آن
در اینجا کنترلر و اکشن متدی را جهت جستجوی قسمتی از نام کاربران را مشاهده میکنید:
کدهای کامل متد SearchUsersAsync در مخزن کد این سری موجود هستند.
از این کنترلر به نحو ذیل در برنامهی Angular برای ارسال اطلاعات و انجام جستجو استفاده میشود:
در اینجا از اپراتور pipe مخصوص RxJS 6x استفاده شدهاست.
تکمیل کامپوننت جستجوی کاربران توسط یک AutoComplete
پس از این مقدمات که شامل تکمیل سرویسهای سمت سرور و کلاینت دریافت اطلاعات کاربران جستجو شده و نمایش صفحهی جستجو به صورت یک popup است، اکنون میخواهیم محتوای این popup را تکمیل کنیم. البته در اینجا فرض بر این است که مطلب «کنترل نرخ ورود اطلاعات در برنامههای Angular» را پیشتر مطالعه کردهاید و با جزئیات آن آشنایی دارید.
تکمیل قالب search-auto-complete.component.html
در این مثال چون کامپوننت search-auto-complete به صورت یک popup ظاهر خواهد شد، ساختار عنوان، محتوا و دکمههای دیالوگ در آن پیاده سازی شدهاند.
سپس نحوهی اتصال یک Input box معمولی را به کامپوننت mat-autocomplete مشاهده میکنید که شامل این موارد است:
- جعبه متنی که قرار است به یک mat-autocomplete متصل شود، توسط دایرکتیو matAutocomplete به template reference variable تعریف شدهی در آن autocomplete اشاره میکند. برای مثال در اینجا این متغیر auto1 است.
- برای انتقال دکمههای فشرده شدهی در input box به کامپوننت، از رخداد input استفاده شدهاست. این روش با هر دو نوع حالت مدیریت فرمهای Angular سازگاری دارد و کدهای آن یکی است.
در کامپوننت mat-autocomplete این تنظیمات صورت گرفتهاند:
- در لیست ظاهر شدهی توسط یک autocomplete، هر نوع ظاهری را میتوان طراحی کرد. برای مثال در اینجا نام و id کاربر نمایش داده میشوند. اما برای تعیین اینکه پس از انتخاب یک آیتم از لیست، چه گزینهای در input box ظاهر شود، از خاصیت displayWith که در اینجا به متد displayFn کامپوننت متصل شدهاست، کمک گرفته خواهد شد.
- از رخداد optionSelected برای دریافت آیتم انتخاب شده، در کدهای کامپوننت استفاده میشود.
- در آخر کار نمایش لیستی از کاربران توسط mat-optionها انجام میشود. در اینجا برای اینکه بتوان تاخیر دریافت اطلاعات از سرور را توسط یک mat-spinner نمایش داد، از خاصیت isLoading تعریف شدهی در کامپوننت استفاده خواهد شد.
تکمیل کامپوننت search-auto-complete.component.ts
کدهای کامل این کامپوننت را در ادامه مشاهده میکنید:
- در ابتدای کار کامپوننت، یک modelChanged از نوع Subject اضافه شدهاست. در این حالت با فراخوانی متد next آن در onSearchChange که به رخداد input جعبهی متنی دریافت اطلاعات متصل است، کار انتقال این تغییرات به اشتراک ایجاد شدهی به آن در ngOnInit انجام میشود. در اینجا بر اساس نکات مطلب «کنترل نرخ ورود اطلاعات در برنامههای Angular»، عبارات وارد شده، به سمت سرور ارسال و در نهایت نتیجهی آن به خاصیت عمومی filteredUsers که به حلقهی نمایش اطلاعات mat-autocomplete متصل است، انتساب داده میشود. در ابتدای اتصال به سرور، خاصیت isLoading به true و در پایان عملیات به false تنظیم خواهد شد تا mat-spinner را نمایش داده و یا مخفی کند.
- توسط متد displayFn، عبارتی که در نهایت پس از انتخاب از لیست نمایش داده شده در input box قرار میگیرد، مشخص خواهد شد.
- در متد onOptionSelected، میتوان به شیء انتخاب شدهی توسط کاربر از لیست mat-autocomplete دسترسی داشت.
- این شیء انتخاب شده را در متد showUser و توسط سرویس MatDialogRef به کامپوننت toolbar که در حال گوش فرادادن به رخداد بسته شدن کامپوننت جاری است، ارسال میکنیم. به این صورت است که کامپوننت toolbar میتواند کار هدایت به جزئیات این کاربر را انجام دهد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای آن:
الف) ابتدا به پوشهی src\MaterialAngularClient وارد شده و فایلهای restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشهی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایلهای restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
استفاده از کامپوننت AutoComplete کتابخانهی Angular Material
کتابخانهی Angular Material به همراه یک کامپوننت Auto Complete نیز هست. در اینجا قصد داریم آنرا در یک صفحهی دیالوگ جدید نمایش دهیم و با انتخاب کاربری از لیست توصیههای آن و کلیک بر روی دکمهی نمایش آن کاربر، جزئیات کاربر یافت شده را نمایش دهیم.
به همین جهت ابتدا کامپوننت جدید search-auto-complete را به صورت زیر به مجموعهی کامپوننتهای تعریف شده اضافه میکنیم:
ng g c contact-manager/components/search-auto-complete --no-spec
import { SearchAutoCompleteComponent } from "./components/search-auto-complete/search-auto-complete.component"; @NgModule({ entryComponents: [ SearchAutoCompleteComponent ] }) export class ContactManagerModule { }
در ادامه برای نمایش این کامپوننت به صورت popup، دکمهی جدید جستجو را به toolbar اضافه میکنیم:
برای این منظور به فایل toolbar\toolbar.component.html مراجعه کرده و دکمهی جستجو را پیش از دکمهی نمایش منو، قرار میدهیم:
<span fxFlex="1 1 auto"></span> <button mat-button (click)="openSearchDialog()"> <mat-icon>search</mat-icon> </button> <button mat-button [matMenuTriggerFor]="menu"> <mat-icon>more_vert</mat-icon> </button>
@Component() export class ToolbarComponent { constructor( private dialog: MatDialog, private router: Router) { } openSearchDialog() { const dialogRef = this.dialog.open(SearchAutoCompleteComponent, { width: "650px" }); dialogRef.afterClosed().subscribe((result: User) => { console.log("The SearchAutoComplete dialog was closed", result); if (result) { this.router.navigate(["/contactmanager", result.id]); } }); } }
کنترلر جستجوی سمت سرور و سرویس سمت کلاینت استفاده کنندهی از آن
در اینجا کنترلر و اکشن متدی را جهت جستجوی قسمتی از نام کاربران را مشاهده میکنید:
namespace MaterialAspNetCoreBackend.WebApp.Controllers { [Route("api/[controller]")] public class TypeaheadController : Controller { private readonly IUsersService _usersService; public TypeaheadController(IUsersService usersService) { _usersService = usersService ?? throw new ArgumentNullException(nameof(usersService)); } [HttpGet("[action]")] public async Task<IActionResult> SearchUsers(string term) { return Ok(await _usersService.SearchUsersAsync(term)); } } }
از این کنترلر به نحو ذیل در برنامهی Angular برای ارسال اطلاعات و انجام جستجو استفاده میشود:
import { HttpClient, HttpErrorResponse } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable, throwError } from "rxjs"; import { catchError, map } from "rxjs/operators"; import { User } from "../models/user"; @Injectable({ providedIn: "root" }) export class UserService { constructor(private http: HttpClient) { } searchUsers(term: string): Observable<User[]> { return this.http .get<User[]>(`/api/Typeahead/SearchUsers?term=${encodeURIComponent(term)}`) .pipe( map(response => response || []), catchError((error: HttpErrorResponse) => throwError(error)) ); } }
تکمیل کامپوننت جستجوی کاربران توسط یک AutoComplete
پس از این مقدمات که شامل تکمیل سرویسهای سمت سرور و کلاینت دریافت اطلاعات کاربران جستجو شده و نمایش صفحهی جستجو به صورت یک popup است، اکنون میخواهیم محتوای این popup را تکمیل کنیم. البته در اینجا فرض بر این است که مطلب «کنترل نرخ ورود اطلاعات در برنامههای Angular» را پیشتر مطالعه کردهاید و با جزئیات آن آشنایی دارید.
تکمیل قالب search-auto-complete.component.html
<h2 mat-dialog-title>Search</h2> <mat-dialog-content> <div fxLayout="column"> <mat-form-field class="example-full-width"> <input matInput placeholder="Choose a user" [matAutocomplete]="auto1" (input)="onSearchChange($event.target.value)"> </mat-form-field> <mat-autocomplete #auto1="matAutocomplete" [displayWith]="displayFn" (optionSelected)="onOptionSelected($event)"> <mat-option *ngIf="isLoading" class="is-loading"> <mat-spinner diameter="50"></mat-spinner> </mat-option> <ng-container *ngIf="!isLoading"> <mat-option *ngFor="let user of filteredUsers" [value]="user"> <span>{{ user.name }}</span> <small> | ID: {{user.id}}</small> </mat-option> </ng-container> </mat-autocomplete> </div> </mat-dialog-content> <mat-dialog-actions> <button mat-button color="primary" (click)="showUser()"> <mat-icon>search</mat-icon> Show User </button> <button mat-button color="primary" [mat-dialog-close]="true"> <mat-icon>cancel</mat-icon> Close </button> </mat-dialog-actions>
سپس نحوهی اتصال یک Input box معمولی را به کامپوننت mat-autocomplete مشاهده میکنید که شامل این موارد است:
- جعبه متنی که قرار است به یک mat-autocomplete متصل شود، توسط دایرکتیو matAutocomplete به template reference variable تعریف شدهی در آن autocomplete اشاره میکند. برای مثال در اینجا این متغیر auto1 است.
- برای انتقال دکمههای فشرده شدهی در input box به کامپوننت، از رخداد input استفاده شدهاست. این روش با هر دو نوع حالت مدیریت فرمهای Angular سازگاری دارد و کدهای آن یکی است.
در کامپوننت mat-autocomplete این تنظیمات صورت گرفتهاند:
- در لیست ظاهر شدهی توسط یک autocomplete، هر نوع ظاهری را میتوان طراحی کرد. برای مثال در اینجا نام و id کاربر نمایش داده میشوند. اما برای تعیین اینکه پس از انتخاب یک آیتم از لیست، چه گزینهای در input box ظاهر شود، از خاصیت displayWith که در اینجا به متد displayFn کامپوننت متصل شدهاست، کمک گرفته خواهد شد.
- از رخداد optionSelected برای دریافت آیتم انتخاب شده، در کدهای کامپوننت استفاده میشود.
- در آخر کار نمایش لیستی از کاربران توسط mat-optionها انجام میشود. در اینجا برای اینکه بتوان تاخیر دریافت اطلاعات از سرور را توسط یک mat-spinner نمایش داد، از خاصیت isLoading تعریف شدهی در کامپوننت استفاده خواهد شد.
تکمیل کامپوننت search-auto-complete.component.ts
کدهای کامل این کامپوننت را در ادامه مشاهده میکنید:
import { Component, OnDestroy, OnInit } from "@angular/core"; import { MatAutocompleteSelectedEvent, MatDialogRef } from "@angular/material"; import { Subject, Subscription } from "rxjs"; import { debounceTime, distinctUntilChanged, finalize, switchMap, tap } from "rxjs/operators"; import { User } from "../../models/user"; import { UserService } from "../../services/user.service"; @Component({ selector: "app-search-auto-complete", templateUrl: "./search-auto-complete.component.html", styleUrls: ["./search-auto-complete.component.css"] }) export class SearchAutoCompleteComponent implements OnInit, OnDestroy { private modelChanged: Subject<string> = new Subject<string>(); private dueTime = 300; private modelChangeSubscription: Subscription; private selectedUser: User = null; filteredUsers: User[] = []; isLoading = false; constructor( private userService: UserService, private dialogRef: MatDialogRef<SearchAutoCompleteComponent>) { } ngOnInit() { this.modelChangeSubscription = this.modelChanged .pipe( debounceTime(this.dueTime), distinctUntilChanged(), tap(() => this.isLoading = true), switchMap(inputValue => this.userService.searchUsers(inputValue).pipe( finalize(() => this.isLoading = false) ) ) ) .subscribe(users => { this.filteredUsers = users; }); } ngOnDestroy() { if (this.modelChangeSubscription) { this.modelChangeSubscription.unsubscribe(); } } onSearchChange(value: string) { this.modelChanged.next(value); } displayFn(user: User) { if (user) { return user.name; } } onOptionSelected(event: MatAutocompleteSelectedEvent) { console.log("Selected user", event.option.value); this.selectedUser = event.option.value as User; } showUser() { if (this.selectedUser) { this.dialogRef.close(this.selectedUser); } } }
- توسط متد displayFn، عبارتی که در نهایت پس از انتخاب از لیست نمایش داده شده در input box قرار میگیرد، مشخص خواهد شد.
- در متد onOptionSelected، میتوان به شیء انتخاب شدهی توسط کاربر از لیست mat-autocomplete دسترسی داشت.
- این شیء انتخاب شده را در متد showUser و توسط سرویس MatDialogRef به کامپوننت toolbar که در حال گوش فرادادن به رخداد بسته شدن کامپوننت جاری است، ارسال میکنیم. به این صورت است که کامپوننت toolbar میتواند کار هدایت به جزئیات این کاربر را انجام دهد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای آن:
الف) ابتدا به پوشهی src\MaterialAngularClient وارد شده و فایلهای restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشهی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایلهای restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
اگر برنامهی شما برای مثال با SMO مربوط به اس کیوال سرور 2008 کامپایل شود، روی سروری با SQL Server 2005 کار نخواهد کرد و پیغام میدهد که نگارش 10 اسمبلی Microsoft.SqlServer.Management.Sdk.Sfc یافت نشد.
یک راه حل آن، نصب Microsoft SQL Server 2008 Management Objects بر روی سرور است، یا راه حل دوم، پیدا کردن اسمبلیهایی که برنامه به آنها ارجاع دارد و کپی کردن آنها کنار فایل اجرایی برنامه در سرور. (درست کردن یک برنامه پرتابل دات نتی، یا نسبتا پرتابل!)
برای این منظور کلاس زیر تهیه شده است که مسیر فایل اجرایی یا dll یک پروژه را دریافت کرده و لیست تمام ارجاعات به آنرا به صورت بازگشتی پیدا میکند. (البته در قسمت یافتن مسیر اسمبلیها، اسمبلیهای سیستمی که با خود دات نت فریم ورک نصب میشوند، حذف شده است)
using System.Collections.Generic;
using System.Reflection;
using System.IO;
namespace App
{
class CFindRef
{
#region Fields (2)
/// <summary>
/// لیستی جهت نگهداری نام اسمبلیها
/// </summary>
private readonly List<string> _assemblies = new List<string>();
/// <summary>
/// لیستی جهت نگهداری مسیر اسمبلیها
/// </summary>
private readonly List<string> _filePath = new List<string>();
#endregion Fields
#region Constructors (1)
/// <summary>
/// سازنده کلاس
/// </summary>
/// <param name="fileName">مسیر اولیه اسمبلی مورد نظر</param>
public CFindRef(string fileName)
{
ListReferences(fileName);
}
#endregion Constructors
#region Properties (2)
/// <summary>
/// لیست مسیر اسمبلیهایی که به آنها ارجاعی وجود دارد منهای موارد سیستمی
/// </summary>
public List<string> ReferencedFiles
{
get
{
_filePath.Sort();
return _filePath;
}
}
/// <summary>
/// لیست کامل اسمبلیهایی که اسمبلی ما به آنها وابسته است
/// </summary>
public List<string> ReferencedNames
{
get
{
_assemblies.Sort();
return _assemblies;
}
}
#endregion Properties
#region Methods (1)
// Private Methods (1)
/// <summary>
/// متدی بازگشتی جهت یافتن لیست ارجاعات
/// </summary>
/// <param name="path">مسیر یا نام اسمبلی</param>
private void ListReferences(string path)
{
//آیا تکراری است؟
if (_assemblies.Contains(path))
return;
Assembly asm;
// آیا فایل است یا نام کامل اسمبلی
if (File.Exists(path))
{
// load the assembly from a path
asm = Assembly.LoadFrom(path);
}
else
{
// سعی در بارگذاری اسمبلی
try
{
asm = Assembly.Load(path);
}
catch
{
asm = null; //جای بهبود دارد
}
}
if (asm == null) return;
// افزودن به لیست نامها
_assemblies.Add(path);
string asmLocation = asm.Location;
//حذف موارد سیستمی از لیست مسیر فایلها
if (asmLocation != null && !asmLocation.Contains("\\System.")
&& !asmLocation.Contains("\\mscorlib"))
_filePath.Add(asmLocation);
// پیدا کردن ارجاعها
AssemblyName[] imports = asm.GetReferencedAssemblies();
// iterate
foreach (AssemblyName asmName in imports)
{
// فراخوانی بازگشتی جهت یافتن تمامی ارجاعات
ListReferences(asmName.FullName);
}
}
#endregion Methods
}
}
مثالی در مورد نحوهی استفاده از آن:
CFindRef cfr = new CFindRef(@"C:\App\test.exe");
foreach (var asmRef in cfr.ReferencedFiles)
{
textBox1.Text += asmRef + Environment.NewLine;
//Application.DoEvents();
}
C:\WINDOWS\assembly\GAC_MSIL\Microsoft.SqlServer.ConnectionInfo\10.0.0.0__89845dcd8080cc91\Microsoft.SqlServer.ConnectionInfo.dll
C:\WINDOWS\assembly\GAC_MSIL\Microsoft.SqlServer.Dmf\10.0.0.0__89845dcd8080cc91\Microsoft.SqlServer.Dmf.dll
C:\WINDOWS\assembly\GAC_MSIL\Microsoft.SqlServer.Management.Sdk.Sfc\10.0.0.0__89845dcd8080cc91\Microsoft.SqlServer.Management.Sdk.Sfc.dll
C:\WINDOWS\assembly\GAC_MSIL\Microsoft.SqlServer.ServiceBrokerEnum\10.0.0.0__89845dcd8080cc91\Microsoft.SqlServer.ServiceBrokerEnum.dll
C:\WINDOWS\assembly\GAC_MSIL\Microsoft.SqlServer.Smo\10.0.0.0__89845dcd8080cc91\Microsoft.SqlServer.Smo.dll
C:\WINDOWS\assembly\GAC_MSIL\Microsoft.SqlServer.SqlClrProvider\10.0.0.0__89845dcd8080cc91\Microsoft.SqlServer.SqlClrProvider.dll
C:\WINDOWS\assembly\GAC_MSIL\Microsoft.SqlServer.SqlEnum\10.0.0.0__89845dcd8080cc91\Microsoft.SqlServer.SqlEnum.dll
برنامه آماده هم در این زمینه موجود است، برای مثال CheckAsm
مشاهده سایت