Friday, March 31, 2006

Sending a file to the client

Sometimes you need to send a file to the client by them clicking a link. If it is stored on your hard drive (appologies for the bad formatting. busy day today):

private sub SendFile(fileName as string)
dim filepath as string = Server.MapPath("files") & fileName

filepath = Server.UrlPathEncode(filepath) 'encoded to remove special characters i.e. spaces

response.clear() 'clear the output buffer

Response.ContentType = "application/binary" 'binaries are almost always downloaded. If it was set to excel for an excel file the browser might try to open it in the browser window

Response.AddHeader("Content-Disposition", "attachment;filename=" + fileName) 'This is what tells the browser the filename to prompt the user with when they save it

Response.TransmitFile(filePath) 'transmit the file (internally reads and sends the file to the client

Response.Flush() 'write everything in the buffer to the client

Response.End() 'end the response so nothing else gets sent to the browser

end sub


Often you have these files in an area outside of the web root or in a database. If you have this scenario you can use a

Response.BinaryWrite(bytearray)

to send a bytearray to the client. You can easily get a byte array by reading a file with a filestream and then copying that to a byte array. Wish I had time to post on that today but sadly I do not.

Thursday, March 30, 2006

Canadian Law And The Web Part 2: Copyright

Copyright Basics
Copyright is the right granted to an individual to protect a peice of work he/she has created. This applies to literary, dramatic, musical, and artistic works.

Copyright is automatic. Meaning that once you have created a work it is already protected by copyright and you have rights to defend that work from being copied.

Copyright protection does not extent to ideas, facts, or information. Meaning that if I came up with an idea for an invention and even wrote it down. It does not prevent someone else from creating that idea. The only thing they can not do is copy what I wrote down. Patents can be used to protect these kinds of ideas (but that is outside the scope of this document).

An interesting thing I discovered is that lectures count as literary works and are also protected. Lecutures includes addresses, speaches, and sermons. I know this really does not apply but just thought it to be interesting.

Copyright lasts. A copyrighted work maintains its copyright 25 years after the author dies. The copyright is passed as part of the estate.

Ownership
In essence the person who created a work is its author and therefore owns the copyright. This is not necessarily true when you are in the employ of a contract of another person

Section 13(3) where the author of a work was in the employment of some other person under a contract of service or apprenticeship and the work was made in the course of his emplyoment by that person, the person by whom the auther was employed shall, in the absence of any agreement to the contrary, be the first owner of the copyright.

There are exclusions though for newspaper, magazine, or similar periodical contributions in this section but that does not apply for us.

If you are in the hire of someone and do work the person who hired you holds the copyright!

Granting Rights to Others
As the owner of a copyright you can grant rights for other people to use, perform, create derivitive works, or full rights. These agreements must be in writting to make them binding. By granting someone the right to use or perform your work they can replicate your work in its entirety. Derivitive works give the right for the licencee to modify the original and re-use it. Full or Exclusive rights give the licencee full rights to the work and can do whatever they want and relicence it to others if they so choose.

What This Means
-If you are hired by someone to do work they own the copyright on the work you performed for them.
-If you take content (be it text or images) from another person without their written permission and place it on a site you are commiting a copyright infringment.
-If you design something on your own (not hired by someone else) and a client wishes to use it. You should sell them a license to use your work.
-If you grant someone a licence to use your work (but not make derivitive works) and they modify it, they have commited an infringement.
-Using music or images on a site that you do not have written permission to use is an infringement.
-If you built something for a client that has hired you and then find another client that needs the same thing. You can not sell them the item you produced without written consent from the original client you did the work for (as they hold the copyright).

