Working with comma separated strings in t-SQL

Problem

When working with t-SQL you might run into a scenario where you want to return all the records that corresponds to the IDs found in a comma separated list, e.g. ‘123,456,789’. There are several ways to solve this problem and while browsing around I found an interesting solution.

Solution

In the example below I take advantage of the CHARINDEX and PATINDEX functions in t-SQL to search for specific patterns in the comma-separated string by concatenating the customer ID with commas and then match the concatenated string with the id string. The functions returns a number  greater than 0 if the sought string exists in the ID string which can be useful in my scenario where I got a comma separated list with customer IDs.

CHARINDEX works similar to the Substring or InStr functions that you can find in other programming languages. The function takes three parameters and the first is the string to search for, the second is the string to search and the third (which is optional) is the starting point where the search will begin in the string to search.

PATINDEX works similar to CHARINDEX, but allows the use of wildcards when performing the search. The function takes two parameters and the first is the string or pattern to search for and the second is the column or variable that will be searched. The method returns an integer bigger.

The example below also show that LIKE can be used to search the comma-separated string for specific IDs using a concatenated value along with wildcards.

USE [master]
GO

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

CREATE DATABASE [WeirdScience]
GO

USE [WeirdScience]
GO

CREATE TABLE [dbo].[Customers](
	[ID] [int] IDENTITY(1,1) NOT NULL,
	[Customer] [nvarchar](50) NOT NULL
) ON [PRIMARY]
GO

INSERT Customers (Customer) VALUES (N'Dan Jansson')
INSERT Customers (Customer) VALUES (N'Lars Svensson')
INSERT Customers (Customer) VALUES (N'Lisa Karlsson')
INSERT Customers (Customer) VALUES (N'Kalle Johansson')
INSERT Customers (Customer) VALUES (N'Gullan Bengtsson')
GO

DECLARE @customerstring varchar(max);
SET @customerstring = '1,3,4,6'

-------------------------------
-- This example is based on the following article:
-- http://www.projectdmx.com/tsql/sqlarrays.aspx
-------------------------------

SELECT *
  FROM Customers
 WHERE CHARINDEX(
	',' + CAST(ID AS NVARCHAR(50)) + ',',
	',' + @customerstring + ','
	) > 0

SELECT *
  FROM Customers
 WHERE PATINDEX(
	'%,' + CAST(ID AS NVARCHAR(50)) + ',%',
	',' + @customerstring + ','
	) > 0

SELECT *
  FROM Customers
 WHERE ',' + @customerstring + ','
  LIKE '%,' + CAST(ID AS NVARCHAR(50)) + ',%'

USE master
GO

DROP DATABASE WeirdScience
GO

For more ideas on how to work with arrays in SQL Server you should take a look at the excellent article found here: http://www.projectdmx.com/tsql/sqlarrays.aspx

Shrinking the Transaction Log in SQL Server

Problem

Well, this is a classical problem: The transaction log has been growing since who knows when and is now taking up almost all space on the server. You need to shrink the transaction log in order to free space on the server.

Solution

There are several solutions to this problem and it all depends on your database. This time I used the following script to get rid of all data in the transaction log and worth mentioning is that I didn’t care about the data in the transaction log; my goal was only to get rid of the unnecessary data and to reduce the size of the transaction log.

USE [MyDatabase]
GO
ALTER DATABASE [MyDatabase]
SET RECOVERY SIMPLE;
GO
DBCC SHRINKFILE ([MyDatabase_Log], 1);
GO
ALTER DATABASE [MyDatabase]
SET RECOVERY FULL;
GO

The script above worked fine for me and you will find similar solutions on the internet. If you analyze the script you will see that it changes the recovery model from FULL to SIMPLE which means that the database will not longer log transactions that occur in the database. After the change we execute the DBCC SHRINKFILE command that will try to shrink the file stated by the logical filename (MyDatabase_Log) to the desired size in megabytes (the second parameter to the DBCC command). When the file has been shrunk we restore the recovery model to FULL that was the original setting for the database; of course this all depends on your scenario, but my guess if you read this is that you are using the FULL recovery model. 

While looking around I found another older solution that were used earlier to accomplish sort of the same thing, however it’s recommended not to use the approach mentioned below.

