Migration Issues
Migration Issues
Sometimes when copying databases, for instance from production down to development, the user accounts become dysfunctional. The database login no longer matches the overall server login. To re-synchronize them use:
exec sp_change_users_login auto_fix, 'someusername'
Now that Vista has gone RTM, lots of us are trying to decide when to upgrade. Here is something I have not seen much mention of:
Visual Studio 2003 is not supported in Vista.
This is going to hold back adoption on my work machine. I'm a consultant, I have to do work on whatever platform the client is using, which unfortunately is VS 2003 sometimes. Is VS 2003 going to be relegated to life in a virtual machine from now on?
Today we are migrating my project from Whidbey Beta1 to Beta 2 (if you know why this is happening today, go ahead and laugh with us), and our biggest hurdle so far is Typed Datasets.
We used dozens of XSD schemas to create typed datasets. Of course, Beta 2 no longer gives a menu option for creating a typed dataset directly from a schema (see this post in the Fedback Center for Visual Studio). The prospect of converting to the Dataset type of schema was daunting, and moving to the command line for xsd.exe was not going over so well either.
We did discover, however, that you can still generate the dataset using the IDE. In the properties of the XSD file, you can set the custom tool to MSDataSetGenerator. Then you can right-click the XSD file and choose Run Custom Tool. The IDE generates the code for the dataset. Whew!
I found out the hard way that when using the trace on an ASP.Net page, the interaction of the trace observer is not totally separate from the page. I am working on a page where performance is a big issue, so I have been using Trace extensively to find the weak spots and shore them up. Essentially the page is searching both the Indexing Service and an Oracle database and finding where the two datasets intersect. Because the results are a grid of links, the user can click the links and then use the Back button to return to the results. Of course I was getting the dreaded “Warning: Page has Expired” message after using the back button. The page with the grid is the result of an HTTP post, so the post has expired. Clicking the Refresh button was the only way to get the data back initially. This is bad, as the page already was having performance problems, so rendering it again was not really an option. So I tried setting the page caching options to this:
<%@ OutputCache Duration="120" VaryByParam="None" Location="Client"%>
This should cause the page to cache on the client. No dice. No matter what I set, the http_cache_control header always reported “no-cache” in the trace results. I tried making the header changes in code. Again, no change. Still reporting “no-cache”. The simple solution to the problem is to turn off tracing on the page. Well, it turns out that with Trace=”true” in the @Page directive, the http_cache_control header is set to “no-cache” no matter what. When the Tracing is off, the “Warning: Page has Expired” message disappears and the page is back in control of setting the headers.
Also, a related tidbit I discovered when trying to solve this is that the page timeout is essentially set to infinity when compiling in debug mode.
As I am continuing to migrate our ASP app to ASP.Net, it has finally come time to address the Indexing Service search. It's a big feature in the application, and the transition of this piece needs to be seamless. We want to stick with Ixsso for the Indexing Service as opposed to using the Oledb driver. Ixsso is considered to be the faster of the two technologies, even using COM interop (See various of Hilary Cotter's comments in
microsoft.public.inetserver.indexserver). Code snippets for using Ixsso with ASP.Net are pretty sparse compared to using Oledb, so I figured I should post mine. First, I used the IDE to create a reference to the ixsso Control Library dll and let the IDE make the .Net wrapper for the COM object (christened Cisso by the IDE).
Imports Cisso
Imports System.Security.Principal
Imports System.Data.OleDb
Private Function GetIndexResults(ByVal Query As String) As DataTable
Dim Q As New CissoQueryClass
Dim util As CissoUtilClass
Dim da As New OleDbDataAdapter
Dim ds As New DataSet("IndexServerResults")
Q.Query = Query
Q.SortBy = "rank[d]"
Q.Columns = "filename, rank, write"
Q.Catalog = "query://DocumentServer/Resumes"
Q.MaxRecords = 1000
util.AddScopeToQuery(Q, "\", "deep")
Q.LocaleID = util.ISOToLocaleID("EN-US")
Dim impContext As WindowsImpersonationContext = impersonateAnonymous()
da.Fill(ds, Q.CreateRecordset("nonsequential"), "IndexServerResults")
Q = Nothing
util = Nothing
impContext.Undo()
Return myDS.Tables("IndexServerResults")
End Function
The impersonateAnonymous function is described in a previous post of mine. In our case the anonymous user on the machine has appropriate privledges to query the remote Indexing Service, but the ASP.Net worker process does not so impersonation is in order for the function. That part is probably optional depending on the situation. The rest of it is not very tricky. I tried to fill the DataTable directly without the DataAdapter, but that didn't work. The CreateRecordset function of the CissoQueryClass returns an ADO recordset and I couldn't find a cast that worked. The DataAdapter seems to be doing the casting work during the call to Fill.
I am in a situation where my application is mixed classic ASP and ASP.Net. I am currently migrating the classic ASP app page-by-page into ASPX as I am developing new functionality. When I have to touch an old page, I convert it.
In an attempt to make them appear to be one app to the user, I need to manage the timeout of the two applications to appear as one. The classic ASP app was using Session variables for this, but obviously that would not work easily in a mixed environment. Luckily, the ASP app is not heavily dependent on Session variables; it only uses a few that are set during login, and a couple on particular pages. So after logging in the login redirects to a classic ASP page to set the few necessary Session variables, and that page redirects to the home page.
- Login using the ASPX login page
- Create a cookie containing context
- Redirect to asp page setting session variables
- Redirect to ASPX home page
Initially, I thought it would be easy to manage the timeouts, just use the cookie generated by the ASPX login page. The classic ASP pages can check the cookie and update its expiration if needed. Of course, it was not that simple. The cookie that is created by the ASPX pages contains the authentication ticket generated by the current http context. Its expiration is essentially the same as the cookie, based on the time set in web.config. The problem lies with the classic ASP pages. They can update the cookie’s expiration, but not the ticket. If a user spends too much time using classic ASP pages, the ticket in the cookie will be expired next time the user visits an ASPX page, even if the cookie is good. The ASPX pages consider a user timed out if either the cookie has expired or the ticket has expired. They will need to login again without actually timing out simply due to using classic ASP pages for a little too long.
My solution results from the ratio of classic ASP to ASPX pages. Right now it is about 5 classic ASP per ASPX page. Using this ratio I changed the authentication ticket timeout to be 5 times as long as the cookie timeout. In order to make this work the ASPX application needs to manage the cookie created by ASPX pages instead of allowing it to occur manually. At the login page, I allow the ASPX cookie to be created as normal using my own CreateAuthCookie.
Public Shared Function CreateAuthCookie(ByVal UserID As String) As HttpCookie
Const TicketLife As Double = 4
Dim Timeout As String = ConfigurationSettings.AppSettings("Timeout").ToString
Dim principalText As String
Dim buffer As New IO.MemoryStream
Dim formatter As New Runtime.Serialization.Formatters.Binary.BinaryFormatter formatter.Serialize(buffer, HttpContext.Current.User)
buffer.Position = 0
principalText = Convert.ToBase64String(buffer.GetBuffer())
'create a forms ticket
Dim ticket As New FormsAuthenticationTicket(1, HttpContext.Current.User.Identity.Name, _
DateTime.Now, DateTime.Now.AddMinutes(Double.Parse(Timeout) * TicketLife), _
False, principalText)
'Encrypt the ticket
Dim encTicket As String = FormsAuthentication.Encrypt(ticket)
'This is the cookie used by both ASP and ASP.Net, ASP never retrieves the value
Dim TheCookie As New HttpCookie(FormsAuthentication.FormsCookieName)
TheCookie.Path = FormsAuthentication.FormsCookiePath
TheCookie.Value = encTicket
TheCookie.Expires = DateTime.Now.AddMinutes(Double.Parse(Timeout))
Return TheCookie
End Function
In the Global.asax, I added code to take the cookie, get the ticket from it and use the ticket to set the HttpContext.Current.User and update its expiration if necessary. If the cookie or the ticket has expired here it will automatically redirect to the login page as a function of ASP.Net.
Private Sub Global_AcquireRequestState(ByVal sender As Object, ByVal e As EventArgs) _
Handles MyBase.AcquireRequestState
Dim cookie As HttpCookie = Request.Cookies.Get(FormsAuthentication.FormsCookieName)
If Not cookie Is Nothing Then
'Cookie found, decrypt the value and recreate the authentication ticket
' and user context for the page
Dim ticket As FormsAuthenticationTicket
Try
If cookie.Value = "" Then
'For some reason the cookie exists but there is no value,
' remove the cookie try again
cookie.Expires = DateTime.Now.AddMinutes(-15)
Response.Redirect("../login/login.aspx", False)
Else
ticket = FormsAuthentication.Decrypt(cookie.Value)
End If
FormsAuthentication.RenewTicketIfOld(ticket)
Dim buffer As New IO.MemoryStream(Convert.FromBase64String(ticket.UserData))
Dim formatter As New Runtime.Serialization.Formatters.Binary.BinaryFormatter
HttpContext.Current.User = CType(formatter.Deserialize(buffer), IPrincipal)
Catch ex As Exception
'Could not set the ticket, remove the cookie
cookie.Expires = DateTime.Now.AddMinutes(-15)
Response.Redirect("../login/login.aspx", False)
End Try
End If
End Sub
All my ASPX pages inherit from my own base page instead of System.Web.UI.Page. In the PreRender event for my base page my CreateAuthCookie gets set again and added to the Response. My base page has already taken the UserID from the HttpContext and set it as a page property.
Private Sub Page_PreRender(ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles MyBase.PreRender
Dim AuthCookie As HttpCookie = CreateAuthCookie(Me.User_ID.ToString)
Response.Cookies.Add(AuthCookie)
End Sub
The classic ASP pages use an include file that just increments the cookie timeout. It also checks the status of the ASP session and resets it if the cookie is still good.
Dim xQString if Request.QueryString <> "" then
xQString = "?" & Request.QueryString
end if
If Request.Cookies("EnCoreUser") = "" then
'No cookie, session expired
Session.Abandon
Response.Cookies("EnCoreUser").Expires = DateAdd("n", -15, Now())
response.redirect "../login/login.aspx?ReturnUrl=" & _
Server.URLEncode(Request.ServerVariables("URL") & xQString)
else
'Got aspx cookie, check asp session
if Session("SessionId") <> Session.SessionID then
if Request.Cookies("asp") = "" then
response.redirect "../login/login.aspx?ReturnUrl=" & _
Server.URLEncode(Request.ServerVariables("URL") & xQString)
else
'Cookie is good but classic ASP session ended. Recreate the
' session without forcing another login.
Response.Redirect "../login/verify.asp?userid=" & user_id & "&url=" & _
Server.URLEncode(Request.ServerVariables("URL") & xQString)
end if
else
'All is well, so reset the cookie
Dim sTempx sTempx = CStr(Request.Cookies("EnCoreUser"))
Response.Cookies("EnCoreUser").Path = "/"
Response.Cookies("EnCoreUser").Expires = DateAdd("n", 35, Now())
Response.Cookies("EnCoreUser") = sTempx
end if
end if
As the ratio classic ASP pages to ASPX pages changes, all I need to do is reduce the multiplier to shorten the ticket length accordingly. Eventually, of course, all this work will get tossed out when there are no more classic ASP pages.
There a fascinating discussion going on at Tim Sneath's blog about the functionality of the business layer moving to compiled stored procedures in Yukon. At least in my project, we have been doing that very thing and struggled with the n-Tier issue as a result. In our case, the database is Oracle 8i, and the application is/was ASP, which we are now converting to .Net. We have some very complex business logic encapsulated in PL/SQL stored procedures. There were two concrete reasons for the decision:
- The amount of data that would need to be transferred was huge.
- Oracle is more efficient than ASP code at many of the operations involved.
With ADO.Net, some of the ineffieciency may be addressed, but the database is designed for efficiency at certain operations that ADO.Net won't be able to match.
We did not need to approach this from a scalability issue, as the app receives hundreds of hits per hour, not thousands or more. I still think it will scale OK as your business objects will have much shorter life spans and you can still pool and queue database connection objects if needed due to long-running stored procedures.
I read about the Data Application Block yesterday and was intrigued, although for the life of me I cannot remember where I read about it originally to pass along some credit. So I checked it out and it is very nice and all, but alas my project uses Oracle not SQL Server. Initially I thought about porting it to Oracle, but I figured that it has already been done. So I did a little Googling and voila!, Microsoft themselves had already done the work in the Nile 3.0 demo application. Sadly, it was in C# and my current project is using Oracle and VB.Net. So now I am back to porting.
We are migrating an ASP app to ASP.Net, but not all at one time as my business customers within the company won't pay for that kind of time all at once. So we are migrating piece-by-piece as we develop new functions within the app. Our plan is to develop all new pages using .Net, and convert classic ASP pages as we have to touch them due to new development. To get the remaining pages converted, we are just going to squeeze them in as often as possible along with the new development, mostly on a one or two at a time basis. So probably of the next 6-12 months the ASP and ASP.Net applications will coexist and appear to the users to be one application.
Right now, we have done what we considered the basic conversion necessary to implement our plan:
- Convert the login process to .Net. We are using Forms authentication since users are checked from our own database. We may migrate this to LDAP sometime in the future. My company uses Novell Netware for network login and GroupWise for email. I would love to hear from anyone who has a web app that can create appointments in GroupWise!!
- Move frequently used Session variables to cookies. Luckily, we were not using lots of Session variables in the classic ASP. We encrypt all the data written to the cookies so spoofing is harder. We are currently not a web farm, but that is also in our future so the Session variables need to go anyway.
- Create a common time-out scheme between the applications using a cookie. The ASP app times out after 35 minutes of inactivity for what the HR folks call “security“, as the app contains lots of personal information about both employees and non-employees.
- Convert common functions to .Net (like checking user roles, encryption, database access, search engine, etc.)
- Convert ASP includes for page structure into .ascx files.
Of course, none of this has gone into production yet (sigh...). It is on the test sever and hopefully will roll out by the end of the month.