How To Protect Yourself as a Developer
-Ensure that you have rights to all of the works you are using in your project.
-Have a contract with your client that outlines who owns what.
-In your contract have a clause that ensures both parties are contributing works that they have rights to.
-I personally like the idea of stating in my contracts for projects that I am the owner of the copyright and the client has been granted a license to use the website. I also like to grant them the right to make derivitive works on the item in case they want to make changes in the future. This way they can go to another developer and have it done in case we had a falling out.
-The best way is to consult with a lawyer. From my reading most laws for the brick and mortar world apply to the internet world.
-If hiring subcontractors to do work ensure that the subcontractors only use works that they have rights to.
-Make sure that you have rights to the work that the subcontractors do. I ensure that I have at least the right to make derivitive works on everything they send me.

Questions
Q. Who owns source files, psd's, fla's, etc.?
A. If you have hired someone to do the work then you have full rights to those works as you have paid for them to be created.

Q. How can I verify that I am not infringing on someone elses copyright?
A. There is no real way to do this that I know of. I would do a search on the interenet for content that I am using on a project and make sure that I can not find it. Usually if you infringe on someones copyright then they will notify you and ask you to change / remove the offending matterial. This is the best case (who wants to go to court anyways?)

If you have any more please send them.

DISCLAIMER: I am not even close to a lawyer. This is not legal advice. Consult with a lawyer in any matter you have about law.

Wednesday, March 29, 2006

Canadian Law And The Web Part 1: Accessability

I have a mild interest in law and how it affects me. I have been working with technology for most of my life and there is still not much I can find about how the law relates to what we do in everyday cases. In a series of posts I am going to make I will talk about some of them.

DISCLAIMER: I am not a lawyer and the following guides are for information only. They should not be treated as legal advice. Contact a lawyer for any legal questions you might have. Don't play with fire. It is hot. You might get burned. Yada yada yada.

Accessability:

From what I could find there is no laws about enabling your website for disabled persons. You might think of this as a good thing but just becuase there is not a law specifically for something means that another one can be adapted to it.