USE [MyDatabase]
GO
DBCC SHRINKFILE([MyDatabase_Log], 1)
BACKUP LOG [MyDatabase] WITH TRUNCATE_ONLY
DBCC SHRINKFILE([MyDatabase_Log], 1)
GO 

Using PowerShell to create rules in Outlook

I recently started using Microsoft PowerShell to carry out administrative tasks and it didn’t take long for me to fall in love with it. Being a script fanatic using DOS instead of Windows, VBScript and JScript, Console-apps in favor to Windows apps, regex, etc. it’s easy to see the tremendous power in PowerShell; you can say that it’s the command-prompt on steroids :-) I’m lovin’ it!

The task – Creating an Outlook rule using PowerShell

Ok, do this might seem to be a trivial task, but as always when you think something seems to be easy you run into problems, especially when working with COM-objects/Automation. So, in order to solve my problem I had to do a lot of looking around at the forums on the web.

The script below creates a new rule in Outlook (I’m currently running Outlook 2010) that checks the sender of the message and looks for certain words in the subject field. If the sender is e.g. foo@bar.com (FROMEMAIL) and the subject contain the words ‘Hello’ and ‘World’ (SUBJECTWORDARRAY), the message should be forwarded to fii@bar.com (FORWARDEMAIL) and moved to the MyCustomFolder (MOVETOFOLDER) folder which is a subfolder to the Inbox folder.

##########################################
# Based on PowerShell 2.0
##########################################
function add_outlook_rule
{
    param([string]$RuleName,
          [string]$FromEmail,
          [string]$ForwardEmail,
          [string]$RedirectFolder,
          [string[]]$SubjectWords)

    Add-Type $class -ReferencedAssemblies Microsoft.Office.Interop.Outlook

    Add-Type -AssemblyName microsoft.office.interop.outlook
    $olFolders = "Microsoft.Office.Interop.Outlook.OlDefaultFolders" -as [type]
    $olRuleType = "Microsoft.Office.Interop.Outlook.OlRuleType" -as [type]
    $outlook = New-Object -ComObject outlook.application
    $namespace  = $Outlook.GetNameSpace("MAPI")
    $inbox = $namespace.getDefaultFolder($olFolders::olFolderInbox)

    $rules = $outlook.session.DefaultStore.GetRules()
    $rule = $rules.Create($RuleName,$olRuleType::OlRuleReceive)

    $SubjectCondition = $rule.Conditions.Subject
    $SubjectCondition.Enabled = $true
    $SubjectCondition.Text = $SubjectWords

    $d = [System.__ComObject].InvokeMember(
        "EntryID",
        [System.Reflection.BindingFlags]::GetProperty,
        $null,
        $inbox.Folders.Item($RedirectFolder),
        $null)
    $MoveTarget = $namespace.getFolderFromID($d)

    # Uncomment the row below if you want to use the DeletedItems
    # folder as MoveToFolder.
    #$MoveTarget = $namespace.getDefaultFolder(
        $olFolders::olFolderDeletedItems)

    $MoveRuleAction = $rule.Actions.MoveToFolder
    [Microsoft.Office.Interop.Outlook._MoveOrCopyRuleAction].InvokeMember(
        "Folder",
        [System.Reflection.BindingFlags]::SetProperty,
        $null,
        $MoveRuleAction,
        $MoveTarget)
    $MoveRuleAction.Enabled = $true

    $FromCondition = $rule.Conditions.From
    $FromCondition.Enabled = $true
    $FromCondition.Recipients.Add($FromEmail)
    $fromCondition.Recipients.ResolveAll()

    $ForwardRuleAction = $rule.Actions.Forward
    $ForwardRuleAction.Recipients.Add($ForwardEmail)
    $ForwardRuleAction.Recipients.ResolveAll()
    $ForwardRuleAction.Enabled = $true

    $rules.Save()
}

##########################################
# SYNTAX:
##########################################
# add_outlook_rule RULENAME
#                  FROMEMAIL
#                  FORWARDEMAIL
#                  MOVETOFOLDER
#                  SUBJECTWORDARRAY
##########################################

add_outlook_rule "This is my custom rule" "fii.from@bar.com" "foo.forward@bar.com" "MyCustomFolder" @("foo","bar")

 

