کار کردن با پروتکل Ping-back آنچنان ساده نیست؛ از این جهت که تبادل ارتباطات آن با پروتکل XML-RPC انجام میشود. XML-RPC نیز توسط PHP کارها بیشتر مورد استفاده قرار میگیرد؛ بجای استفاده از پروتکلهای استاندارد وب سرویسها مانند Soap و امثال آن. پیاده سازیهای ابتدایی Pingback نیز مرتبط است به Wordpress معروف که با PHP تهیه شدهاست. در ادامه نگاهی خواهیم داشت به جزئیات پیاده سازی ارسال ping-back توسط برنامههای ASP.NET.
یافتن آدرس وب سرویس سایت پذیرای Pingback
اولین قدم در پیاده سازی Pingback، یافتن آدرسی است که باید اطلاعات مورد نظر را به آن ارسال کرد. این آدرس عموما به دو طریق ارائه میشود:
الف) در هدری به نام x-pingback و یا pingback
ب) در قسمتی از کدهای HTML صفحه به شکل
<link rel="pingback" href="pingback server">
همانطور که ملاحظه میکنید، نیاز است Response header را آنالیز کنیم.
private Uri findPingbackServiceUri() { var request = (HttpWebRequest)WebRequest.Create(_targetUri); request.UserAgent = UserAgent; request.Timeout = Timeout; request.ReadWriteTimeout = Timeout; request.Method = WebRequestMethods.Http.Get; request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; using (var response = request.GetResponse() as HttpWebResponse) { if (response == null) return null; var url = extractPingbackServiceUriFormHeaders(response); if (url != null) return url; if (!isResponseHtml(response)) return null; using (var reader = new StreamReader(response.GetResponseStream())) { return extractPingbackServiceUriFormPage(reader.ReadToEnd()); } } } private static Uri extractPingbackServiceUriFormHeaders(WebResponse response) { var pingUrl = response.Headers.AllKeys.FirstOrDefault(header => header.Equals("x-pingback", StringComparison.OrdinalIgnoreCase) || header.Equals("pingback", StringComparison.OrdinalIgnoreCase)); return getValidAbsoluteUri(pingUrl); } private static Uri extractPingbackServiceUriFormPage(string content) { if (string.IsNullOrWhiteSpace(content)) return null; var regex = new Regex(@"(?s)<link\srel=""pingback""\shref=""(.+?)""", RegexOptions.IgnoreCase); var match = regex.Match(content); return (!match.Success || match.Groups.Count < 2) ? null : getValidAbsoluteUri(match.Groups[1].Value); } private static Uri getValidAbsoluteUri(string url) { Uri absoluteUri; return string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out absoluteUri) ? null : absoluteUri; } private static bool isResponseHtml(WebResponse response) { var contentTypeKey = response.Headers.AllKeys.FirstOrDefault(header => header.Equals("content-type", StringComparison.OrdinalIgnoreCase)); return !string.IsNullOrWhiteSpace(contentTypeKey) && response.Headers[contentTypeKey].StartsWith("text/html", StringComparison.OrdinalIgnoreCase); }
targetUri، آدرسی است از یک سایت دیگر که در سایت ما درج شدهاست. زمانیکه این صفحه را درخواست میکنیم، response.Headers.AllKeys حاصل میتواند حاوی کلید x-pingback باشد یا خیر. اگر بلی، همینجا کار پایان مییابد. فقط باید مطمئن شد که این آدرس مطلق است و نه نسبی. به همین جهت در متد getValidAbsoluteUri، بررسی بر روی UriKind.Absolute انجام شدهاست.
اگر هدر فاقد کلید x-pingback باشد، قسمت ب را باید بررسی کرد. یعنی نیاز است محتوای Html صفحه را برای یافتن link rel=pingback بررسی کنیم. همچنین باید دقت داشت که پیش از اینکار نیاز است حتما بررسی isResponseHtml صورت گیرد. برای مثال در سایت شما لینکی به یک فایل 2 گیگابایتی SQL Server درج شدهاست. در این حالت نباید ابتدا 2 گیگابایت فایل دریافت شود و سپس بررسی کنیم که آیا محتوای آن حاوی link rel=pingback است یا خیر. اگر محتوای ارسالی از نوع text/html بود، آنگاه کار دریافت محتوای لینک انجام خواهد شد.
ارسال Ping به آدرس سرویس Pingback
اکنون که آدرس سرویس pingback یک سایت را یافتهایم، کافی است ping ایی را به آن ارسال کنیم:
public void Send() { var pingUrl = findPingbackServiceUri(); if (pingUrl == null) throw new NotSupportedException(string.Format("{0} doesn't support pingback.", _targetUri.Host)); sendPing(pingUrl); } private void sendPing(Uri pingUrl) { var request = (HttpWebRequest)WebRequest.Create(pingUrl); request.UserAgent = UserAgent; request.Timeout = Timeout; request.ReadWriteTimeout = Timeout; request.Method = WebRequestMethods.Http.Post; request.ContentType = "text/xml"; request.ProtocolVersion = HttpVersion.Version11; makeXmlRpcRequest(request); using (var response = (HttpWebResponse)request.GetResponse()) { response.Close(); } } private void makeXmlRpcRequest(WebRequest request) { var stream = request.GetRequestStream(); using (var writer = new XmlTextWriter(stream, Encoding.ASCII)) { writer.WriteStartDocument(true); writer.WriteStartElement("methodCall"); writer.WriteElementString("methodName", "pingback.ping"); writer.WriteStartElement("params"); writer.WriteStartElement("param"); writer.WriteStartElement("value"); writer.WriteElementString("string", Uri.EscapeUriString(_sourceUri.ToString())); writer.WriteEndElement(); writer.WriteEndElement(); writer.WriteStartElement("param"); writer.WriteStartElement("value"); writer.WriteElementString("string", Uri.EscapeUriString(_targetUri.ToString())); writer.WriteEndElement(); writer.WriteEndElement(); writer.WriteEndElement(); writer.WriteEndElement(); } }
در اینجا sourceUri آدرس صفحهای در سایت ما است که targetUri ایی (آدرسی از سایت دیگر) در آن درج شدهاست. در یک pinback، صرفا این دو آدرس به سرویس دریافت کنندهی pingback ارسال میشوند.
سپس سایت دریافت کنندهی ping، ابتدا sourceUri را دریافت میکند تا عنوان آنرا استخراج کند و همچنین بررسی میکند که آیا targetUri، در آن درج شدهاست یا خیر (آیا spam است یا خیر)؟
تا اینجا اگر این مراحل را کنار هم قرار دهیم به کلاس Pingback ذیل خواهیم رسید:
Pingback.cs
نحوهی استفاده از کلاس Pingback تهیه شده
کار ارسال Pingback عموما به این نحو است: هر زمانیکه مطلبی یا یکی از نظرات آن، ثبت یا ویرایش میشوند، نیاز است Pingbackهای آن ارسال شوند. بنابراین تنها کاری که باید انجام شود، استخراج لینکهای خارجی یک صفحه و سپس فراخوانی متد Send کلاس فوق است.
یافتن لینکهای یک محتوا را نیز میتوان مانند متد extractPingbackServiceUriFormPage فوق، توسط یک Regex انجام داد و یا حتی با استفاده از کتابخانهی معروف HTML Agility Pack:
var doc = new HtmlWeb().Load(url); var linkTags = doc.DocumentNode.Descendants("link"); var linkedPages = doc.DocumentNode.Descendants("a") .Select(a => a.GetAttributeValue("href", null)) .Where(u => !String.IsNullOrEmpty(u));
ASP.NET MVC #22
برای شروع ابتدا مدل برنامه رو به صورت زیر تعریف کنید.
public class Category { public int Id { get; set; } public string Title { get; set; } }
public class CategoryMap : EntityTypeConfiguration<Entity.Category> { public CategoryMap() { ToTable( "Category" ); HasKey( _field => _field.Id ); Property( _field => _field.Title ) .IsRequired(); } }
using System.Data.Entity; using System.Data.Entity.Infrastructure; namespace DataAccess { public interface IUnitOfWork { DbSet<TEntity> Set<TEntity>() where TEntity : class; DbEntityEntry<TEntity> Entry<TEntity>() where TEntity : class; void SaveChanges(); void Dispose(); } }
چون کلاس DatabaseContext از اینترفیس IUnitOfWork ارث برده است برای همین از InheritedExport استفاده میکنیم.
[InheritedExport( typeof( IUnitOfWork ) )] public class DatabaseContext : DbContext, IUnitOfWork { private DbTransaction transaction = null; public DatabaseContext() { this.Configuration.AutoDetectChangesEnabled = false; this.Configuration.LazyLoadingEnabled = true; } protected override void OnModelCreating( DbModelBuilder modelBuilder ) { modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); modelBuilder.AddFormAssembly( Assembly.GetAssembly( typeof( Entity.Map.CategoryMap ) ) ); } public DbEntityEntry<TEntity> Entry<TEntity>() where TEntity : class { return this.Entry<TEntity>(); } }
public static class ModelBuilderExtension { public static void AddFormAssembly( this DbModelBuilder modelBuilder, Assembly assembly ) { Array.ForEach<Type>( assembly.GetTypes().Where( type => type.BaseType != null && type.BaseType.IsGenericType && type.BaseType.GetGenericTypeDefinition() == typeof( EntityTypeConfiguration<> ) ).ToArray(), delegate( Type type ) { dynamic instance = Activator.CreateInstance( type ); modelBuilder.Configurations.Add( instance ); } ); } }
برای پیاده سازی قسمت BusinessLogic ابتدا کلاس BusiessBase را در آن قرار دهید:
public class BusinessBase<TEntity> where TEntity : class { public BusinessBase( IUnitOfWork unitOfWork ) { this.UnitOfWork = unitOfWork; } [Import] public IUnitOfWork UnitOfWork { get; private set; } public virtual IEnumerable<TEntity> GetAll() { return UnitOfWork.Set<TEntity>().AsNoTracking(); } public virtual void Add( TEntity entity ) { try { UnitOfWork.Set<TEntity>().Add( entity ); UnitOfWork.SaveChanges(); } catch { throw; } finally { UnitOfWork.Dispose(); } } }
تمام متدهای پایه مورد نظر را باید در این کلاس قرار داد که برای مثال من متد Add , GetAll را براتون پیاده سازی کردم. UnitOfWork توسط ImportAttribute مقدار دهی میشود و نیاز به وهله سازی از آن نیست
کلاس Category رو هم باید به صورت زیر اضافه کنید.
public class Category : BusinessBase<Entity.Category> { [ImportingConstructor] public Category( [Import( typeof( IUnitOfWork ) )] IUnitOfWork unitOfWork ) : base( unitOfWork ) { } }
public class Plugin { public void Run() { AggregateCatalog catalog = new AggregateCatalog(); Container = new CompositionContainer( catalog ); CompositionBatch batch = new CompositionBatch(); catalog.Catalogs.Add( new AssemblyCatalog( Assembly.GetExecutingAssembly() ) ); batch.AddPart( this ); Container.Compose( batch ); } public CompositionContainer Container { get; private set; } }
- AssemblyCatalog : در اسمبلی مورد نظر به دنبال تمام Export Attributeها میگردد و آنها را به عنوان ExportedValue در Container اضافه میکند.
- TypeCatalog: فقط یک نوع مشخص را به عنوان ExportAttribute در نظر میگیرد.
- DirectoryCatalog : در یک مسیر مشخص تمام Assembly مورد نظر را از نظر Export Attribute جستجو میکند و آنها را به عنوان ExportedValue در Container اضافه میکند.
- ApplicationCatalog : در اسمبلی و فایلهای (EXE) مورد نظر به دنبال تمام Export Attributeها میگردد و آنها را به عنوان ExportedValue در Container اضافه میکند.
- AggregateCatalog : تمام موارد فوق را Support میکند.
class Program { static void Main( string[] args ) { Plugin plugin = new Plugin(); plugin.Run(); Category category = new Category(plugin.Container.GetExportedValue<IUnitOfWork>()); category.GetAll().ToList().ForEach( _record => Console.Write( _record.Title ) ); } }
6- کل مدیریت فایل در فایل index.html میباشد و اگر تمایل به تغییر عنوان، درج لوگوی خودتان و ... را دارید به این فایل مراجعه کنید.
خوب، اکنون فایل Tinymce.html را اجرا کنید و نتیجه را ملاحظه بفرمایید.
برای انتقال دادهها از طریق WCF بین سیستمهای مختلف باید دادههای مورد نظر حتما سریالایز شوند که مثال هایی از این دست رو در همین سایت میتونید مطالعه کنید:
(^ ) و (^ ) و (^ )
با توجه به این که دادهها سریالایز میشوند، در نتیجه امکان انقال داده هایی که از نوع object هستند در WCF وجود ندارد. بلکه نوع داده باید صراحتا ذکر شود و این نوع باید قابیلت سریالایز شدن را دارا باشد.برای مثال شما نمیتونید متدی داشته باشید که پارامتر ورودی آن از نوع delegate باشد یا کلاسی باشد که صفت [Serializable] در بالای اون قرار نداشته باشد یا کلاسی باشد که صفت DataContract برای خود کلاس و صفت DataMember برای خاصیتهای اون تعریف نشده باشد. حالا سوال مهم این است اگر متدی داشته باشیم که پارامتر ورودی آن حتما باید از نوع delegate باشد چه باید کرد؟
برای تشریح بهتر مسئله یک مثال میزنم؟
سرویسی داریم برای اطلاعات کتاب ها. قصد داریم متدی بنوسیم که پارامتر
ورودی آن از نوع Lambda Expression است تا Query مورد نظر کاربر از سمت
کلاینت به سمت سرور دریافت کند و خروجی مورد نظر را با توجه به Query ورودی
به کلاینت برگشت دهد.( متدی متداول در اکثر پروژه ها). به صورت زیر عمل میکنیم.
*ابتدا یک Blank Solution ایجاد کنید.
*یک ClassLibrary به نام Model ایجاد کنید و کلاسی به نام Book در آن بسازید .(همانطور که میبینید کلاس مورد نظر سریالایز شده است):
[DataContract] public class Book { [DataMember] public int Code { get; set; } [DataMember] public string Title { get; set; } }
یک Contract برای ارتباط بین سرور و کلاینت میسازیم:
using System; using System.Collections.Generic; using System.Linq.Expressions; using System.ServiceModel; namespace WcfLambdaExpression { [ServiceContract] public interface IBookService { [OperationContract] IEnumerable<Book> GetByExpression( Expression<Func<Book, bool>> expression ); } }
using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; namespace WcfLambdaExpression { public class BookService : IBookService { public BookService() { ListOfBook = new List<Book>(); } public List<Book> ListOfBook { get; private set; } public IEnumerable<Book> GetByExpression( Expression<Func<Book, bool>> expression ) { ListOfBook.AddRange( new Book[] { new Book(){Code = 1 , Title = "Book1"}, new Book(){Code = 2 , Title = "Book2"}, new Book(){Code = 3 , Title = "Book3"}, new Book(){Code = 4 , Title = "Book4"}, new Book(){Code = 5 , Title = "Book5"}, } ); return ListOfBook.AsQueryable().Where( expression ); } } }
به طور حتم با خطا روبرو خواهید شد. دلیل آن هم این است که امکان سریالایز کردن برای پارامتر ورودی expression میسر نیست.
خطای مربوطه به شکل زیر خواهد بود:
Type 'System.Linq.Expressions.Expression`1[System.Func`2[WcfLambdaExpression.Book,System.Boolean]]' cannot be serialized. Consider marking it with the DataContractAttribute attribute, and marking all of its members you want serialized with the DataMemberAttribute attribute. If the type is a collection, consider marking it with the CollectionDataContractAttribute. See the Microsoft .NET Framework documentation for other supported types
روشهای زیادی برای بر طرف کردن این محدودیت وجود دارد. اما در این پست روشی رو که خودم از اون استفاده میکنم رو براتون شرح میدهم.
در این روش باید از XElement استفاده شود که در فضای نام System.Linq.Xml قرار دارد. یعنی آرگومان ورودی سمت کلاینت باید به فرمت Xml سریالایز شود و سمت سرور دوباره دی سریالایز شده و تبدیل به یک Lambda Expression شود. اما سریالایز کردن Lambda Expression واقعا کاری سخت و طاقت فرساست . با توجه به این که در اکثر پروژهها این متدها به صورت Generic نوشته میشوند. برای حل این مسئله بعد از مدتی جستجو، کلاسی رو پیدا کردم که این کار رو برام انجام میداد. بعد از مطالعه دقیق و مشاهده روش کار کلاس، تغییرات مورد نظرم رو اعمال کردم و الان در اکثر پروژه هام دارم از این کلاس استفاده میکنم.
یک مثال از روش استفاده :
برای اینکه از این کلاس در هر دو پروژه (سرور و کلاینت) استفاده میکنیم باید یک Class Library جدید به نام Common بسازید و یک ارجاع از اون رو به هر دو پروژه سمت سرور و کلاینت بدید.
سرویس و Contract بالا رو به صورت زیر باز نویسی کنید.
[ServiceContract] public interface IBookService { [OperationContract] IEnumerable<Book> GetByExpression( XElement expression ); }
using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Xml.Linq; namespace WcfLambdaExpression { public class BookService : IBookService { public BookService() { ListOfBook = new List<Book>(); } public List<Book> ListOfBook { get; private set; } public IEnumerable<Book> GetByExpression( XElement expression ) { ListOfBook.AddRange( new Book[] { new Book(){Code = 1 , Title = "Book1"}, new Book(){Code = 2 , Title = "Book2"}, new Book(){Code = 3 , Title = "Book3"}, new Book(){Code = 4 , Title = "Book4"}, new Book(){Code = 5 , Title = "Book5"}, } ); Common.ExpressionSerializer serializer = new Common.ExpressionSerializer(); return ListOfBook.AsQueryable().Where( serializer.Deserialize( expression ) as Expression<Func<Book, bool>> ); } }
using System; using System.Linq.Expressions; using TestExpression.MyBookService; namespace TestExpression { class Program { static void Main( string[] args ) { BookServiceClient bookService = new BookServiceClient(); Expression<Func<Book, bool>> expression = x => x.Code > 2 && x.Code < 5; Common.ExpressionSerializer serializer = new Common.ExpressionSerializer(); bookService.GetByExpression( serializer.Serialize( expression ) ); } } }
خروجی هم به صورت زیر خواهد بود:
دریافت سورس کامل Expression-Serializationبه روز رسانی فایل OPML وبلاگهای IT ایرانی؛ اردیبهشت 93
اگر فکر میکنید لینکی در آن نباید باشد و یا باید اضافه شود، آنرا به صورت یک pull request جدید ارسال کنید.
همیشه با نزدیک شدن آخر سال، به روز کردن مناسبتهای تقویم سال بعد ضروری میشود. دوستان لینوکسی ما هم در این مورد زحمت کشیده و برنامهای را تهیه کردهاند که از آدرس زیر قابل دریافت است:
http://download.gna.org/jalali-calendar/
پس از دریافت برنامه، مناسبتهای سال 1388 در فایل 1388.xml قابل مشاهده است (با تقدیر و تشکر از زحمات این عزیزان). فرض کنید میخواهیم این اطلاعات را به اس کیوال سرور منتقل کنیم.
فرمت این فایل به شکل زیر است:
<?xml version="1.0" encoding="UTF-8"?>
<cal1388>
<day>
<num>1/1</num>
<desc>عید نوروز، لحظه تحویل سال:ساعت 15 و 13 دقیقه و 39 ثانیه </desc>
</day>
</cal1388>
حداقل سه راه حل برای انجام اینکار (خواندن فایل xml توسط اس کیوال سرور) موجود است:
راه اول:
SELECT '1388' saal,
X.day.query('num').value('.', 'nVARCHAR(50)') rooz_maah,
X.day.query('desc').value('.', 'nVARCHAR(max)') tozih
FROM (
SELECT CAST(x AS XML)
FROM OPENROWSET(
BULK
'c:\1388.xml',
SINGLE_BLOB
) AS T(x)
) AS T(x)
CROSS APPLY x.nodes('cal1388/day') AS X(day);
راه دوم:
DECLARE @MyXML XML
SELECT @MyXML = CAST(x AS XML)
FROM OPENROWSET(
BULK
'c:\1388.xml',
SINGLE_BLOB
) AS T(x)
SELECT 1388 saal,
nref.value('num[1]', 'nvarchar(50)') rooz_maah,
nref.value('desc[1]', 'nvarchar(max)') tozih
FROM @MyXML.nodes('//day') AS R(nref)
راه سوم:
DECLARE @MyXML XML
DECLARE @handle INT
SELECT @MyXML = CAST(x AS XML)
FROM OPENROWSET(
BULK
'c:\1388.xml',
SINGLE_BLOB
) AS T(x)
EXEC sp_xml_preparedocument @handle OUTPUT,
@MyXML
SELECT 1388 saal,
*
FROM OPENXML(@handle, '/cal1388/day', 2) WITH
(num VARCHAR(20), [desc] NVARCHAR(MAX))
EXEC sp_xml_removedocument @handle
اکنون که میتوانیم اطلاعات این فایل را select کنیم، Insert آن ساده است . برای مثال:
INSERT INTO tblMonasebat
(
saal,
rooz_mah,
tozih
)
SELECT '1388' saal,
nref.value('num[1]', 'nvarchar(50)') rooz_maah,
nref.value('desc[1]', 'nvarchar(max)') tozih
FROM @MyXML.nodes('//day') AS R(nref)
بدیهی است امکان مقدار دهی @MyXML به صورت یک رشته که حاوی محتویات فایل است نیز مهیا میباشد (بجای خواندن از فایل).
برای مطالعهی بیشتر
XML Support in Microsoft SQL Server 2005
Beginning SQL Server 2005 XML Programming