For instance:
Section 15 of the Canadian Charters of Rights and Freedom(http://laws.justice.gc.ca/en/charter/) state that "every individual is equal before the law"
Section 5 of the Canadian Human Rights Act state that access to goods or services can not be denied on the basis of descrimination

The Human Rights Act also states that descrimination is unacceptable wether it is intentional or not.

Basically this sums up to: "ignorance is not an excuse" and "brick and mortar law" could be applied to the the internet in this case. I am going to recommend that you try to ensure that you make websites as available as possible to the public.

Here is the W3s guide on website accessability: http://www.w3.org/TR/WAI-WEBCONTENT/

Honestly, reading through the guide has a lot of helpfull tips even for designing a website for the general public as well and it does not take that much extra time to develop with.

(most information for this article was found here: http://www.zvulony.com/accessibility.html)

Determining if a session has expired

The first thing to discuss is Session.IsNewSession() this returns true if a session was just created on this request.

On the first request to a page this will return true.
durring the session it will return false
after a timeout error occurs this will also return true (the session no longer exists so it gets recreated).

So we know that if IsNewSession() returns true that either this is a new request or the session has timed out.

Now the way that sessions work is the server sends the browser a cookie with the session ID. On every request the client sends this cookie and the server loads your session.

On the first request to a page the browser will not send a cookie
durring the session it will send the cookie
after a timout occurs it will still send the cookie

So now we can determine if we have a timeout by using this code:

If Session.IsNewSession Then
Dim strCookieHeader As String = Request.Headers("Cookie")
If strCookieHeader.Length > 0 AndAlso strCookieHeader.IndexOf("ASP.NET_SessionId") > 0 Then
'redirect to timeout page here
end if

I put this code into a basepage that all of my pages that use session data inherit from.

Why not to depend on client side validation

A great example on why not to depend on client side validation only:
http://thedailywtf.com/forums/65974/ShowPost.aspx

GUIDs and Databases

For those of you who don't know what a GUID is it is a 128 bit value that is "Unique across space and time". A GUID looks like this: B2658C9D-A76G-4D72-B0E4-B732332408D6. There garanteed uniqueness has been used by the com+ system in windows for years. This is why two com components with the same name can be installed without conflicting (they each have a GUID that keeps them seperate).

Developers seem to be pessimistic about GUID collissions in their systems. I just have to say that it will not happen. As I said com+ has been using this forever and I have never heard of a GUID collission ever. GUID itself stands for Globally Unique IDentifier. i.e. accrosss the globe no one else has the same GUID.

Now onto why these are usefull in databases:

1. Merging of data
If you have 2 databases with the same tables but are currently seperated (i.e. an identical sales database in 2 seperate offices). If you want to combine these into a data warehouse you will have issues with numerical identies colliding. If you used a GUID then there would be no conflict.
If you are using replication then GUIIDS are your friends for this same reason

2. Security
If you look at this query string
editpage.aspx?userId=23526
I would guess that chaning that userId up or down one would allow me to be another person on the system. Granted it is bad design to expose this information to the user (hide it in session state instead so the client never sees it). But as I always say security should be layered. If we used a GUID for our userIds we would have a query string like this:
editpage.aspx?userId=B2658C9D-A76G-4D72-B0E4-B732332408D6
By its very deffinition it would be impossibly for anyone to brute force this (they could never generate another GUID that matches one in our system because they are globally unique).
3. Disconnected Data
Typically when creating data we create the records, insert it, then read back the numerical key that sql has created. By using a GUID we can set our key in data to be that GUID and fire it off to the database knowing for sure that no other ID like it will exist in the datbase.
4. Integer Data Space
You can run out of integers in a database (I can't remember the limit) for IDs but by using GUIDs you can have limitless number of IDs without fear of exhaustinga limit.


The Cons
Speed: It takes time to generate a number that is that unique. This takes CPU cycles
Readability: It is easier to read and type a query like select * from table where ID=30 than select * from table where ID='B2658C9D-A76G-4D72-B0E4-B732332408D6'
Space: GUIDs are larger than integers (4 times larger) and take up more space. I am of the adage that storage is cheap now so unless you are going to have a massive massive massive database this does not concern me that much
Page Splits: Due to the size of GUIDs they can contribute to page splits in SQL
Searching and Comparison: It takes a bit longer to compare two guids when searching that it does an integer

Monday, March 27, 2006

Removing while iterating through a collection

Often times developers want to loop through a collection and filter out items. Unfortunatley they get the error that they can not change the contents while enumerating it. There are 2 solutions to this problem

1. Copy the records to be removed to an array or collection of some sort and then remove them in a second loop

2. Run it through a for loop backwards and remove the entries as needed

Dim refRow As dataRow
For i As Integer = ds.tables(0).Rows.Count - 1 To 0 Step -1
If ds.tables(0).rows(i).item("refundeeId") = refundeeId Then
ds.tables(0).Rows.RemoveAt(i)
End If
Next


If we ran the loop forward instead of backwards and removed 10 out of 100 items when we got to the 90th iteration of the loop it would crash. This is because the size of the table has been reduced to 90 instead of 100 so when we try to access ds.tables(0).rows(91) it actually no longer exists.

By going backwards we are counting down and removing items. So if we removed 10 out of 100 again our table has only 90 records in it BUT we are now accessing ds.tables(0).rows(9) which still exists instead of try ds.tables(0).rows(91) which does not.

Hope that makes sense! :)

Tuesday, March 21, 2006

Javascript rounding weirdness

I have this in code:

var total = parseFloat(txtGLItemAmount1.value) + parseFloat(txtGLItemAmount2.value)

when I insert 110.16 and 7.74 I should get 117.90 but instead get 117.8999999999

Pretty freakin weird! I assume this has something to do with floating point math and all its weirdness. The fix I found for this was this line:

total = total.toFixed(2);

This yeilds the correct value. I bet that I could do this as well to get it correct:

var total = txtGLItemAmount1.value.toFixed(2) + txtGLItemAmount2.value.toFixed(2);

Friday, March 17, 2006

Locking

One thing that you have to worry about in a multithreaded application is the problem of concurrency in that you do not want two seperate threads acessing a value as it is changing. .NET has a method to solve this and it is called locking.

In c# it is Lock
vb it is SyncLock

public property GetId() as integer
get
syncLock me
return _id
end syncLocl
end get
set (value as integer)
syncLock me
_id = value
end synclock
end set
end property

by calling syncLock me we lock the current object from change. This might not be what is desired so finer grained locking can be done.

locks work that only one thread can have a lock on an object.
if the object is already locked a calling thread will have to wait until the lock is released.
A single thread is allowed to acquire the same lock on an object unlimited times

Controls and Threading

Controls are not thread safe! you should never try to access a control or modify it from a different thread than the one the control exists on. If you do strange results will happen.

Fortunately controls have two methods that help us overcome this issue with ease.
control.invoke() to do simple tasks and control.beginInvoke() for longer running tasks that we want to have happen on a background thread.

Here is a simple example of using beginInvoke()

dim params() as object = {me, System.EventArgs.Empty}
control.BeginInvoke(new System.EventHandler(Addressof UpdateUi), params)

private sub UpdateUi(sender as object, e as eventArgs)
{
lblStatus.text = "Finished"
}

as you can see we pass in an array of parameters. In this case the sender and eventargs.
You could also use custom event args to accomplish this:

MyProgressEvent e = new MyProgressEvent(msg, percentDone);
object[] pList = { this, e };

private void UpdateUI(object sender, MyProgressEvents e) {
lblStatus.Text = e.Msg;
myProgressControl.Value = e.PercentDone;
}

Sometimes in our code we will not know if we are on another thread or not. Thankfully again there is a method that lets us know that as well. It is control.InvokeRequired.
It will return false if the calling thread is the thread the control exists on. It will return true when the caller is on another thread.

Monday, March 06, 2006

Objects make your code expandable

thats pretty much it. I am a big fan of oop and am working on a project that is not. It now takes me so much longer to do anything and it has a huge impact on the code base and its consumers.

I have one method like this:

CreateCashout(clerkId as integer, officeId as integer, cashTotal as double, creditCardTotal as double, debitTotal as double, PaymentIds() as integer)

This is then validated and passed to the same method signature in the data layer.

So for one I have a long method signature. Some of these items will be zero (i.e. debit totals for some consumers) so why make them put it in at all.

Also I would like to see what payments were cash, credit card, and debit but right now I just know all the payments by the paymentId array.

I would create an object like this:

1 Public Class Cashout

2

3 Private mClerkId As Integer

4 Private mOfficeId As Integer

5 Private mCashTotal As Double

6 Private mCreditCardTotal As Double

7 Private mDebitTotal As Double

8

9 Private mCashPaymentIds() As Integer

10 Private mCreditCardPaymentIds() As Integer

11 Private mDebitPaymentIds() As Integer

12

13 'properties for accessing ClerkId, OfficeId, CashTotal, CreditCardTotal, and DebitTotal

14

15 Public ReadOnly Property PaymentIds() As Integer()

16 Get

17 'just an example. I have not tested this

18 Dim allIds(mCashPaymentIds.Length + mCreditCardPaymentIds.Length + mDebitPaymentIds.Length) As Integer

19 mCashPaymentIds.CopyTo(allIds, 0)

20 mCreditCardPaymentIds.CopyTo(allIds, mCashPaymentIds.Length)

21 mDebitPaymentIds.CopyTo(allIds, mCashPaymentIds.Length + mCreditCardPaymentIds.Length)

22 Return allIds

23 End Get

24 End Property

25

26 Public Sub New(ByVal clerkId As Integer, ByVal officeId As Integer, ByVal cashTotal As Double, ByVal creditCardTotal As Double, ByVal debitTotal As Double)

27 Me.ClerkId = clerkId

28 Me.OfficeId = officeId

29 Me.CashTotal = cashTotal

30 Me.CreditCardTotal = creditCardTotal

31 Me.DebitTotal = debitTotal

32 End Sub

33

34 Public Sub New(ByVal clerkId As Integer, ByVal officeId As Integer)

35 Me.ClerkId = clerkId

36 Me.OfficeId = officeId

37 End Sub

38 End Class


And the implementation for just creating a cash cashout would now look like this:

1 Dim cashout As New Cashout(clerkId, officeId)

2 cashout.cashTotal = 30.00

3 cashout.CashPaymentIds = cashPaymentIds

4 CreateCashout(cashout)


Notice how I now enforced that at least the clerkId and OfficeId are mandatory by having it in both New constructors.

Also note that in the new constructor that I assign the parameters to properties and not to the fields. i.e. I assigne cashTotals to me.CashTotals instead of mCashTotals

The reason for this is that there is now a central point to validate all data if need by. i.e. lets say that CashTotals can not be negative we would put the check in the property and be done with it. If we directly assigned it to mCashoutId in the new constructor we would have to check once in the new constructor and once in the property as there are now two ways to set the value of mCashTotals.

Saturday, March 04, 2006

SQL Administration

I run a small hosting provider and one of my issues is backing up SQL databases. Everytime I create a database I have to create a script to back them up. Here is a handy script that I found and modified a bit that will backup all databases to a specified folder. It also keeps a retention history for a user defineable number of days

here is a sample usage:
isp_FullBackup_UserDBs 'c:\backups', 10

this will backup all user databases (note it wil not backup the master database) and keep a history of 10 days.




----------------------------------------------------------------------------------------------------
-- OBJECT NAME : isp_FullBackup_UserDBs
--
-- AUTHOR : Tara Duggan
-- DATE : December 18, 2003
--
-- INPUTS : @Path - location of the backups
@HistoryDays - Number of days back to keep

-- OUTPUTS : None
-- DEPENDENCIES : None
--
-- DESCRIPTION : This stored procedure performs a full backup on all of the user databases
--
-- EXAMPLES (optional) : EXEC isp_FullBackup_UserDBs @Path = 'C:\MSSQL\Backup\', @HistoryDays=14
--
-- MODIFICATION HISTORY :
-- Jan 3, 2005: David Woods - Added the @HistoryDays filter to allow a user specified time period of backups
-- Feb 2, 2005: David Woods - Fixed a bug that would not backup databases with dashes ('-') in the name.

----------------------------------------------------------------------------------------------------
--
----------------------------------------------------------------------------------------------------
CREATE PROC isp_FullBackup_UserDBs
@Path VARCHAR(100),
@HistoryDays int --number of days to hold the database
AS

SET NOCOUNT ON

DECLARE @Now CHAR(14) -- current date in the form of yyyymmddhhmmss
DECLARE @DBName SYSNAME -- stores the database name that is currently being processed
DECLARE @SQL VARCHAR(7000) -- stores the dynamically created xp_backup_database command
DECLARE @cmd SYSNAME -- stores the dynamically created DOS command
DECLARE @Result INT -- stores the result of the dir DOS command
DECLARE @RowCnt INT -- stores @@ROWCOUNT

--make sure our path has a trailing slash otherwise we will have a mess of prefixed directories
if SUBSTRING(@path, len(@path), 1) != '\'
Set @Path = @Path + '\'

-- Get the list of the databases to be backed up, does not include master, model, msdb, tempdb, Northwind, or pubs
SELECT name
INTO #WhichDatabase
FROM master.dbo.sysdatabases
WHERE name NOT IN ('master', 'model', 'msdb', 'pubs', 'tempdb', 'Northwind')
ORDER BY name

-- Get the database to be backed up
SELECT TOP 1 @DBName = name
FROM #WhichDatabase

SET @RowCnt = @@ROWCOUNT

-- Iterate throught the temp table until no more databases need to be backed up
WHILE @RowCnt <> 0
BEGIN

-- Get the current date using style 120, remove all dashes, spaces, and colons
SELECT @Now = REPLACE(REPLACE(REPLACE(CONVERT(VARCHAR(50), GETDATE(), 120), '-', ''), ' ', ''), ':', '')

-- Build the dir command that will check to see if the directory exists
SELECT @cmd = 'dir ' + @Path + @DBName

-- Run the dir command, put output of xp_cmdshell into @result
EXEC @result = master.dbo.xp_cmdshell @cmd

-- If the directory does not exist, we must create it
IF @result <> 0
BEGIN

-- Build the mkdir command
SELECT @cmd = 'mkdir ' + @Path + @DBName

-- Create the directory
EXEC master.dbo.xp_cmdshell @cmd, NO_OUTPUT

END
-- The directory exists, so let's delete files older than two days
ELSE
BEGIN

-- Stores the name of the file to be deleted
DECLARE @WhichFile VARCHAR(1000)

CREATE TABLE #DeleteOldFiles
(
DirInfo VARCHAR(7000)
)

-- Build the command that will list out all of the files in a directory
SELECT @cmd = 'dir ' + @Path + @DBName + ' /OD'

-- Run the dir command and put the results into a temp table
INSERT INTO #DeleteOldFiles
EXEC master.dbo.xp_cmdshell @cmd

-- Delete all rows from the temp table except the ones that correspond to the files to be deleted
DELETE
FROM #DeleteOldFiles
WHERE ISDATE(SUBSTRING(DirInfo, 1, 10)) = 0 OR DirInfo LIKE '%<dir>%' OR SUBSTRING(DirInfo, 1, 10) >= GETDATE() - @HistoryDays

-- Get the file name portion of the row that corresponds to the file to be deleted
SELECT TOP 1 @WhichFile = SUBSTRING(DirInfo, LEN(DirInfo) - PATINDEX('% %', REVERSE(DirInfo)) + 2, LEN(DirInfo))
FROM #DeleteOldFiles

SET @RowCnt = @@ROWCOUNT

-- Interate through the temp table until there are no more files to delete
WHILE @RowCnt <> 0
BEGIN

-- Build the del command
SELECT @cmd = 'del ' + @Path + + @DBName + '\' + @WhichFile + ' /Q /F'

-- Delete the file
EXEC master.dbo.xp_cmdshell @cmd, NO_OUTPUT

-- To move to the next file, the current file name needs to be deleted from the temp table
DELETE
FROM #DeleteOldFiles
WHERE SUBSTRING(DirInfo, LEN(DirInfo) - PATINDEX('% %', REVERSE(DirInfo)) + 2, LEN(DirInfo)) = @WhichFile

-- Get the file name portion of the row that corresponds to the file to be deleted
SELECT TOP 1 @WhichFile = SUBSTRING(DirInfo, LEN(DirInfo) - PATINDEX('% %', REVERSE(DirInfo)) + 2, LEN(DirInfo))
FROM #DeleteOldFiles

SET @RowCnt = @@ROWCOUNT

END

DROP TABLE #DeleteOldFiles

END

-- Build the xp_backup_database command dynamically
SELECT @SQL = ''
SELECT @SQL = @SQL + 'BACKUP DATABASE [' + @DBName + ']' + CHAR(10)
SELECT @SQL = @SQL + 'TO DISK = ''' + @Path + @DBName + '\' + @DBName + '_' + @Now + '.BAK''' + CHAR(10)
SELECT @SQL = @SQL + 'WITH INIT ' + CHAR(10)
print @SQL
-- Backup the database using xp_backup_database
EXEC (@SQL)

-- To move onto the next database, the current database name needs to be deleted from the temp table
DELETE
FROM #WhichDatabase
WHERE name = @DBName

-- Get the database to be backed up
SELECT TOP 1 @DBName = name
FROM #WhichDatabase

SET @RowCnt = @@ROWCOUNT

-- Let the system rest for 5 seconds before starting on the next backup
WAITFOR DELAY '00:00:05'

END

DROP TABLE #WhichDatabase

SET NOCOUNT OFF

RETURN

GO

Friday, March 03, 2006

Security Rant

Just a quick note on security while it is in my head

1. Use an accept list instead of a deny list.
i.e. use a regular expression that matches [A-Za-z0-9]
vs. ![/*.()<>\......]

if you miss one character then your validation is useless. The first validation allows only alphanumeric characters. All else are excluded by the rule.

Microsoft had this issue with IIS 5 (I beleive) in that people were exploiting it by using the urlencoded values to do directory transversal i.e. www.victim.com/%2c%2c/%2c%2c/%2c%2c/c:\windows\system32\command\cmd.exe
(now that is from memory so don't shoot me)

If the processor only accepted .. instead of %2c things would have been good

(note that having %2c is valid so it should have been decoded to a . before it was validated instead of after but that would ruin my example)

2. Fail closed!
I can not stress this enough. If something goes wrong... shut down! fail! throw a billion exceptions.

My best example is a firewall. If an unexpected action occured in the firewall what should be done:
1. Crash and leave all ports open
2. Crash and close all ports cutting off any legitimate services

Ok one impacts people connecting but it SHOULD! they will tell you and then you know there is an issue and you can fix it. By failing open in this case you might not know for months that your firewall is not working as no one has complained.

Thursday, March 02, 2006

Throwing Exceptions Part II

Sometimes you need to change an exceptions type (usually to a custom exception type) but still want to maintain stacktrace information (read about this issue in part1). Well it is really easy just look at this example

1

2 Public Sub Layer2()

3 Throw New NotImplementedException

4 End Sub

5

6 Public Sub LogError(ByVal ex As Exception)

7 'TODO: log the error

8 End Sub

9

10 Private Sub Layer1()

11 Try

12 Layer2()

13 Catch ex As Exception

14 Throw New CustomException("Layer1 error", ex)

15 End Try

16 End Sub

17

18 Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

19 Try

20 Layer1()

21 Catch ex As Exception

22 LogError(ex)

23 Throw

24 End Try

25 End Sub


By throwing a new exception and adding the caught exception as the inner exception we maintain the stacktrace information.

FYI my stack output looks like this:

System.Exception: Layer1 error ---> System.NotImplementedException: The method or operation is not implemented.
at WindowsApplication1.Form1.Layer2() in C:\...\Form1.vb:line 3
at WindowsApplication1.Form1.Layer1() in C:\...\Form1.vb:line 12
--- End of inner exception stack trace ---
at WindowsApplication1.Form1.Layer1() in C:\..\Form1.vb:line 14
at WindowsApplication1.Form1.Button1_Click(Object sender, EventArgs e) in C:\..\Form1.vb:line 20


you can see that the inner exception has all the trace information down to where the original exception occured but the outer exception shows only that at Layer1() did an exception occur.

Throwing Exceptions

Don't do this:

1 Public Sub DoTask()

2 Throw New NotImplementedException

3 End Sub

4

5 Public Sub LogError(ByVal ex As Exception)

6 'TODO: log the error

7 End Sub

8

9 Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

10 Try

11 DoTask()

12 Catch ex As Exception

13 LogError(ex)

14 Throw ex

15 End Try

16 End Sub



Do this:

The reason for this is calling throw ex wipes out the stack trace

The output of the first example using throw ex would be:

Unhandled Exception: System.NotImplementedException: The method or operation is not implemented.
at WindowsApplication1.Form1.Button1_Click(Object sender, EventArgs e) in C:\...\Form1.vb:line 70


Whereas with just using throw:

Unhandled Exception: System.NotImplementedException: The method or operation is not implemented.
at WindowsApplication1.Form1.DoTask() in C:\...\Form1.vb:line 58
at WindowsApplication1.Form1.Button1_Click(Object sender, EventArgs e) in C:\...\Form1.vb:line 70


By using Throw ex it looks like the exception occured in the method that throws it. By using Throw you can perserve all of the stack trace information and easily see where the error came from.