And now for the problems I ran into…

Problem number 1

The first problem I ran into was with the criterion that looked for certain words in the Subject field. I found out that the subject field expected an array of type string, but by some strange reason I didn’t get it to work, even tough I Reflected the PIA (Primary Interop Assembly) for Microsoft Outlook and checked exactly what types the class expected and tried to cast the variable in all possible ways available.  After some additional investigation I found out that the string array needed to contain more than one item which means that if you have an array with two or more items the script started to work.

$SubjectCondition = $rule.Conditions.Subject
$SubjectCondition.Enabled = $true 

# Will not work...
# $a = $(“foo”)

# Works like a charm
$a = $(“foo”,”fii”)

$SubjectCondition.Text = $a

 

Problem number 2

When I tried to set the Folder property on the MoveToFolder object I got an error stating that I was trying to set a property that expected an object that implemented the interface MAPIFolder to a ComObject which apparently doesn’t do that.

This is the error message I received:

Exception setting "Folder": "Cannot convert the "System.__ComObject" value of type "System.__ComObject#{00063006-0000-0000-c000-000000000046}" to type "Microsoft.Office.Interop.Outlook.MAPIFolder"."

No matter how I tried to cast the ComObject to a MAPIFolder the script constantly failed. I tried to get a handle on the desired folder in many different and this part was actually quite easy, but the problem still remained: It was not possible to cast the ComObject to a MAPIFolder. So what now? Give up? Never! :-)

I came up with the idea that maybe the solution would be not to cast the ComObject but instead try to interact with it somehow, and for this we have our .NET friend – Reflection! By using reflection on objects in .NET it is possible to get and set properties on ComObjects and this turned out to be the solution to my problem.

In the first code block below I use the InvokeMember method that is part of reflection for types in .NET to first get a property from a ComObject. There are other ways to get a handle on a folder in Exchange/Outlook, but I wanted to show an alternate way of doing this in this sample. The second code block I set the Folder property on the MoveToFolder action object. This solved my second problem.

$d = [System.__ComObject].InvokeMember(
	"EntryID",
	[System.Reflection.BindingFlags]::GetProperty,
	$null,
	$inbox.Folders.Item($RedirectFolder),
	$null)
$MoveTarget = $namespace.getFolderFromID($d)

$MoveRuleAction = $rule.Actions.MoveToFolder
[Microsoft.Office.Interop.Outlook._MoveOrCopyRuleAction].InvokeMember(
	"Folder",
	[System.Reflection.BindingFlags]::SetProperty,
	$null,
	$MoveRuleAction,
	$MoveTarget)
$MoveRuleAction.Enabled = $true

 

Problem number 3

When everything started to work (scripts are like that – it’s all or nothing) everything seemed fine until I used the script in a batch where I created many rules on a specific mailbox store. The problem here turned out to be a limit in the definition storage on the Exchange server that only allowed the size of the rule definitions for an account to be 32 kb. In my case I exceeded the limitation and got an error when executing my scripts.

More information

Architecture mismatch when using ODBC

Today I ran into a problem when I was trying to connect to a data source using ODBC.  Currently I’m working on a web application that uses ASP.NET running on .NET Framework 2.0 and that has one connection to a SQL Server database and a number of other connections to databases based on Pervasive SQL. The web application uses ODBC to connect to the Pervasive SQL databases.

Ok, and now for the problem…

When I was trying to start the web application I got an error message saying “ERROR [IM014] [Microsoft][ODBC Driver Manager] The specified DSN contains an architecture mismatch between the Driver and Application”.

After a little investigation I found out that my Visual Studio project was compiled targeted towards the x64 architecture (64 bit) using ODBC drivers created for the x86 architecture (32 bit) which led to the error message. Apparently you need to have ODBC-drivers that is compiled for the same architecture as the application that uses them.

This is how I solved the problem and got my application up and running…

The first thing I did was to make sure that I had a valid ODBC connection, and since I’m running Windows 7 x64 (64 bit) I needed to open the old ODBC Data Source Administrator using the following path:

%WINDIR%\SYSWOW64\ODBCAD32.exe

image

