Best practices: Daylight saving time and Timezone

שיטות עבודה מומלצות עם DateTime אזורי זמן ושעון הקיץ

SpirallingTimeמנקודת מבטו של מתכנת – המסע בזמן בהחלט אפשרי, במיוחד אם לא נזהרים כראוי. זוכרים את באג 2000? מוכנים לבאג 2038? או שאולי, בעצם אלו דוגמאות קיצוניות מדי ולא נוגעות לנו המשתמשים בטכנולוגיות מודרניות?

הנה לכם הוכחה קצרצרה שטיפול לא נכון בנתוני DateTime ב.NET יכול לגרום לכשלים קריטיים דומים. נתבונן בקוד הבא:

בשביל ההמחשה נניח שאני צריך לכתוב תוכנה שמחשבת אורך שיחה של לקוחות ספק תקשורת סלולרית. כשלקוח מחייג, אני רושם במשתנה start אז זמן תחילת השיחה (02/10/2011 00:00:00), וכשהוא מנתק, מציב בfinish את זמן סיום השיחה (02/10/2011 02:00:00). בסיום השיחה התוכנה מחשבת את אורך השיחה ומחזירה את מספר השעות שארכה השיחה לצורך חיוב הלקוח. במקרה שלפנינו, בהתחשב בזמני תחילת וסיום השיחה, התוכנה פולטת שהלקוח שלי דיבר במשך שעתיים.

ברוב ימות השנה זו אפילו תהיה התוצאה הנכונה. הבעיה היא, שבדיוק באותו היום ישראל עברה משעון קיץ לשעון חורף. כלומר, בזמן שהלקוח שלי היה באמצע השיחה, החזירו את השעון הישראלי מהשעה 01:59:59 לשעה 01:00:000. נמצא, שכשהתוכנה שלי רשמה את שעת סיום השיחה ב2 לפנות בוקר לפי שעון חורף (שהרגע הוחל) זה בעצם היה ב3 לפנ"ב לפי שעון הקיץ. והלקוח שלי שהתחיל את השיחה לפי שעון הקיץ בעצם דיבר 3 שעות ולא 2!

למרות שמעבר השעון מתרחש בשעות הלילה הפחות פעילות, בעולם שכולו OnLine הסיכוי הוא שגם בשעות האלו התוכנה\אתר\שירות שלכם יהיו פעילים. ואם הם כתובים בצורה לא נכונה – הם יבצעו חישובים ופעולות שגויות!

כלל הזהב: לשמור ולבצע חישובים רק על זמן UTC

שימוש בזמן אוניברסלי פותר את רוב הבעיות הקיימות בזמן מקומי. בשעון אוניברסלי לכל יממה יש 24 שעות, ולעולם לא עוברים לשעון קיץ. לשם כך בDateTime יש מאפיין סטטי בשם UtcNow המחזיר את השעה הנוכחית לפי השעון האוניברסלי – UTC.

היות וליתר בהירות בדוגמה שלפנינו השתמשתי בתאריך "מוקשח" ולא בDateTime.Now, אני ממיר את הערכים שנוצרו כברירת מחדל בשעון הנוכחי (אצלי – שעון ישראל) לשעון UTC ע"י הפונקציה ToUniversalTime. חשוב לזכור: הפונקציה לא משנה את הערך של הDateTime שעליו היא הופעלה, אלא מחזירה מופע חדש של DateTime עם הערך החדש.

עכשיו, נראה את אותה התוכנה בשינוי קל:

כעת שהחישוב מתבצע על שעון אוניברסלי, התוכנה מחזירה את התוצאה הרצויה: 3 שעות!

טיפול בקלט ממשתמשים

כבר אמרנו, שאם נשמור נתונים ונבצע חישובים רק בUTC נהיה בטוחים. אבל מה עושים עם קלט שהמשתמש מקליד לפי השעון המקומי שלו? הרי זה לא פרקטי להכריח את המשתמשים להקליד את כל הזמנים בUTC.

במקרים כאלה, יש שני תסריטים הדורשים טיפול מיוחד:

זמן לדוגמה בעיה כלים לטיפול

01/04/2011 02:00:00

זמן לא קיים: בתאריך הנ"ל עברנו משעון חורף לשעון קיץ. כלומר, משעה 01:59:59 לשעה 03:00:00. לא הייתה שעה 22 באותו היום! IsInvalidTime

02/10/2011 01:00:00

זמן שקיים פעמיים: בתאריך הנ"ל חזרנו משעון קיץ לשעון חורף. כלומר, משעה 01:59:59 חזרה לשעה 01:00:00. באותו יום שעה 11 הייתה פעמיים! IsAmbiguousTime
GetAmbiguousTimeOffsets

לצורך הדגמה, נניח שאני כותב תוכנה לחדר יולדות, בה הרופא יזין את תאריך הלידה ואת שעת הלידה של כל התינוקות במחלקה. למען הפשטות, נניח שאנחנו יודעים שבתוכנה הזו ישתמשו רק בבי"ח בישראל ולכן כל הזמנים יוזנו לפי שעון ישראל.

עכשיו נניח שרופא מזין בתוכנה את התאריך הבא: "02/10/2011 01:00:00". נזכיר שוב, שבתאריך הזה ישראל עברה לשעון חורף ע"י חזרה אחורה מהשעה 01:59:59 לשעה 01:00:00. כלומר, מלבד שבאותה יממה יש 25 שעות – בנוסף, גם השעה 1 מתרחשת פעמיים; פעם ראשונה בשעה 1 לפי שעון קיץ ובפעם השנייה בשעה 2 לפי השעון הישן.

אם כן, איך ניתן לדעת האם הרופא התכוון לשעה 1 לפי שעון קיץ או לפי שעון חורף? ובכן, בלי מידע נוסף – זה בלתי אפשרי.

כדי לטפל במקרים כאלה, צריך שהתוכנה שלנו תדע לזהות שהזמן שהוזן הוא בעייתי, ולשאול את המשתמש לאיזה שעה 1 הוא מתכוון. לצורך זיהוי הזמנים הבעייתיים נוספה ב.NET Framework 3.5 (ומעלה) המחלקה TimeZoneInfo, החושפת את המתודה IsAmbiguousTime שמחזירה האם הזמן שמעבירים לה כפרמטר יכול להתפרש לשני אופנים, כמו בדוגמה שלנו. וכן המתודה GetAmbiguousTimeOffsets המחזירה את שני האופנים בהם ניתן לתרגם את התאריך לUTC.

כמו"כ, במתודה TryParseAndValidateDate אני בודק בעזרת המתודה IsInvalidTime, האם הזמן שהוזן אכן קיים, ואם לא – מדווח את השגיאה למשתמש.

אותה שגיאה לוגית עלולה להתקבל גם בקוד הבא:

שכן אם הערך של DateTime.Now (שהוא לפי שעון מקומי) יהיה אחד הזמנים הבעייתיים – המתודה ToUniversalTime לא תדע להמיר אותו לזמן לUTC בצורה נכונה. לכן הצורה הנכונה לכתוב את זה היא:

באג בUpdatePanel וIE בURLים עבריים

כידוע, ASP.NET WebForms משמר את הערכים של הפקדים בדף בין הpostbackים ע"י שימוש בform אחד המכיל, בדרך כלל, את כל שאר התגיות שבדף. בנוסף, בWebForms מקובל שכל דף עושה post לעצמו. כלומר, בדף default.aspx הערך של האטריבוט action של הform יהיה בדרך כלל גם כן default.aspx:

בנוסף, ASP.NET מבצע אופטימיזציה לURL המופיע בaction שיהיה יחסי לURL הנוכחי. כך למשל בדף הזמין בכתובת/blog/default.aspx הערך של action יהיה default.aspxx בלבד.

למה זה חשוב? כנראה, בגלל ש .NET 4.0 היא הגרסה הראשונה של הframework שבה WebForms תומך בRouting, ומאפשר URLים ידידותיים, לא חשבו על תסריט של כתובת URLL ידידותיות ובעברית, או לפחות לא בדקו את המימוש בצורה מספיקה.

לגבי מה הדברים אמורים?

נניח שהגדרנו Route כדלהלן:

עכשיו, אם נגלוש לכתובת "/באג/בדיקה", נגיע לדף default.aspx כשהערך של הפרמטר unicode הוא "בדיקה". כמו"כ, בדף שיוגש לנו הערך של הaction בform יהיה, כצפוי, "בדיקה".

כעת אם הדף default.aspx יעשה PostBack לעצמו, השאילתא תתבצע בהצלחה, ותיראה כך:

עד כאן הכל טוב ויפה. הבעיה מתחילה כשמנסים לבצע CallBack מאותו הדף.

עכשיו, נניח שעטפנו את הכפתור שעשה את הPostBack בUpdatePanel, וכעת במקום PostBack הוא מבצע CallBack (שזה הדרך של ASP.NET WebForms לבצע Ajax):

מה שקורה מתחת לפני השטח כשלוחצים על הכפתור btnTestAjax הוא, שמתבצעת קריאה לפונקציה Sys$WebForms$PageRequestManager$_onFormSubmit שבקובץ MicrosoftAjaxWebForms.js (חלק מASP.NET WebForms). הפונקציה מבצעת שאילתת ajax לURL שהיא לוקחת מבform.action:

בכל הדפדפנים חוץ מInternet Explorer הערך של document.forms[0].action הוא:

כך שהשאילתא מתבצעת בהצלחה לכתובת המבוקשת, בדיוק כמו PostBack רגיל.

הבעיה נעוצה בכך שלIE יש מנגנון שונה לקידוד URLים. לכן בIE, הערך של form.action בIE הוא:

בצורה שהשם של הדף הנוכחי ('בדיקה') נלקח ישר מform.attributes["action"] בלי שעבר קידוד. הבדיקה היחידה שנעשית היא, האם המחרוזת מכילה את התו '%', ואם התשובה היא חיובית – הפונקציה מסיקה שהמחרוזת כבר מקודדת כURLL תקין ולוקחת אותה כמות שלי. מה שלא נלקח בחשבון הוא התסריט המתקיים בIE שבו נוצרים URLים המכילים חלק מקודד וחלק לא מקודד. כתוצאה, בIE השאילתא שMicrosoftAjax מבצעת עלIE נראית כך:

שאילתא שIIS לא מוכן לקבל, ומחזיר בתגובה מצב שגיאה HTTP/1.1 400 Bad Request.

הפועל יוצא הוא, שכל שאילתות הAjax המתבצעות מדפים עם כתובת עברית ייכשלו בצורה שקטה (בלי להציג שום הודעה למשתמש) – המשתמש יצא מבולבל, ולכם כמפתח אין מה לעשות בנידון, שכן השאילתא לעולם לא מגיעה לקוד שלכם. מבחינתכם, כל הבאג מתרחש בקוד "קופסא שחורה" של צד ג'.

אני אדע האם הבאג משפיע גם על האתרים שלי?

הבאג קיים בASP.NET 4.0 כשהלקוח הוא IE (בדקתי גרסאות 9 ו10) ומשפיע על כל דף הנמצא בURL שבסגמנט האחרון שלו יש תווים unicode וכן יש לפחות סגמנט אחד נוסף שגם בו יש תווים unicode.

Workaround: אז מה אפשר לעשות?

ההיגיון הישר אומר שהיות והבעיה היא בכך שMicrosoftAjax נכשל לקודד את הערך של form.attributes["action"] כראוי, אם אני אקודד את הערך בעצמי, זה יעקוף את הבעיה. תוך שאני יוצא מנקודת ההנחה הזאת, כתבתי את הworkaround הבא:

בבדיקות שעשיתי, הסתבר שלאחר שכל CallBack מסתיים, MicrosoftAjax מאפס את הערך של form.attributes["action"] חזרה לערך ההתחלתי (הלא מקודד). לכן, בנוסף לקידוד הערך בעת טעינת הדף, אני מבצע קידוד מחדש בכל פעם שמתרחש האירוע endRequest של PageRequestManager. כדי לא להגיע למצב שאני מקודד את הaction פעמיים (מה שיביא לURL שגוי), אני מבצע decodeURI בכל פעם לפני הencodeURI.

היות והסקריפט לא מבצע שום בדיקות למוכנות הDOM, יש למקם אותו כאלמנט האחרון בbody אחרי סגירת הform.

כעת, (אחרי שהסקריפט מתבצע) הform נראה כך:

והערך של form.action בIE נהיה:

כמו בכל שאר הדפדפנים. ובא לציון גואל.

חשוב לציין, שקידוד הaction לא משפיע על ביצוע הPostBackים הרגילים, בגלל שגם הערך החדש (המקודד) הוא סגמנט URL תקין, והדפדפן מבין אותו.

מצורף: פרוייקט POC להדגמת הבאג והworkaround.

נ.ב. שמחתי לראות שהבאג תוקן ב.NET Framework 4.5 הבא עלינו לטובה.

הכירו את Entity Framework 4.1

מבוא