The WOW part in SYSWOW64 basically means Windows 32 bit on Windows 64 bit and is sort of a copy of the system32 folder created on the x86/32 bit architecture

I checked my ODBC connection and made sure that it was properly created.

Next thing – Visual Studio …

Ok, the ODBC connection seems to work and the next thing I had to do was to change the platform for the projects in my Visual Studio solution. First a little background: the solution contains five C# projects that are class libraries and two projects that are ASP.NET web site projects. The web sites has references to the class libraries and everything is created with default settings in Visual Studio, i.e. the platform is set to “Any CPU”. The web sites are configured to run using IIS and not Cassini, the ASP.NET development server used by Visual Studio.

Changing the target platform for the class libraries was easy and I simply just opened the properties for each project and changed the Platform target found under the Build tab in the Properties dialog.

image

When I tried to do the same thing with the The ASP.NET web site projects I discovered that that type of Visual Studio projects doesn’t have any setting for this. In order to make the web sites run on the x86 architecture I found out that this has to be done in Internet Information Services Manager (IIS).

Next and final thing – IIS

So, almost there! I started the IIS Manager (INETMGR) and expanded the node for Application Pools. This is where we get the possibility to tell the application what platform to run on. I selected the application pool used by my web site and clicked Advanced Settings in the right panel. The next thing I did was to alter the Enable 32-Bit Applications setting to true which basically means that my web site application should run as an application using the x86 (64 bit) architecture.

image

 

Problem solved!

I solved the problem following the steps above and my web site application is up and running.

Problem with Integration Features in Windows 7

Lately I have had a lot of problems with my virtual pc environments which is running Windows 7. My hosting OS is Windows 7 and the big reason for the trouble is because of the Windows 7 Service Pack 1. Since it was installed on my hosting machine it totally messed up the Virtual PC Integration Features for my virtual machines.

The mouse stopped working in the virtual machine plus everything else except from the keyboard, so I was quite stuck with using shortcuts to navigate. Every time I tried to install the Integration Components I got an error, think it was error code 1602 or something, which resulted in a rollback for the installation process.

How to solve the problem

After a lot of browsing around on the net I came across some information that explained that it is possible to prohibit the installer from doing a rollback of the installation of the Integration Feature Components if anything should fail (which it always does on my machine). By prohibiting a rollback the installer is unable to create certain files used for the rollback and the result is that the Integration Features are installed anyway even though something might be wrong. This solved my problem!

image

 

  1. Open MMC using the textbox in the start menu.
  2. Add the Group Policy Object Editor snap-in to the console.
  3. Select the Computer Configuration node and expand it.
  4. Select the Administrative Templates node and expand it.
  5. Select Windows Components and locate the Windows Installer item.
  6. In the Windows Installer; locate the Prohibit rollback setting.
  7. Set the value for Prohibit rollback to Enabled.
  8. Save the value and close the Console application.
  9. Restart the system.
  10. Start the installer for the Integration Features.
  11. If you get questions regarding processes that needs to be shut down you can terminate them using the task manager. I needed to terminate two processes related to the Virtual PC and I simply wrote down the PID numbers and terminated them in Task Manager.
  12. The installer should complete and inform you that the installation succeeded.
  13. Restart the system.
  14. Now at least the mouse should be working.
  15. I don’t really know how to fix the rest of the integration features, but I will update this blog post if I get some ideas. I think that the cause of the problem is something with the hardware together with service pack 1 for Windows 7.

Changing language for PowerPoint presentations

When you want to change the dictionary that is being used for a PowerPoint presentation, you might think that it is just to select all slides and change dictionary or enter some magic dialog where you find some setting for the current presentation, but this is where the problem occur and you realizes that it is not as easy as you might think.

Everything that appear in a PowerPoint slide are considered to be an object. This means that the title of the slide is an object as well as the text area where you have a bulleted list, the additional text areas, etc.. And if you think a little bit further you will soon realize that you need to select each object in each slide in order to change to the correct dictionary. Pheew!

Luckily there is always a way of scripting this, so I created a macro in VBA for PowerPoint that iterates through every slide in the presentation and changes dictionary for all objects found on the slide. The script is quite self-explanatory and you’ll find it below.

Sub SetLanguageIDEnglishUS()
    Dim slideCount, shapeCount, j, k
    slideCount = ActivePresentation.Slides.Count
    For j = 1 To slideCount

        ' Change dictionary for all shape objects
        shapeCount = ActivePresentation.Slides(j).Shapes.Count
        For k = 1 To shapeCount
            If ActivePresentation.Slides(j).Shapes(k).HasTextFrame Then
                ActivePresentation.Slides(j).Shapes(k).TextFrame _
                    .TextRange.LanguageID = msoLanguageIDEnglishUS ' msoLanguageIDSwedish
            End If
        Next k

        ' Change dictionary for all notes
        shapeCount = ActivePresentation.Slides(j).NotesPage.Shapes.Count
        For k = 1 To shapeCount
            If ActivePresentation.Slides(j).NotesPage.Shapes(k).HasTextFrame Then
                ActivePresentation.Slides(j).NotesPage.Shapes(k).TextFrame _
                    .TextRange.LanguageID = msoLanguageIDEnglishUS ' msoLanguageIDSwedish
            End If
        Next k
    Next j
End Sub

Create a Word document based on a PowerPoint presentation

Sometimes it can be handy to have a more detailed document with information that belongs to the slideshow displayed on the projector. This document can have e.g. code samples, diagrams and other relevant information that doesn’t fit on the PowerPoint presentation. I needed to do this when writing course material, so I created this script that creates a Word document with the slide as a miniature image so that it is possible to add comments or key points.

This script creates a linked PowerPoint slide object in a Word document. The script also extracts the notes belonging to the slide and adds them to the current page. Each slide in the presentation will generate one page in the Word document and the linked PowerPoint slide will be centered and resized to 9 centimeters.

In order to make this script work, simply just paste this code into the macro editor in your word document.

Sub CreateWordPagesBasedOnPowerPointPresentationLink()

    Dim sImagePath As String
    Dim sImageName As String
    Dim objPPT As PowerPoint.Application

    Dim oSlide As Slide '* Slide Object
    On Error GoTo Err_ImageSave

    strDocPath = InputBox("Path: ", _
        "Path to PowerPoint presentation", _
        "C:\MyPath\MyPresentation.pptx")

    Set objPPT = CreateObject("Powerpoint.application")
    'objPPT.Visible = False
    objPPT.Presentations.Open strDocPath

    For Each oSlide In objPPT.ActivePresentation.Slides
        If Not oSlide.SlideShowTransition.Hidden = msoTrue Then
            sImageName = oSlide.Name & ".PNG"

            oSlide.Copy

            Selection.PasteSpecial Link:=True, DataType:=wdPasteOLEObject, Placement:= _
                wdInLine, DisplayAsIcon:=False

            Selection.MoveLeft Unit:=wdCharacter, Count:=1, Extend:=wdExtend
            With Selection.Borders(wdBorderTop)
                .LineStyle = Options.DefaultBorderLineStyle
                .LineWidth = Options.DefaultBorderLineWidth
                .Color = Options.DefaultBorderColor
            End With
            With Selection.Borders(wdBorderLeft)
                .LineStyle = Options.DefaultBorderLineStyle
                .LineWidth = Options.DefaultBorderLineWidth
                .Color = Options.DefaultBorderColor
            End With
            With Selection.Borders(wdBorderBottom)
                .LineStyle = Options.DefaultBorderLineStyle
                .LineWidth = Options.DefaultBorderLineWidth
                .Color = Options.DefaultBorderColor
            End With
            With Selection.Borders(wdBorderRight)
                .LineStyle = Options.DefaultBorderLineStyle
                .LineWidth = Options.DefaultBorderLineWidth
                .Color = Options.DefaultBorderColor
            End With

            Selection.MoveRight Unit:=wdCharacter, Count:=1
            Selection.TypeParagraph
            Selection.TypeParagraph

            If oSlide.NotesPage.Shapes.Placeholders.Count > 0 Then
            Dim foo As Object
                Selection.TypeText (oSlide.NotesPage.Shapes.Placeholders(2).TextFrame.TextRange.Text)
                Selection.TypeParagraph
            End If
            Selection.InsertBreak Type:=wdPageBreak
        End If
        DoEvents
    Next oSlide

    objPPT.Quit
    Set objPPT = Nothing

    For Each oShape In ActiveDocument.InlineShapes
        With oShape
            .LockAspectRatio = msoTrue
            .Width = CentimetersToPoints(8.99)
            .Height = CentimetersToPoints(6.74)
            .Range.ParagraphFormat.Alignment = wdAlignParagraphCenter
        End With
    Next oShape