לאלו שלא מכירים, ADO.Net Entity Framework (בהמשך EF) היא המילה האחרונה בתחום הORM מבית מיקרוסופט. EF מאפשר למפתחים להגדיר בצורה נוחה את עולם הישויות שלהם (Conceptual Model) את הטבלאות שלהם (Storage Model) ואת המיפוי ביניהם (Mapping Model) כששלשתם יחד יוצרים את הEDM (ר"ת Entity Data Modell). כמו"כ הוא מספק את הממשקים הן לאחזור ישויות מבסיס הנתונים והן לעדכון ושמירה שלהם לבסיס הנתונים בחזרה.

כמו ששמתם לב, בעולם המושגים של EF, המחלקות אינן מייצגות רשומות במסד הנתונים, אלא הנן "ישויות" הממופות לנתונים באמצעות הEDM. בדרך זו, כל ישות יכולה להיות ממופה ליותר מטבלה אחת, וכן לדוגמה להשתמש בStored Procedure לפעולות CRUD (ר"ת Create, Read, Update, Delete).

אם טרם יצא לכם לעבוד עם EF תוכלו להכיר את היסודות דרך המאמרים המצוינים של עידו פלטו "הכירות עם EF" וסדרת המאמרים של הרה"ג דוט נט המתאימה במיוחד ללימוד עצמי.

מה התחדש בEF 4.1?

החידושים שבגרסה החדשה כולם נשענים על הAPI החדש שחושפות המחלקות  System.Data.Entity.DbContext וSystem.Data.Entity.DbSet (את הרשימה המלאה תוכלו למצוא בMSDN).

אחד השיפורים המשמעותיים ביותר בEF 4.1 הוא תמיכה בגישת Code First. גישת Code first מאפשרת להגדיר את המודל כולו במחלקות C# או VB.NET רגילות ולהוסיף או לשנות קשרים בין מחלקות ע"י שימוש בAPI של DbContext. והחשוב מכל, EF יידע לבד ליצור מסד נתונים המתאים לשדות והקשרים שהגדרתם באמצעות המחלקות.

כדי להבין את מהות הגישה נעיין בהיסטוריה של EF והORMים המסורתיים:

.NET 3.5 SP1 Entity Framework

EF בגרסתו הראשונה תמך רק בגישה Database first, כלומר מתחילים מיצירת מסד נתונים וממנו הVisual Studio יודע ליצור EDM וקוד .NET. גישה זו היא גם היחידה שנתמכת בlinq2sql.

image003
image002 .NET 4.0 Entity Framework 4.0

בVisual Studio 2010 נוספה תמיכה גם בגישת Model first. כלומר, ניתן להתחיל מיצירת המודל, וממנו VS יידע לייצר טבלאות במסד הנתונים ואת הקוד.

.NET 4.0 Entity Framework 4.1

הגרסה האחרונה של EF, מביאה את את בשורת הCode first. מעתה, אפשר להתמקד בפיתוח הDAL ולתת לEF 4.1 למפות את המחלקות הDAL שיצרנו לEDM ובסופו של דבר, לאחד ממסדי המנונים הנתמכים ע"י EF 4.1 והכל בזמן ריצה!

image001

חשוב להדגיש: שלא כמו בשני הגישות האחרות, בגישת Code first הEDM אינו "מפורש", כלומר לא נוצרים קבצי edmx בפרוייקט, אלא הEDM "נגזר" ממבנה המחלקות באופן לא מפורש בזמן ריצה. כמו"כ, שבגישת Code first לא משתמשים בכלי או אשף כדי ליצור את מסד הנתונים, אלא גם מסד הנתונים נוצר בזמן ריצה (אם הוא לא היה קיים קודם) ע"י המחלקהSystem.Data.Entity.Database.

הרווח הברור הוא, שבגישה זו שאין צורך לתחזק קבצי SQL לאתחול מסד הנתונים ומחלקות DAL בנפרד. מעתה יש ליצור רק DAL בגישת OOP המסורתית, והשאר יקרה באופן שקוף למשתמש באדיבות הEF.

הדגמה

נדגים את גישת Code first ע"י יצירת אפליקציה של חנות פשוטה. החנות מכילה מוצרים המחולקים לקטגוריות ומאפשרת לבצע הזמנה של מוצר.

איך מתחילים?

קודם כל, אם עוד לא התקנתם יש להוריד והתקין את Entity Framework 4.1.

שנית, צריך להוסיף לפרויקט שלכם references לספריות של EF:

references

עכשיו כשהכל מוכן לעבודה, נתחיל מטיוטה ראשונית של מחלקות הDAL שלנו:

כפי שניתן לראות אלו מחלקות POCO, בלי שום מודעות לEF. למעט המחלקה StoreContext היורשת מDbContext (ראה המשך).

Conventions

אני אקפוץ קצת קדימה ואציג את מבנה הטבלה שEF יצר עבור המחלקה Product:

products_nullable

אפשר לראות, שEF הסיק אוטומטית שאני רוצה שהמאפיין Id יהיה מפתח ראשי של הטבלה וכן יצר קשר Foregin Key לCategory. פירוש זה מתאפשר כי EF מבין conventions (מבני קוד מקובלים). ביניהם:

  • מאפיין בעל שם Id או ID יהיה המפתח הראשי של הטבלה
  • מאפיין virtual מטיפוס של מחלקה אחרת (במקרה שלנו Category במחלקה Product) ייצור Foregin Key לטבלה השנייה
  • מאפיין virtual מטיפוס של ICollection<T> כשT היא מחלקה אחרת (במקרה שלנו Product במחלקה Category) מבטא קשר יחיד לרבים (מספר Product לכל Categoryy)
  • כל שדה יכול לקבל Null או בSQL – להיות Nullable
  • מסד הנתונים ייוצר ב.\sqlexpress וייקרא על שם המחלקה שמייצגת את הDbContext (במקרה שלנו: StoreContext)

כל ההגדרות הנ"ל יכולות להיות משוכתבות ע"י עוד 2 פקטורים הנלקחים בחשבון ע"י הEF בעת קביעת תצורת הטבלאות: Data Annotations וDbContext Fluent API. כשסדר העדיפות שלהם הוא כדלהלן (כל המאוחר דורס את קודמיו):

  1. Conventions
  2. Data Annotations
  3. DbContext Fluent API

Data Annotations

שני דברים לא מוצאים חן בעיניי בטבלה שייצר הEF ע"פ הconventions. האחד – השדה Title לא צריך להיות Nullable, כמו"כ השדה Category_Id. עכשיו נשנה את הדברים האלו בעזרת הData Annotations.

אנו נשתמש באטריבוט RequiredAttribute ממרחב שמות System.ComponentModel.DataAnnotations כדי לציין שאנחנו רוצים ששתי השדות הנ"ל לא יהיו Nullable. נשנה את המחלקה Product להיראות כך:

והנה, הטבלה נראית יותר לטעמינו!

products_nonnullable

עכשיו, נניח שאנחנו רוצים להוסיף להזמנה שלנו את כתובת המגורים של המזמין. אבל, במקום להוסיף שדות כמו רחוב ועיר למחלקה של ההזמנה עצמה, אנחנו רוצים לארוז אותם במחלקה ייעודית. נקרא לה – AddressInfo:

ניתן לראות, שסימנתי את המחלקה באטריבוט ComplexTypeAttribute.

כעת נשנה את המחלקה Order להכיל את מאפיין הכתובת:

במסד הנתונים זה ייראה כך:

orders_with_address

בגלל שסימנו את המחלקה AddressInfo כComplexType, במקום ליצור טבלה מיוחדת עבור הטיפוס AddressInfo, כמו שEF עשה עם המחלקות האחרות בDAL שלנו, EF "שיטח" את מבנה הנתונים והכניס את השדות של AddressInfoo ישר אל תוך הטבלה Orders ומיפה אותם אל הטיפוס המקורי שלהם – AddressInfoo.

אגב, זו דוגמה טובה למושג ישות. אין במסד הנתונים שלנו טבלה לAddressInfo. במקום, הישות AddressInfo ממופה ל4 שדות מתוך טבלה אחרת.

נציין בקצרה Data Annotations שימושיים נוספים:

DbContext

DbContext מהווה את הגרעין של גישת Code first. פעולת "גילוי" המודל של EF מתחילה ממחלקה זו. בדיוק בשביל זה, כפי שאתם זוכרים, יצרנו מחלקה StoreContext שיורשת מDbContext. במחלקה, הכרזנו 3 מאפיינים מסוג DbSet<T> עבור שלשת הישויות שלנו (Category, Product, Orderr). אציין, שלא חייבים ליצור מאפיינים עבור כל הישויות במודל, למשל את הישות AddressInfo מנוע הEF "מגלה" אוטומטית כשהוא מנתח את המחלקה Order.

מטרה נוספת של DbContext היא חשיפת הAPI לאפיון המודל, הנקרא גם DbContext Fluent API על שם אופן השימוש בו. הAPI ממומש בצורה שמאפשרת "לשרשר" מספר קריאות למתודות, דבר שמאפשר ביטוי טבעי ודקלרטיבי.

לדוגמה: אם נרצה להגדיר שהמאפיין Title בProduct חייב להיות לא Nullable באמצעות הFluent API (במקום מה שהגדרנו קודם בData Anotationss) אנחנו נכתוב כך:

והתוצאה תהיה זהה.

IDatabaseInitializer

ומה אם נרצה לאתחל את מסד הנתונים בנתונים כלשהם מיד עם יצירתו? גם לזה קיים פתרון מובנה. EF מאפשר ליצור אסטרטגיית אתחול מתואמת אישית ע"י ירושה מאחת המחלקות הבאות:

  • CreateDatabaseIfNotExists<TContext> – פעולת האתחול תתרחש רק אם מסד הנתונים לא היה קיים ונוצר זה עתה.
  • DropCreateDatabaseAlways<TContext> – מסד הנתונים ייווצר מחדש והמידע יאותחל בפעם הראשונה שהDbContext יהיה בשימוש בתוך AppDomainn (כל הנתונים הקיימים יימחקו).
  • DropCreateDatabaseIfModelChanges<TContext> – מסד הנתונים ייווצר מחדש והמידע יאותחל אם המודל שונה מאז שמסד הנתונים נוצר (כל הנתונים הקיימים יימחקו).
  • IDatabaseInitializer<TContext> – ממשק המאפשר מימוש של אסטרטגיית אתחול מתואמת אישית.

לשם הדגמה, נכתוב את אסטרטגיית האתחול שלנו בStoreInitializer:

כדי שEF יידע להריץ את  תהליך האתחול שלנו אנו נכריז בבנאי הסטטי של הDbContext שלנו, שאנחנו רוצים שEF ישתמש במחלקה שיצרנו לצורך אתחול המסד נתונים, בצורה כזאת:

חשוב לציין: EF בגרסה 4.1 לא תומך ב"שדרוג" מסדי נתונים. כלומר, אם שיניתם את במנה המודל בצורה שאינו תואם עוד את מבנה הטבלאות במסד הנתונים, EF לא יידע לעדכן את מבנה הטבלאות תוך שמירה על הנתונים שבהן. אם כי תוכלו לכתוב לוגיקה שלכם לביצוע שדרוג זה ע"י מימוש IDatabaseInitializer<TContext>.

מה ההבדל בין EF ל-LINQ to SQL

רבים תוהים מה ההבדל בין LINQ to SQL (להבא linq2sql) לבין EF, במיוחד לאור כך שהם שוחררו בהפרש קטן זה מזה.

עיקר ההבדל נעוץ בתפיסת העולם של EF, שכמו שאמרנו עוסקת ב"ישויות" ולא ב"מבנה טבלאי". כך שבlinq2sql אין את מנגנון המיפוי העשיר שיש לEF. בנוסף, linq2sql עוצבה לתת מענה לעבודה מול בסיס נתונים ממשפחת SQL Server בלבד.

לאומת זאת, EF מתאימה למודלי ישויות גדולים ומורכבים, תומכת בעבודה עם אובייקטים פשוטים (POCO) ובעבודה מול בסיסי נתונים מסוגים שונים (Oracle, MySql, Sybase ועוד) ע"י מודל Providerים גמיש וניתן להרחבה.

כמובן אם לקחת בחשבון את השיפורים שנכנסו בגרסה 4.1 כל השוואה לlinq2sql מחווירה.

How to remove duplicate HTML5 schema on Visual Studio 2010 SP1

duplicate html5

עם שחרור הVisual Studio 2010, שבגרסתו הראשונה לא כלל תמיכה בHTML5, כמו רבים אחרים התקנתי Add-In של intellisense לHTML5 שמשלים את החסר.

הבעיה היא שכשעדכנתי את הVS שלי לSP1 שכבר מכיל תמיכה מובנית בHTML5, נוצר כפל.

סביבת העבודה שלי מציגה כעת שני סכימות HTML5, האחת שייכת לAdd-In והשנייה לSP1. בנוסף שמתי לב שהסכימה של הAddin מוצגת כ'HTML 5' כשהסכימה שבאה עם SP1 נקראת 'HTML5' (ללא רווח).

עם כל הרצון הטוב, לא מצאתי אפשרות ידידותית להסרת הAddin, ולכן נאלצתי לאלתר.

לאחר חיפוש קצר, מצאתי שVS מאחסן את הסכימות המוכרות על ידו ברגיסטרי:

משם למדתי, שאכן ישנם שני מפתחות שונים עבור שתי הסכימות:

regedit_html5addin

של הסכימה של הAddin. והשנייה:

regedit_html5sp1

עבור הסכימה של הSP1. ולכן הכפילות.

schemasעוד למדתי, שהסכימה עצמה היא בעצם אותה האחת לשני הרשומות המוצגות. שכן, שתי הרשומות ברגיסטרי מצביעות לאותו הקובץ: html_5.xsd. כשבדקתי את הקובץ, מצאתי שהוא בעצם נדרס ע"י התקנת הSP1 (על פי חתימת התאריך שעל הקובץ שתאמה לתאריך בו התקנתי את SP1) כך ששתי הסכימות הן בעצם הסכימה המאוחרת יותר – של SP1 כך שהבעיה היא יותר אסתטית.

הפתרון

היות וכל הבעיה נגרמת כתוצאה מכפל רשומות ברגיסטרי, כל מה שצריך לעשות כדי לפתור אותה הוא למחוק את הרשומה שיצר הAddin:

doneהתוצאה לפניכם:

ואם כבר עברתי את הדרך, אז יצרתי קובץ reg להסרת הAddin של HTML5 לVisual Studio – פשוט להוריד ולהריץ.

How to extract text from HTML

איך ניתן לחלץ טסקט מתוך HTML

לאחרונה הייתי צריך לייצר תקצירים טקסטואליים מתוך תוכן HTMLי. לא מדובר פה בניתוח סמנטי של התוכן, אלא רק שליפת עד 160 תווים ראשונים. מסתבר שגם זה לא כ"כ פשוט.

כשניגשתי למשימה, בניגוד לרגיל, לא קבעתי לעצמי מראש מה בדיוק קטע הקוד שאני כותב אמור לעשות, במקום זה התחלתי ממה שהוא וודאי אמור לעשות, ואז שיפרתי עד שקיבלתי את התוצאה שרציתי. בגדול, מה שהפונקציה צריכה לעשות הוא לדלג על כל הקטעים בתוכן שנמצאים בין התו < לבין התו > שאלו הם התגים לדוגמה ב: '<span dir="rtl">abc</span>' יישאר רק'abc'.

ככל שהתקדמתי בכתיבת הקוד והרצת בדיקות מולו, גיליתי שיש בעיות נוספות שעלי לפתור על מנת להגיע להמרה איכותית.

אחת מהן היא טיפול בHTML Entities. אם למשל מופיע בHTML הרצף &lt; ארצה שהוא יומר ל'<'. כאן באה השאלה מתילבצע את ההמרה? שכן אם אבצע אותה אחרי שהסרתי את התגיות אז בדוגמה הבאה: '&l<b>t;</b>&lt;abc&gt;' אני אקבל '<<abc>'. ואם אבצע אותה לפני הסרת התגיות אז אקבל '&lt;', כשהתוצאה הרצויה היא: '&lt;<abc>'.

בעיה נוספת שהיה עלי לפתור היא Whitespace collapsing. בHTML, מספר תווי רווח עוקבים נחשבים לאחד, ככה שהפלט עבור'<b>hello </b> <b> world</b>' צריך להיות 'hello world' עם רווח בודד בין המילים. גם כאן חשוב לזכור שביצוע הסרת רווחים מיותרים אחרי המרת HTML Entities יגרום להסרת כל ה &nbsp; שלא אמורים להיות מוסרים בבwhitespace collapsingg.

כמו"כ, חשוב לזכור את נושא הביצועים. כמה פעמים הפונקציה צריכה לרוץ על כל התוכן כדי לבצע את כל הפעולות האלו? מסתבר שמספיק פעם אחת בלבד!

את הפונקציה בדקתי מול המחרוזת הזו:

הפונקציה שהדגמתי תיתן את התוצאה הכי קרובה לParser מלא של HTML לדוגמה Html Agility Pack אבל במהירות גבוהה בהרבה. חשוב לציין שהיא תעבוד באופן זהה בין על XHTML תיקני ובין על HTML 44.

דרך נוספת, אבל פחות יעילה הן מבחינת ביצועים והן מבחינת תאימות, תהיה שימוש בביטוי רגולרי להסרת התגיות.

איך לא חשבתי לקרוא לזה ASWIFT?!

היום נתקלתי בבלוג Google Code במאמר הנושא את השם "Your Web, Half a Second Sooner", דרכו הגעתי לווידאו של הרצאה מרתקת שהתקיימה בכנס Velocity 2010 בה איזה בחור מגוגל סיפר על טכניקה "חדשה" שהם המציאו. הם קראו לה ASWIFT. בקצרה, הם (ואני מדבר על צוות של גוגל) "מצאו" פתרון לטעינת קוד javascript של צד ג' בצורה אסינכרונית (שלא נועלת את הדף עד לסיומה) והרצתם תחת הדומיין של הדף המארח (לצורך עקיפת מנגנון Same Origin). במקרה שלהם זה היה עבור הAdSense.

העניין הוא, שאני (ולמיטב זכרוני לא היה לי צוות) כתבתי פתרון זהה לבעיה זהה בפיתוח ווידג'ט להפצת רשימת המאורסים עוד ב2008. כיום הווידג'ט הזה מוטמע ברשימה של אתרים מובילים בציבור הדתי-חרדי, ביניהם באתר "בחדרי חרדים". ההבדל הוא שגוגל כנראה ראו בזה הברקה, כשאני לתומי ראיתי בזה בסך הכל דרך לספק מוצר איכותי.

מה אני לומד מזה? האמת היא – לא יודע. אולי הייתי צריך לקרוא לזה ASWIFT?

שימושיות ואבטחה בLogout

השאלה המתבקשת היא: זה Logout, מה כבר יכול להשתבש?! – אז מסתבר שלא מעט. מדובר בדברים פשוטים וברורים מאליהם עד כדי כך, שפשוט לא חושבים עליהם.

את לינק היציאה (או logout) תמצאו ברוב האתרים על הMasterPage. הלינק מוצג רק למשתמשים שמזוהים באתר ע"י ביצוע כניסה (או login), שכן רק עבורם שייכת פעולה זו. השאלה המעניינת היא, מה מתרחש כשלוחצים על הלינק?

ניתן לממש logout בכמה דרכים:

  • כמטפל לאירוע הClick בCode Behind של הMasterPage עצמו.
  • ע"י הפנייה לדף מיוחד (נקרא לו Logout.aspx) שכל מטרתו היא לבצע logout.

בשני המקרים מתחילים מקריאה למתודה FormsAuthentication.SignOut() שמבצעת עבורנו את השמדת העוגיות וכדו'. עד כאן הכל פשוט. הבעיה מתעוררת בדף שיוחזר למשתמש.

במקרה של מימוש ע"י מטפל אירוע Click בMasterPage יש שתי בעיות פוטנציאליות:

  1. מטפלי האירועים מבוצעים בשלב מאוחר במחזור חיי הדף. ולכן, תכנים רבים שאמורים להיות מוצגים רק למשתמשים מזוהים, עדיין יוצגו בדף בפעם הראשונה שהדף ייטען לאחר הlogout.
  2. היות ומדובר בMasterPage, ייתכן שהדף שממנו בוצע הlogout נגיש למשתמשים מזוהים בלבד. כתוצאה, לאחר הlogout המשתמש יוקפץ מיידית לדף הכניסה (או login)!!

על אף שלכל אחת מהבעיות הנ"ל קיימים פתרונות פשוטים, אני בחרתי בדרך השנייה: הפנייה לדף נפרד שכל מטרתו היא ביצוע logout. 

מימוש Logout באמצעות דף נפרד

כרגיל, מתחילים מקריאה לFormsAuthentication.SignOut(). עכשיו מתעורר השאלה לאיפה מעבירים את המשתמש (redirect) לאחר ביצוע ההתנתקות מהמערכת.

אם נחזיר את המשתמש לדף הקודם (הדך ממנו הוא ביצע login) ייתכן ועכשיו, לאחר ההתנתקות, אין לו הרשאות לגשת לדף ההוא והוא יוקפץ לדף הlogin כמו בMasterPage. כמו"כ זה יפתח פתח לתקיפות התחזות ע"י זיוף שדה הreferer. אם נחזיר אותו לדף הבית, הקפצה שרירותית מעומק הניווט באתר לדף הבית תפגע בחוויית השימוש.

הגישה שבחרתי היא:

  • אם יש ערך לreferer, והוא נמצא בדומיין של האתר עצמו, ויש למשתמש לא מזוהה גישה אליו –> עבור לreferer.
  • אחרת, עבור לדף הבית.

הקוד:

שימוש בWeb.sitemap לשמירת מידע SEO

מפת אתר

כולם מכירים את הSiteMap בתור מפת אתר עבור מנגנון הניווט של ASP.NET. אבל, מה אם היה אפשר להוציא ממנו יותר?

במאמר זה אדגים שימוש בWeb.sitemap לשמירת תכנים עבור תגיות המטא description וkeywords. גישה זו תאפשר ניהול נוח של מידע metaa של האתר, תוך הפרדה מוחלטת בין לוגיקה ומידע.

כמו"כ, כחלק מהפתרון, אציג טכניקה אלגנטית להרחבת הפונקציונליות של המחלקה Page ללא צורך בירושה ממנו. פתרון זה יחול אוטומטית על כל הדפים באתר ללא צורך בהגדרות נוספות.

מהן תגיות המטא?

תגיות meta הן אחד הפקטורים שמנועי החיפוש משקללים על מנת להבין טוב יותר את תוכן דף האינטרנט. תגיות אלו ממוקמות בתוך התגית head של הדף. הם אמנם אינם גורמים המשפיעים במידה ניכרת על דירוג האתר במנועי החיפוש, אך חשיבותם רבה לתחום הסמנטיקה:

  • תגית description – מייצגת את ה"תקציר" של תוכן הדף, או לעיתים תיאור של תוכנו. מידע זה מוצג, לדוגמה, בגוגל מתחת לכותרת התוצאה.
  • תגית keywords – מייצגת את מילות המפתח (או אם תרצו, תגיות) של הדף. מידע זה מסייע למנועי החיפוש להבין טוב יותר מה מבחינתכם עיקר התוכן. למשל, מילת מפתח תיחשב למרכזית אם היא מוזכרת בtitle, בkeywords, בתגית h11 וכן מופיעה בגוף תוכן העמוד.

דוגמה:

ASP.NET 4.0 נוספו למחלקה Page שני מאפיינים חדשים עבור תגיות אלו: MetaDescription וMetaKeywords עבור description וkeywords, בהתאם.

עוד בנושא תגיות מטא.

על web.sitemap בקצרה

הקובץ web.sitemap מייצג עץ של siteMapNode. לכל node בעץ יש 3 מאפיינים מובנים:

  1. url – מציין את הכתובת שאותה באה הרשומה לתאר.
  2. title – הכותרת של כתובת הזו כפי שתוצג בפקדי הניווט.
  3. description – תיאור מורחב על הדף. גם הוא מוצג ע"י פקדי הניווט.

בנוסף לאלו, אם קראתם את התיעוד של ASP.NET Site Maps וודאי נתקלתם בSiteMapNode.Attributes, מערך ערכים מתואמים אישית בהם ניתן לאכסן מידע נוסף אודות הSiteMapNode. – בהם בדיוק נעשה שימוש בפתרון זה.

הרעיון

הרעיון הוא ליצור מנגנון פשוט להטמעה באתר קיים שייקח את המידע עבור תגיות המטא מattribute בsiteMapNode, ויציב אותם במאפיינים של הדפים. אבל, רק במקרה שלא הוצב שם ערך ע"י הדף עצמו! הדגש הוא על פשטות ושימוש בכלים קיימים.

לצורך הדגמת הקונצפט, יצרתי פרוייקט asp.net בעל 3 דפים:

  1. Default.aspx – דף הבית
  2. About.aspx – אודות
  3. HardcodedMeta.aspx – דף המכיל כבר מידע מטא

בשני הדפים הראשונים לא הצבתי כל מידע במאפייני המטא בדף (MetaDescription וMetaKeywords) ולכן המידע הזה יילקח מweb.sitemap. לאומת זאת, בדף השלישי, מידע מטא מוטמע בגוף הדף, ולכן הוא לא יושפע מהSiteMap.

בתור התחלה, יצרתי את קובץ הweb.sitemap הוספתי בו את הערכים עבור מילות המפתח:

ניתן לראות, שמלבד הערכים הרגילים, הוספתי לכל אחד מהדפים מאפיין keywords שמכיל את מילות המפתח הקשורות לדף. כעת, כל מה שנותר הוא להציב את הערכים האלו במאפיינים המיועדים בדפים. השאלה היא, איך?

לשלוף את הערכים מתוך SiteMap זה החלק הפשוט:

עכשיו צריך להציב אותם בדפים. כמובן שניתן להוסיף קוד זה בPage_Load בכל דף, או לרשת בכל הדפים ממחלקה אחת ולממש הצבות אלו בה. בכל מקרה – זה לא נח! מה, אם היה ניתן לממש את הלוגיקה הזו במקום אחד, כך שתשפיע על כל הדפים באתר? – הנה רעיון:

יצרתי מחלקה בשם SitemapMetadataModule שמממשת את IHttpModule. בInit שלה הוספתי מטפל לPreRequestHandlerExecute המתרחש בשלב שבו כבר ידוע מה יהיה הHttpHandler שיטפל בשאילתא (request), אבל לפני ביצועו. (לאלו שלא מכירים את הנושא, מומלץ לקרוא על HttpModule וHttpHandler בMSDN)

במטפל של PreRequestHandlerExecute אני בודק האם המטפל הנוכחי הוא מסוג Page. בשלב זה של הpipeline קיים כבר מופע של הPage, אך שרשרת האירועים שלו טרם התחילה. זה שלב מצוין להוספת מטפל לאירוע PreRenderComplete של הדף הנוכחי.

אירוע PreRenderComplete מתרחש האחרון לפני שלב רינדור הדף, השלב שממנו והלאה לא ניתן יותר לשנות את תוכן הדף. כאן אני מניח הנחה שכל ההצבות שהיו אמורות להתבצע ע"י הדף עצמו, כבר התבצעו בשלבים מוקדמים יותר. ולכן, אם אני רואה שלא הוצבו ערכים בתגיות המטא, אני מרשה לעצמי להציב אותם במקום הדף. (עוד על מחזור החיים של Page בMSDN)

תוכלו להוריד את פרוייקט ההדגמה במלואו מכאן. (VS2010)

מה עוד מתאפשר בגישה זו?

המון! לדוגמה: web.sitemap תומך בריבוי שפות ע"י קביעת enableLocalization לtrue ושימוש בresources כערכים. בשילוב הדברים, מתאפשר להציג מידע מתואם שפה בdescription והkeywordss של האתר בקלות רבה.

ובגדול, לדעתי, הפתרון הזה שואף להרבה יותר. למה, למשל, לא לכלול בSiteMap הוראות לרובוטים, מידע עבור מפת אתר של גוגל, ועוד…? – אני בעד! ועל זה, בעזרת השם, במאמר הבא בסדרה.

פתרון "האתרוג באמצע"

אתרוג

אתמול יצא לי לראות שאינטרנט רימון ו"אתרוג" הוסיפו בקודש שירות חדש, או ליתר דיוק שיפור לשירות קיים. "בשורה משמחת ללקוחות אינטרנט "אתרוג": פיתוח חדש וייחודי – סינון כל האתרים כולל אתרי SSL", כך נכתב בהודעה באתר אתרוג.

"מדובר באפליקציה המאפשרת סינון תוכן בתוך אתרים מאובטחים דוגמת SSL ו HTTPS. הפיתוח הינו היחיד בעולם ומהווה פריצת דרך מקצועית בעולם סינון התוכן באינטרנט. אפליקציה זו פותחה ע"י אינטרנט רימון עבור חטיבת אתרוג המספקת אינטרנט מסונן ומבוקר בהמלצת גדולי ישראל ורבנים שליט"א." כתב מגיב משולהב בפורום ב"חדרי חרדים".

ובמקום התחילה התנצחות של השערות, לגבי אופן הפעולה (והסיכונים?) של השירות החדש.

כמובן שהעניין הצית את יצר הסקרנות שלי, שכן סינון SSL אמנם אינו בלתי אפשרי אך מדובר במשימה לא פשוטה בכלל, אז יצאתי לבדוק ולחטט קצת בעצמי.

אז איך זה עובד?

כדי שהסינון יתאפשר, אינטרנט רימון ממליצים ללקוחותיהם להתקין במחשבים שלהם קובץ המכיל רשומת registry, המתקינה Trusted Root Certification Authority. כלומר, רשות מאשרת חדשה.

למה זה נחוץ? – מכאן זו השערה.

זה נראה שאינטרנט רימון מבצעים "התקפת" האדם באמצע על פרוטוקול SSL. ע"י המרת חתימת SSL המקורית של האתר באחרת – שלהם. כדי להבין את התהליך, קודם צריך להבין מה זה SSLL, למה הוא משמש ואיך הוא עובד.

מה זה SSL?

SSL (וTSL) הן שכבות מתחת לפרוטוקול HTTP שהופכות תקשורת בין שרת ולקוח למאובטחת. פרוטוקול HTTP על גבי SSL נקרא HTTPS.

פרוטוקול תקשורת נחשב לבטוח אם מתקיימים בו 3 הכללים הבאים:

  1. צד ג' לא יכול לפענח את ה"שיחה"
  2. ניתן לוודא שההודעה לא שונתה ע"י צד ג'
  3. ניתן לוודא את זהות השולח

פרוטוקול SSL מבטיח את שלושת ההבטחות האלו.

איך SSL עובד?

זה מתחיל ברגע שהלקוח פונה לשרת. דבר ראשון צריכים להבטיח שצד ג' לא יוכל להקשיב (או לשנות) לשיחה. שני תנאים אלו מובטחים ע"י תהליך "לחיצת יד" (handshake).

בתהליך הhandshake משתמשים בשני מפתחות הצפנה. הראשון אסימטרי, והשני סימטרי.

  1. השרת שולח ללקוח מפתח ציבורי
  2. הלקוח מצפין בעזרתו מפתח סימטרי (שהוא עצמו מייצר) ושולח את התוצאה לשרת
  3. מכאן המשך ה"שיחה" בין הצדדים כולה מוצפנת בעזרת המפתח הסימטרי שייצר הלקוח

היות והלקוח הצפין את המפתח הסימטרי ע"י המפתח הציבורי שקיבל מהשרת, גורם שמקשיב ל"שיחה" לא יוכל להשיג את המפתח הסימטרי ובעזרתו לפענח את השיחה, שכן הוא לא יודע את המפתח הפרטי שרק בעזרתו ניתן לפענח את המפתח הסימטרי.

בשלב הבא, על מנת להבטיח את זהות השרת (לדוגמה, כדי למנוע מאתר זדוני להתחזות לבנק) לשם כך האתר שולח ללקוח מספר אישור שהוא קיבל מרשות מאשרת ואת שם הרשות, בגדר אם אתה לא מאמין לי – תשאל את אבא שלי.

היות ווידוי הזהות תלוי בבדיקה מול ראשות מאשרת, לכך כדי שאישור הSSL יוכר בכל הדפדפנים ניתן לרכוש SSLים ממספר מצומצם ומוכר של "רשמים" כשבמקרים רבים הם הם הרשות המאשרת. לכל אחת מרשויות אלו יש Certificate הנקרא root "שורש" שמותקן בכל מערכת הפעלה בעולם. כך בכל חתימת SSL תלויה ברשות מאשרת מוכרת ע"י מערכת ההפעלה.

נחזור לפתרון של אינטרנט רימון.

הבעיה והפתרון

הבעיה שמונעת ספקי סינון מלסנן תוכן המועבר כHTTPS היא, שאין להם דרך לדעת את המפתח הסימטרי כדי שבעזרתו הם יוכלו לפענח ו"לראות" את התשדורת. ולכן, בעצם, אין להם מושג מה מעביר השרת ללקוח, וכן להפך.

הפתרון, בא בצורת "פריצה" של מנגנון הhandshake.netspark

כשהרשת שולח ללקוח את המפתח הציבורי, אינטרנט רימון, מחליפה אותו במפתח ציבורי שלהם, וכך כשהלקוח מצפין בעזרתו את המפתח הסימטרי שלו, אינטרנט רימון יכולים לפענח אותו ומכאן את כל ההמשך התקשורת ביניהם.

הבעיה היא שישנו עוד מנגנון שנותר להם "לפרוץ", והוא מנגנון האימות. שכן, היות ואינטרנט רימון לא יכולים לדעת מהו המפתח של השרת המקורי ולכן לא יוכלו לשלוח ללקוח כמו שצריך את מספר האישור לשם אימות…

על הבעיה הזו הם התגברו בעזרת התקנת Root Certificate חדש במחשבי הלקוחות, כך שבעצם מעתה הם יכולים "להמציא" מספר אישור עבור כל אתר ולצרף את שם הרשות המאמתת שלהם, וכך כשיבוא הלקוח לאמת את הזהות השרת זה יהיה מולם ולא מול הרשות המאמתת המקורית!

פתרון מקורי של אינטרנט רימון?

לא בטוח. החתימה שאינטרנט רימון ממליצים להתקין שייכת לnetspark.com, וייתכן שבעצם גם הפתרון. כמו שניתן לראות, ב"פתרונות" של netspark מופיע פתרון בשם Secure Site Inspection, המתואר פתרון זהה, בתוספת אפשרות של חסימת אתרים מסוימים מפני סריקה – אפשרות שלא קיימת באינטרנט רימון.

זה טוב או רע ליהודים?

כמו שכבר תהו רבים, מה יהיה על הפרטיות של הגולשים? שכן, בתהליך הסינון תוכן האתר גלוי ללא שום הצפנה.

כמו"כ, יש את שאלת האחריות. בדרך כלל אחריות על בטיחות חתימת SSL מוטלת על מנפיק החתימה והביטוח מגיע לכ100,000$ עבור חתימות מסויימות, במקרה של אינטרנט רימון, שהתחימות המקריות מוחלפות בחתימות שהנפיקה אינטרנט רימון, האחריות, לכאורה, תיפול עליהם. בקיצר, טוב או רע, תחליטו אתם…

איך ניתן להסיר את ההתקנה של רימון?

החלטתם להסיר את ההתקנה? רימון אמנם לא מציעים באתר שלהם תוכנת הסרה, אבל זה די פשוט. – בדיוק בשביל זה הכנתי קובץ להסרת התוסף של אינטרנט רימון פשוט להוריד ולהריץ.

הכירו את IIS URL Rewrite 2.0

URL Rewrite

תהיתם פעם איך ניתן לשפר את חווית המשתמש ודירוגו של אתר קיים בנועי החיפוש בלי לבצע שינויים נרחבים לאתר כולו? איך לשמר כתובות ישנות של דפים שלאחר שדרוג עברו לכתובת חדשה בלי לזהם את הקוד החדש במידע אודות הכתובות הישנות?

הכירו את IIS URL Rewrite 2.0! בשונה מהגרסה הקודמת, שידעה בעיקר לתרגם כתובות מתחת לפני השטח, הגרסה החדשה מהווה חבילת פתרונות שיכתוב וניתוב עשירה עם ממשק ניהול מוטמע בממשק הIIS. כעת, נדרשים רק קליקים ספורים כדי לבצע פעולות שיכולות לשדרג משמעותית את האתר, הן עבור הגולשים – שיקבלו כתובות URL הגיוניות, והן מבחינת מנועי החיפוש ע"י ביטול תוכן כפול, והכנסת מילות מפתח לURL עצמו.

קנוניקליזציה של הדומיין

קנוניקליזצית (canonicalization) הדומיין היא בעצם, הגדרה של הדומיין המועדף. כידוע, מקובל שלכל אתר ניתן לגשת ע"י אחת משתי הכתובות: www.domain.com וdomain.com (אא"כ, במקרים נדירים, הוגדר אחרת).

כמה מוזר שזה נשמע, מבחינה טכנית אלו שני סאב-דומיינים שונים. וכך, הזחלן רואה את אותו התוכן זמין בשתי כתובות שונות!

יתרה מזאת, אם לדוגמא הטמעתם את כפתור הLike של פייסבוק בעמוד באתרכם (נגיד article.html), מונה הלייקים יהיה שונה עבור: www.domain.com/article.html וdomain.com/article.html שכן, הם שני URLים שונים.

אז מה עושים?

כדי להגיע להגדרות הURL Rewrite של אתר יש לפתוח את ממשק הניהול הIIS או בשמו: Internet Information Services Manager. לבחור את האתר עליו רוצים להחיל את ההגדרות החדשות, שם תראו את האייקון של URL Rewrite.

IIS URL Rewrite icon

לפניכם יוצגו שתי רשימות: האחת Inbound rules, שבה יופיעו הכללים שיקבעו מה צריך לקרות כשפונים לURL מסויים בשרת, והשנייה Outbound rules, שתקבע מה לעשות עם URLים (לינקים וכדו') המופיעים בדפים שמוחזרים ע"י השרת.

add ruleהיות ועבור כלל הקנוניקליזציה יש תבנית מובנית, נשאר רק להליק על Add Rule בפאנל בצד ימין של המסך, ולבחור בCanonical domain name.

בחלון שיפתח יש להזין את הדומיין הרצוי (לדוגמה www.domain.com) וOK! מעתה כל מי שיגלוש לdomain.com יופנה ע"י הפניית 301 לwww.domain.com.

מה בעצם קרה פה?

אם תפתחו את web.config של האתר שאותו הגדרתם, אתם תגלו שנוספו שם כמה תגיות חדשות,

וליתר דיוק הקוד הבא:

קצת הסבר: התגית rewrite מכילה את כל הכללים (rules) הקובעים מה לעשות אם שאילתות HTTP נכנסות. לכל כלל (rule) יש שם (אטריבוט name) לצורך זיהוי.

התגית match קובעת ביטוי רגולרי של "התאמה" עבור כתובת URL הנכנסת (רק לחלק הpath שלה), הגדרה זו נועדה "ללכוד" קטעים מהURL המקורי על מנת להעבירם להמשך עיבוד (ע"ע action).

קבוצת conditions כשמה, מכילה רשימה של תנאים שכולם חייבים להתקיים כדי שהכלל יוחל על השאילתה הנוכחית. במקרה הזה ישנו תנאי יחיד: משתנה השרת HTTP_HOST צריך לא להתאים (negate="true") לביטוי הרגולרי "^www\.domain\.com$". או בעברית, כל פנייה לאתר שלא באמצעות www.domain.com. בpattern ניתן להשתמש בביטוי רגולרי או בסינטקס כוכבית.

action הוא החלק שבו קורה כל האקשן, כאן אנו אומרים לIIS מה לעשות עם השאילתא הנוכחית (במקרה שהיא עונה על כל התנאים שלעיל). האטריבוט type מציין שאנחנו רוצים לעשות הפניית 301 לכתובת המצוינת באטריבוט url. שימו לב, שבסופו של הערך של url מופיע הביטוי {R:1}. הוא מתייחס לקבוצה הראשונה שלכדנו ע"י הביטוי הרגולרי בmatch! כלומר, אם לדוגמה, הURL המקורי היה domain.com/about.html אזי, התוצאה תהיה הפניית 301 לכתובת www.domain.com/about.html! [בהרחבה בנושא]

ישנם עוד מספר תבניות מובנות עבור כללים שימושיים:

הפיכת כל הURLים הלטיניים לאותיות קטנות

בעיית האותיות הגדולות והקטנות בURL הלטיניים היא נושא ידוע, לדוגמה, שני הURLים הבאים מחזירים את אותו התוכן:

  • www.domain.com/index.html
  • www.domain.com/INDEX.HTML

הפתרון, בדיאלוג Add Rule בקטגוריית SEO ישנה תבנית בשם "Enforce lowercase URLs". וOK!

חשוב לזכור! באתרים רבים, באמת ישנו הבדל בין אותיות גדולות לקטנות, וכלל זה יכול לשבש את פעילותו התקינה של האתר.

הוספת או הסרת סלש בסוף הURL

גם הסלש "/" שמסיים את הURL עלול ליצור בעיית SEO, לדוגמה, שני הURLים הבאים מחזירים את אותו התוכן:

  • www.domain.com/en
  • www.domain.com/en/

בדיאלוג Add Rule בקטגוריית SEO חפשו את התבנית "Appedn or remove trailing slash symbol". כל שנותר לעשות הוא לבחור האם אתם רוצים את הסלשים או לא וOK!

פתרון לבעיית כפילות תוכן בדף בית של אתר

כפי שכבר כתבתי בעבר, קיימת בעיית כפילות תוכן בדף המוגדר כדף בית של האתר. שכן, שתי הכתובות הבאות מכילות את אותו התוכן!

  • www.domain.com
  • www.domain.com/index.html

בעיה זו ניתן לפתור ע"י הפניית כל הפניות מindex.html לwww.domain.com באמצעות הכלל הבא:

שימו לב! בדפי ASP.NET (לדוגמה default.aspx) הפנייה זו תגרום להשבתת מנגנון הPostBack של ASP.NET, שכן הaction של הform בדפים אלו מוגדר לשם הדף (ולא לכתובת שדרכה הגענו לדף) ולכן כשהדפדפן מחזיר את מידע הform לדף default.aspx הוא מקבל תשובת 301 וכל תוכן הformm הולך לאיבוד!

איך לגרום לIIS URL Rewrite לעבוד עם ASP.NET WebForms?

הפתרון לבעיה דווקא פשוט! החל מASP.NET 3.5 SP1 למחלקה HtmlForm קיים מאפיין Action, דרכו ניתן לקבוע ידנית לאיפה אנחנו רוצים שהמידע של הform יישלח.

כל מה שנותר לעשות הוא להוסיף לדף את הקוד הבא:

במקרים של שימוש נרחב בURL Rewrite יהיה מומלץ ליצור מחלקה שממנה יירשו כל הדפים באתר ולהחיל את הפתרון עליה.

בעיה נפוצה נוספת היא לאימות לWebResources.axd של ASP.NET. אם אחד הכללים ישנה את כתובתו, רכיבים חיוניים של ASP.NET עלולים לא להיטען. לכן בכל כלל שעלול להשפיע על הקובץ הנ"ל יש להוסיף את התנאי:

למידע נוסף ראה: תאימות IIS URL Rewrite וASP.NET WebForms.

מה ההבדל בין IIS URL Rewrite לבין ASP.NET Routing

במבט לכאורה שני מנגנונים האלו עושים עבודה דומה – שניהם מאפשרים לאתר להציע כתובות ידידותיות למשתמש ולמנועי החיפוש. למרות זאת קיימים הבדלים יסודיים בין שתי הטכנולוגיות.

מבחינה טכנית

IIS URL Rewrite

  • הינו מנגנון "שכתוב" (rewrite) טהור, הוא עוסק בהמרה ומיפוי של כתובות URL שונות.
  • פועל בשלב המוקדם ביותר ברצף הביצוע של IIS, באירוע BeginRequest.
  • לא ניתן להרחבה מצד היישום.

ASP.NET Routing

  • הוא יותר מנגנון של מיפוי "מטפלים" לURLים.
  • פועל בשלב מאוחר יותר, באירועים ResolveRequestCache וMapRequestHandler.
  • פתוח ומיועד להרחבה מצד היישום.

מבחינה מעשית

  1. IIS URL Rewrite מיועד לשימוש עם כל טכנולוגיה, בין ASP.NET, PHP, ASP וקבצים סטטיים. בASP.NET Routing אפשר להשתמש רק מיישומים מבוססי NET.
  2. IIS URL Rewrite פועל בצורה זהה ללא הבדל בין Integrated mode לבין Classic mode. בASP.NET Routing נדרשות הגדרות מיוחדות כדי להתאימו לClassic mode.
  3. בנוסף לשכתוב URLים IIS URL Rewrite יכול גם לבצע הפניות, להחזיר קודי מצב מתואמים אישית, ולבטל שאילתות. משא"כ ASP.NET Routing.

אין לי גישה לIIS Manager מה אני יכול לעשות?

במקרים מסוימים, לדוגמה באחסון, לא תהיה לכם גישה לIIS Manager, זה לא אומר שאתם לא יכולים עדיין להגדיר URL Rewrite…

קודם כל, כפי שציינתי, ניתן לייצור כל כלל ע"י עריכה ישירה של web.config. אבל בדרך כלל לא יהיה צורך להגיע לכך, שכן Visual Studio יודע לעבוד מול פרויקטים שיושבים בIIS המקומי. כל שעליכם לעשות הוא לפתוח את הפרוייקט בIIS המקומי, וכך תקבלו גם את אפשרויות הdebug של Visual Studio וגם את ממשקי הניהול של IISS.

כמובן קודם יש לוודא שURL Rewrite 2.0 אכן מותקן בשרת האחסון שלכם.