Err_ImageSave:
        If Err <> 0 Then
            MsgBox Err.Description
        End If

End Sub

Writing PowerPoint Presentations

When creating presentations in PowerPoint (or with other programs) there are a lot of things to think about. This article explains some of the discoveries that I have made when working with Microsoft Word 2007 and Microsoft PowerPoint 2007 to create course material for technical training.

This article will not cover the rhetorical aspects of teaching or writing presentations, but more focus on giving tips and tricks that can help you ease the burden of writing and maintaining presentations.

PowerPoint Templates

When I start the work with creating a presentation I usually take advantage of a template that contains the company profile with logo, header, footer, etc. A good PowerPoint template should contain slide templates for e.g. writing bulleted lists, displaying tables, images or objects, topic pages, sub topic pages, etc.

If you are not satisfied with the slide templates you  can alter the PowerPoint template in: View / Slide Master. When you have entered the Slide Master view you can add necessary slide templates, change already existing templates or delete unnecessary templates. The templates are arranged in a hierarchy with a top template with a default layout and descending templates that specializes specific parts of the master. This means that if you put the company logo on the master template you will have the logo on all descending templates if not overridden. The templates you define here will appear in the dialog for inserting a new slide when you are in normal mode.

Arranging slides

When the presentation grows and ends up in many slides it can be cumbersome to arrange the slides in a specific order. In the View tab you will find the Slide Sorter layout. This view will help you sort the slides simply by dragging and dropping slides in the desired order.

Embedded objects

A normal way of doing things is to simply make a screen dump of something you need to explain in the slideshow and lazy as we all are we simply hit ALT + PRTSC (or Print screen) and paste the data right into our presentation.

This will kill the idea of a small handy presentation.

If you consider and appreciates space and want your presentation to remain small and handy I recommend you to paste your images into Microsoft Paint or even better Adobe Photoshop so that you can reduce the size of the images. You are normally not creating a presentation that will be printed out in a size where one page can cover the walls where you are sitting with remained sharpness. In other words: Save your images as PNG files or something similar and insert them into your presentation after that. This will reduce the size of the presentation.

One side effect that occur if you don’t consider reducing your embedded images is that this will make the generated Word document even bigger (see Create a Word document based on a PowerPoint presentation).

Miscellaneous tips

Here are some tips that will easy the job with creating great presentations:

  • Create a PowerPoint template that covers all the slide types that you will need in your presentation.
  • Always try to limit the size of the images or objects you embed into the slide.
  • Use the slide sorter to arrange the slides when the presentation grows.
  • Split slides with too much content by using the shortcut CTRL + D (duplicate slide).
  • Try to make your presentation fit the rule “6+6+6”, i.e.
    • 6 Slides per subject.
    • 6 Bullets per slide.
    • 6 words per bullet.
  • Remember that images or a diagrams are better that plain text in the slides.

Lost TAB- and ESC-keys in Virtual PC

I work a lot in Virtual PC environments and one problem I have stumbled across now and then is that keys sometimes stops working. Today it happened again so I “googled” it up and found a solution to my “lost keys”-problem.

I’m currently working in a VPC running Windows 2008 Server on my laptop that runs Windows 7 (64 bit).
The problem I’ve experienced is that the ESC- and TAB-keys occasionally stops working along with the ‘ö’-key on my keyboard (Swedish).

The solution to my problem was to make changes to the Local Security Policy that is located under the Administrative Tools in Control Panel.

image

I solved my problem by doing the following changes:

  1. Close all VPC:s including the Virtual PC Console.
  2. Open the “Local Security Policy”.
  3. Select “Software Restriction Policies”.
  4. Select “Additional Rules”.
  5. Add a new “path rule” and enter the following path:

    %appdata%\microsoft\virtual pc\vpckeyboard.dll

    Note: The path to the vpckeyboard.dll-file may differ depending on what OS
    you are running. The text %appdata% is an environment variable in the system
    and can be replaced with the full path if you like.

  6. Set the “Security Level” to “Unrestricted”.
  7. Save the new rule by clicking the OK-button
  8. Start your Virtual PC and now the key should work just fine.

The image below shows the Local Security Policy with the “Software Restriction Policies”-node selected.

image

MembershipProvider – Importing old user-accounts

Recently i stumbled into a problem where I needed to import already existing user-accounts from an old website into a new EPiServer website that was using the ASP.NET Membership-system.

The question was:

  • How can you automatically create approximately 7,500 user-accounts in an easy way by using the ASP.NET Membership-system?
  • What are the requirements for doing this and is it possible to connect the newly created user-account with a certain role or several roles?
  • What about Profiles? Can you create the UserProfile as a part of the import-process?

Well, after a little research i came up with the following solution:

  • Create a Windows desktop-application (this is not a requirement, but it eases the development process) that uses the classes for the ASP.NET Mebership system.
  • Make sure the application can access the datasource for the old useraccounts; preferable a SQL Server database or some sort of datasource that is easy to extract data from.
  • Log every account that is imported even if the import failed in order to investigate the reason for the failure.

Requirements:

  • The passwords must be in clear-text in the datasource you are importing from. When the account is created in the ASP.NET Membership-system the password will be hashed/encrypted which means that you will not be able to read it in clear-text, so this is a one-way import process.

The solution is quite simple and straightforward. A couple of methods and some configuration makes it possible to create n user-accounts from the old membership-system.

Sourcecode for the client application.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Web;
using System.Web.Profile;
using System.Web.Security;
using System.IO;

namespace AccountConverter
{
    public partial class frmConvert : Form
    {
        public frmConvert()
        {
            InitializeComponent();
        }

        /// <summary>
        /// Start the conversion process.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btConvert_Click(object sender, EventArgs e)
        {
            // Get all active users
            List<ConvertingCustomer> oldAccounts = GetOldAccounts();

            progressBar1.Minimum = 0;
            progressBar1.Maximum = oldAccounts.Count();

            // Iterate through all users that should be converted.
            int i = 0;
            foreach (ConvertingCustomer account in oldAccounts)
            {
                // Set current rownumber
                i += 1;

                try
                {
                    // Convert the current account
                    CreateAccount(account);

                    // Update the listbox with status for the current account
                    UpdateListBox(i, string.Format("SUCCESS: {0}\t{1}",
                        account.AccountNumber,
                        account.UserName));

                    // Log that the account was successfully converted
                    Log(account.AccountNumber, ConvertStatus.SUCCESS.ToString(), "Account was successfully converted.");
                }
                catch (Exception ex)
                {
                    // Update the listbox with status
                    UpdateListBox(i, string.Format("FAILED: {0}\t{1}\t{2}",
                        account.AccountNumber,
                        account.UserName,
                        ex.Message));

                    // Log that the account could not be converted
                    Log(account.AccountNumber, ConvertStatus.FAILED.ToString(), ex.Message);
                }

                // Update the progressbar
                progressBar1.Value = i;
                progressBar1.Update();
            }
        }

        /// <summary>
        /// Convert a single account. Create a new account with the ASP.NET Membership-system, connect it to a certain role and create an user-profile.
        /// </summary>
        /// <param name="convertingCustomer">An object with necessary data for the customer to be converted.</param>
        private void CreateAccount(ConvertingCustomer convertingCustomer)
        {
            // Create the User
            MembershipUser mu = Membership.CreateUser(
                string.Format("{0}{1}", "TEST_", convertingCustomer.UserName),
                convertingCustomer.Password,
                convertingCustomer.UserName);
            mu.IsApproved = true;

            // Add the user to the Customers-role
            Roles.AddUserToRoles(
                convertingCustomer.UserName,
                new string[] { "Customers" });

            // Update the User-Profile
            WebProfile webProfile = new WebProfile(WebProfile.Create(convertingCustomer.UserName));
            webProfile.Email = convertingCustomer.UserName;
            webProfile.CustomerNumber = convertingCustomer.AccountNumber;
            // ... add your own fields here
            webProfile.Save();
        }

        /// <summary>
        /// Connect to a datasource and get data for the old accounts that is to be converted.
        /// </summary>
        /// <returns></returns>
        private List<ConvertingCustomer> GetOldAccounts()
        {
            // Get old account-data.
            //var myOldAccounts = datasource.GetAccounts();

            // Get accounts from datasource
            //var accountDataList = from account in myOldAccounts
            //                      select new ConvertingCustomer
            //                      {
            //                          AccountNumber = account.CustomerId,
            //                          UserName = account.UserName,
            //                          Password = account.Password
            //                      };
            //return accountDataList.ToList<ConvertingCustomer>(); 

            return new List<ConvertingCustomer>
            {
                AccountNumber = "123456",
                UserName = "TestUser@labs.com",
                Password = "Pa$$w0rd"
            }
        }

        /// <summary>
        /// Log the progress with the account conversion.
        /// </summary>
        /// <param name="accountNumber">The customers accountnumber in the backend.</param>
        /// <param name="status">Status of the progress.</param>
        /// <param name="message">An errormessage or a message with information about the conversion.</param>
        private void Log(string accountNumber, string status, string message)
        {
            using (StreamWriter sw = new StreamWriter(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "convert.log"), true, Encoding.UTF8))
            {
                sw.WriteLine("{0}\t{1}\t{2}", status, accountNumber, message);
            }
        }

        /// <summary>
        /// Update the ListBox in the GUI to display the progress.
        /// </summary>
        /// <param name="recordNumber">An integer with the rownumber of the account that was processed.</param>
        /// <param name="message">The resulting message from the conversion process.</param>
        private void UpdateListBox(int recordNumber, string message)
        {
            lbConvertProgress.Items.Insert(0, string.Format("#{0} - {1}", recordNumber, message));
            lbConvertProgress.Update();
        }

        /// <summary>
        /// An internal class used to handle customer-data.
        /// </summary>
        private class ConvertingCustomer
        {
            public string UserName;
            public string Password;
            public string AccountNumber;
        }

        /// <summary>
        /// An enum with conversion-status.
        /// </summary>
        private enum ConvertStatus
        {
            SUCCESS,
            FAILED
        }

    }
}

 

The App.Config file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
        <add name="MyDataSourceConnectionString"
             connectionString="Data Source=myDatabaseServer;Database=myDatabase;User Id=myDBUser;password=P@ssw0rd;Connection Timeout=30"
             providerName="System.Data.SqlClient" />
    </connectionStrings>
    <system.web>
        <membership defaultProvider="SqlServerMembershipProvider">
            <providers>
                <clear/>
                <add name="SqlServerMembershipProvider"
                     type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
                     connectionStringName="MyDataSourceConnectionString"
                     requiresQuestionAndAnswer="false"
                     applicationName="MyWebApplication"
                     requiresUniqueEmail="true"
                     passwordFormat="Hashed"
                     maxInvalidPasswordAttempts="5"
                     minRequiredPasswordLength="6"
                     minRequiredNonalphanumericCharacters="0"
                     passwordAttemptWindow="10"
                     passwordStrengthRegularExpression=""/>
            </providers>
        </membership>
        <roleManager enabled="true"
                     defaultProvider="SqlServerRoleProvider">
            <providers>
                <clear/>
                <add name="SqlServerRoleProvider"
                     connectionStringName="MyDataSourceConnectionString"
                     applicationName="MyWebApplication"
                     type="System.Web.Security.SqlRoleProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"/>
            </providers>
        </roleManager>
        <profile enabled="true"
                 defaultProvider="SqlProfile"
                 automaticSaveEnabled="true">
            <properties>
                <add name="CustomerNumber"
                     type="System.String"/>
                <add name="Email"
                     type="System.String"/>
                <add name="Name"
                     type="System.String"/>
                <add name="Phone"
                     type="System.String"/>
                <add name="CellPhone"
                     type="System.String"/>
            </properties>
            <providers>
                <clear/>
                <add name="SqlProfile"
                     type="System.Web.Profile.SqlProfileProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
                     connectionStringName="MyDataSourceConnectionString"
                     applicationName="MyWebApplication"/>
            </providers>
        </profile>
    </system.web>
    <appSettings />
</configuration>
Technorati-taggar: ,,