The middle ground for code analysis custom dictionaries

I posted almost a year ago about using custom dictionaries for spell checking in code analysis. At the time, it seemed like a good idea to modify the custom dictionary in C:\Program Files\Microsoft Visual Studio 9.0\Team Tools\Static Analysis Tools\FxCop so that any code on the machine will be evaluated against that dictionary. I'm now starting to swing away from this idea.

Having a custom dictionary in one spot on the machine is a good because any solution you use on that machine will adhere to that dictionary. The problem in a team environment is that the custom dictionary is not in source control. Any new dev machine will not have those changes and is likely to fail code analysis. Build servers will also suffer a maintenance nightmare with their custom dictionary as it will need to support all possible solutions being built, not just the ones on a particular dev machine.

If you swing to the other extreme, suppressing spelling failures in code is messy, but is in source control for other developers and build servers to benefit from.

I am working with a new model that is the middle ground of these two extremes.

I noticed a while ago that [VS] now contains more build actions for files in a project. One of them is CodeAnalysisDictionary. This becomes very helpful.

image

My new method of satisfying spell checking in code analysis is to have my custom dictionary checked into source control. This usually means that the xml file will be located in the same place as the solution file.

The first thing to do is add the custom dictionary as a solution item.

image

This makes it easy to open, check out, edit and check in this file without having to go through Source Code Explorer to do a manual check out, edit, check in cycle. It also means that it is part of the solution in the Pending Changes dialog when check in files are filtered by the current solution.

Next, we need to associate this single custom dictionary with all of the projects in the solution. We do not want to copy this file between the projects as this would still be a maintenance nightmare. [VS] is kind enough to have a file link facility. In each project, open up the Add Existing Item dialog, find the custom dictionary and add it as a link.

image

This will then add this file as a link to the project. You will notice that the overlay of the icon in Solution Explorer also indicates that this file is a shortcut to another location.

image

Open up the properties on this file, and select CodeAnalysisDictionary as the Build Action for this xml file.

image

As you compile the application and run code analysis, this custom dictionary will be referenced.

The advantages of this solution are:

  • Under source control so it is accessible to all developers
  • Under source control so it is accessible to team build servers
  • It contains only custom dictionary entries that are related to the projects that reference it
  • Common to all projects in the solution
  • Common to all projects in a TFS project/branch depending on TFS project structure
  • Easily updated and checked in
  • Visible to developers who use the solution

The disadvantage of this solution is:

  • The custom dictionary may need to be duplicated for each solution/TFS project

At war with F1

After implementing the double F1 solution a few moths ago, I now get continuously bitten by the change. I keep pressing F1 just once and then wait for help to come up only to remember that I need to hit F1 a second time. It probably doesn't help that MSDN from [VS] takes a long time to come up the first time, but eventually I realise that I need to hit F1 a second time.

It's a war between my automated single F1 press for help vs my misfiring over from the ESC key. I will keep the setting how it is though. Forgetting the second F1 press isn't as bad as accidentally hitting F1 instead of ESC.

Supporting Community Server urls in BlogEngine.Net

One of the parts of my migration from CS to [BE] was to continue support for CS url formats. Primarily, this is because I have lots of Google traffic and people who are subscribed to syndication feeds on those urls. Writing an http module was the easiest way to provide support for these existing urls.

I have released the first version of this module which is being used on this site currently. You can get the binaries and source from the CodePlex project here.

The following are the release notes which are also found in the comments in the code:

Neovolve.BlogEngine.Web 1.0 contains a redirector module that translates Community Server url formats into BlogEngine urls. This module will redirect Community Server pages, posts, month post lists, day post lists, tags, syndication and tagged syndication urls to the most appropriate location in BlogEngine. BlogEngine relies on its own friendly written urls when the urls are processed. This module cannot rewrite Community Server urls as BlogEngine will not be able to understand what the requested resource is. The module must redirect requests to the equivalent BlogEngine friendly url so that BlogEngine url writing can occur such that resources are resolved correctly.

In Community Server, tags and categories are the same thing and it supports tag filtering by allowing multiple tags to be defined. BlogEngine supports both tags and categories as separate entities and has a concept of a category hierarchy, but doesn't support tag filtering. When a Community Server url is encountered where multiple tags are defined, the first Community Server tag in the url that can be matched to either a BlogEngine category or tag will be the resource used to response to the client request. Preference is given to categories over tags.

To use this module, put the release binary into your bin directory and add the following to the httpModules section in web.config.

<add name="CSUrlRewrite" type="Neovolve.BlogEngine.Web.CSUrlRedirector, Neovolve.BlogEngine.Web"/>

Download from here.

Conflict between HttpUtility.UrlEncode and IIS7

I have previously posted about the issues I had with CS on IIS7 with + characters in urls. Under the default configuration, IIS7 returns a 404 error because it is concerned about double escaping. With my migration from CS to [BE], I wanted to maintain existing incoming CS links. This means that I still needed to support + characters in urls.

I also found that the TagCloud control in [BE] renders + characters as well. After looking at the code, it eventually calls down into HttpUtility.UrlEncode. This code uses + instead of %20 for encoding spaces. This is a big problem. Any code that uses the HttpUtility.UrlEncode (which is very common), will prevent pages being served correctly on IIS7 without using the workaround indicated in the previous post.

Moving categories to tags

As part of my CS to [BE] migration, I wanted to clean up my use of categories. I had quite a few of them, and reducing the number of categories by moving a lot of them into tags seemed like a good idea. There were a few scenarios involved. Sometimes I wanted to leave the existing category. I also wanted to rename a category and optionally move the old category into a tag.

Here is the script:

BEGIN TRANSACTION 

SET NOCOUNT ON 

DECLARE @OldCategory NVARCHAR(50) 
DECLARE @NewCategory NVARCHAR(50) 
DECLARE @Tag NVARCHAR(50) 

SET @OldCategory = 'BlogEngine.Net' 
SET @NewCategory = 'Applications' 
SET @Tag = 'BlogEngine.Net' 

/* 

An old category must be provided 
If a tag is provided, posts with the old category will be assigned the provided tag 
If the new category is the same as the old category, the existing category will be left unchanged 
If a new category is provided, the old category will be renamed to the new category 
If a new category is not provided, the old category will be deleted 

*/ 

IF ISNULL(@OldCategory, '') = '' 
BEGIN 

      RAISERROR('No old category has been specified', 16, 1) 

END 

IF ISNULL(@Tag, '') != '' 
BEGIN 

      PRINT 'Converting category ' + @OldCategory + ' to tag ' + @Tag 

      INSERT INTO be_PostTag 
      ( 
            PostID, 
            Tag 
      ) 
      SELECT PC.PostID, @Tag 
      FROM be_PostCategory PC 
            INNER JOIN be_Categories C ON PC.CategoryID = C.CategoryID 
            LEFT OUTER JOIN be_PostTag PT ON PC.PostID = PT.PostID AND PT.Tag = @Tag 
      WHERE C.CategoryName = @OldCategory 
            AND PT.Tag IS NULL 

      PRINT CAST(@@ROWCOUNT AS NVARCHAR(50)) + ' categories converted into tags' 
      PRINT '' 

END 

IF ISNULL(@OldCategory, '') != ISNULL(@NewCategory, '') 
BEGIN 

      IF ISNULL(@NewCategory, '') != '' 
      BEGIN 

            PRINT 'Renaming category ' + @OldCategory + ' to ' + @NewCategory 
            PRINT 'WARNING: The descriptions of renamed categories need to be manually reviewed' 

            IF NOT EXISTS 
            ( 
                  SELECT CategoryID 
                  FROM be_Categories 
                  WHERE CategoryName = @NewCategory 
            ) 
            BEGIN 

                  PRINT 'Category ' + @NewCategory + ' doesn''t exist. The category ' + @OldCategory + ' will be renamed' 

                  -- The new category doesn't yet exist 
                  UPDATE be_Categories 
                  SET CategoryName = @NewCategory 
                  WHERE CategoryName = @OldCategory 

            END 
            ELSE 
            BEGIN 

                  PRINT 'Category ' + @NewCategory + ' already exists. Posts for the category ' + @OldCategory + ' will be migrated before the category is deleted' 

                  -- The new category already exists 
                  -- We need to migrate to the new category Id and delete the old category 
                  DECLARE @OldCategoryID UNIQUEIDENTIFIER 
                  DECLARE @NewCategoryID UNIQUEIDENTIFIER 

                  SELECT @OldCategoryID = CategoryID 
                  FROM be_Categories 
                  WHERE CategoryName = @OldCategory 

                  SELECT @NewCategoryID = CategoryID 
                  FROM be_Categories 
                  WHERE CategoryName = @NewCategory 

                  -- Migrate posts to the new category if they aren't already assigned to it 
                  UPDATE be_PostCategory 
                  SET CategoryID = @NewCategoryID 
                  WHERE CategoryID = @OldCategoryID 
                        AND PostID NOT IN 
                        ( 
                              SELECT PostID 
                              FROM be_PostCategory 
                              WHERE CategoryID = @NewCategoryID 
                        ) 


                  PRINT CAST(@@ROWCOUNT AS NVARCHAR(50)) + ' posts migrated to new category' 
                  PRINT '' 

                  -- Remove relationships which had both categories assigned 
                  DELETE 
                  FROM be_PostCategory 
                  WHERE CategoryID = @OldCategoryID

                  DELETE 
                  FROM be_Categories 
                  WHERE CategoryID = @OldCategoryID 

            END 

      END 
      ELSE IF ISNULL(@NewCategory, '') = '' 
      BEGIN 

            PRINT 'Deleting category ' + @OldCategory 

            DELETE 
            FROM be_PostCategory 
            WHERE CategoryID IN 
            ( 
                  SELECT CategoryID 
                  FROM be_Categories 
                  WHERE CategoryName = @OldCategory 
            ) 

            DELETE 
            FROM be_Categories 
            WHERE CategoryName = @OldCategory 

      END 

END 

COMMIT TRANSACTION

Normal deal applies, use this at your own risk and don't run it on a production database until you have thoroughly tested it first.

Migration SQL script from Community Server to BlogEngine.Net

I took the inspiration from Dave to write a script to migrate the things I wanted from CS over to [BE]. His script was a great help as he identified some of the obscure bits of information like post type id values.

Here is my script. It copies across posts, pages, comments and trackbacks (in [BE] format). It pulls out author information and slug names for URLs. The main bit of information I didn't migrate was stats over views, reads and downloads.

-- TODO 
-- Replace [AccountId] with your account id in the site 
-- Replace [AccountName] with your account name in the site 
-- Replace [NewDatabaseName] with your new database name 
-- Replace [OldDatabaseName] with your old database name 
-- Replace [DomainName] with your domain name 
-- Set @BlogSectionId to your section id

USE [NewDatabaseName] 
GO 

ALTER TABLE [NewDatabaseName].dbo.be_Posts 
ADD CSPostID INT NULL 
GO 

ALTER TABLE [NewDatabaseName].dbo.be_categories 
ADD CSCategoryID INT NULL 
GO 

-- Posts and Comments ------------------------------- 

DECLARE @BlogSectionId INT 
SET @BlogSectionId = 3 

-- Insert pages 
INSERT INTO [NewDatabaseName].dbo.be_Pages 
( 
    PageID, 
    Title, 
    [Description], 
    PageContent, 
    Keywords, 
    DateCreated, 
    DateModified, 
    IsPublished, 
    IsFrontPage, 
    Parent, 
    ShowInList 
) 
SELECT NEWID(), 
    [Subject], 
    ([OldDatabaseName].dbo.FetchExtendendAttributeValue('Excerpt', c.PropertyNames, c.PropertyValues)), 
    Body, 
    NULL, 
    PostDate, 
    PostDate, 
    1, 
    0, 
    NULL, 
    1 
FROM [OldDatabaseName].dbo.cs_Posts c 
WHERE SectionID = @BlogSectionId 
    AND PostLevel = 1 
    AND ApplicationPostType = 2 

-- Insert posts 
INSERT INTO [NewDatabaseName].dbo.be_Posts 
( 
    PostID, 
    Title, 
    [Description], 
    PostContent, 
    DateCreated, 
    DateModified, 
    Author, 
    IsPublished, 
    IsCommentEnabled, 
    Slug, 
    CSPostID 
) 
SELECT NEWID(), 
    [Subject], 
    ([OldDatabaseName].dbo.FetchExtendendAttributeValue('Excerpt', c.PropertyNames, c.PropertyValues)), 
    Body, 
    PostDate, 
    PostDate, 
    '[AccountId]', 
    1, 
    1, 
    CASE WHEN ISNULL(PostName, '') = '' THEN 
        CAST(PostID AS NVARCHAR(255)) 
    ELSE 
        PostName 
    END, 
    PostID 
FROM [OldDatabaseName].dbo.cs_Posts c 
WHERE SectionID = @BlogSectionId 
    AND PostLevel = 1 
    AND ApplicationPostType = 1 

-- Insert comments 
INSERT INTO [NewDatabaseName].dbo.be_PostComment 
( 
    PostID, 
    CommentDate, 
    Author, 
    Email, 
    Website, 
    Comment, 
    Ip, 
    IsApproved 
) 
SELECT b.PostID, 
    c.PostDate, 
    ([OldDatabaseName].dbo.FetchExtendendAttributeValue('SubmittedUserName', c.PropertyNames, c.PropertyValues)), 
    'nobody@[DomainName]', 
    ([OldDatabaseName].dbo.FetchExtendendAttributeValue('TitleUrl', c.PropertyNames, c.PropertyValues)) , 
    c.Body, 
    c.IPAddress, 
    1 
FROM [NewDatabaseName].dbo.be_Posts b 
    INNER JOIN [OldDatabaseName].dbo.cs_posts c ON b.CSPostID = c.ParentID 
WHERE c.SectionId = @BlogSectionId 
    AND c.PostLevel = 2 
    AND c.PostType = 1 
    AND c.ApplicationPostType <> 8 

-- Insert trackbacks 
INSERT INTO [NewDatabaseName].dbo.be_PostComment 
( 
    PostID, 
    CommentDate, 
    Author, 
    Email, 
    Website, 
    Comment, 
    Ip, 
    IsApproved 
) 
SELECT b.PostID, 
    c.PostDate, 
    ([OldDatabaseName].dbo.FetchExtendendAttributeValue('trackbackName', c.PropertyNames, c.PropertyValues)), 
    'trackback', 
    ([OldDatabaseName].dbo.FetchExtendendAttributeValue('TitleUrl', c.PropertyNames, c.PropertyValues)) , 
    'Trackback from ' + ([OldDatabaseName].dbo.FetchExtendendAttributeValue('trackbackName', c.PropertyNames, c.PropertyValues)) + '\n\n' + CAST(c.Body AS NVARCHAR(MAX)), 
    c.IPAddress, 
    1 
FROM [NewDatabaseName].dbo.be_Posts b 
    INNER JOIN [OldDatabaseName].dbo.cs_posts c ON b.CSPostID = c.ParentID 
WHERE c.SectionId = @BlogSectionId 
    AND c.PostLevel = 2 
    AND c.PostType = 1 
    AND c.ApplicationPostType = 8 

UPDATE [NewDatabaseName].dbo.be_PostComment 
SET Author = '[AccountName]' 
WHERE Author IS NULL 

-- Categories -------------------------------------------- 

INSERT INTO [NewDatabaseName].dbo.be_categories 
( 
    CategoryName, 
    CSCategoryID 
) 
SELECT [Name], 
    CategoryID 
FROM [OldDatabaseName].dbo.cs_post_categories c 
WHERE c.IsEnabled = 1 
    AND SectionID = @BlogSectionId 

INSERT INTO [NewDatabaseName].dbo.be_postcategory 
( 
    PostID, 
    CategoryID 
) 
SELECT b.postID, 
    bc.CategoryID 
FROM [NewDatabaseName].dbo.be_categories bc 
    INNER JOIN [OldDatabaseName].dbo.cs_post_categories c ON c.CategoryID = bc.CSCategoryID 
    INNER JOIN  [OldDatabaseName].dbo.cs_posts_incategories cic ON c.CategoryID = cic.CategoryID 
    INNER JOIN  [OldDatabaseName].dbo.cs_posts p ON cic.PostId = p.PostId 
    INNER JOIN [NewDatabaseName].dbo.be_posts b ON b.CSPostID = p.postID 

GO 

-- Clean up 

ALTER TABLE [NewDatabaseName].dbo.be_Posts 
DROP COLUMN CSPostID 
GO 

ALTER TABLE [NewDatabaseName].dbo.be_categories 
DROP COLUMN CSCategoryID 
GO 

I haven't run this script for a few weeks now and I did do a few other little manual scripts at the time. As usual, use this at your own risk and definitely don't use this on a production database without testing it thoroughly first.

I also had a script to clean up the membership store, but I can't publish that one without giving away some secrets.

Load test results not available

I have received an email report from TFS for a nightly team build that I have set up. The build ran the unit tests successfully, but failed on some of the load tests. The warning provided is Warning: only a part of test result was loaded because test type implementation is not available. If I try to open the test, I get a message box saying There are no Test Result Details available.

Google fails to provide any insight into this error.

Load Test Results Error

Anyone come across this before?

WCF service contract design article

I had a conversation yesterday regarding WCF service contract design with my tech lead at work. Funnily enough, I then got a comment on an old post that afternoon from Ciaran O'Neill which is really about the same topic. I thought that I should write up my thoughts on the subject. See here for the article.

Rory Primrose | Take Thy Gun And Shoot Thy Foot........Repeatedly

Take Thy Gun And Shoot Thy Foot........Repeatedly

For the last couple of weeks, I have been developing a cool ASP.Net app that allows customer administrators to make requests for users to be added to Active Directory and given access to their instances of online products. This has all been working great, until yesterday.

This solution, which is stored in ClearCase, has a lot of projects in it and I wanted to get some good naming of namespaces. I also changed the names of the projects to represent their namespaces. This worked well for all of last week with only problem being a few instances of locked files in VSWebCache, but another rebuild tended to get rid of those.

Yesterday morning, I installed Whidbey Beta 2 to have a bit of a play. At the same time, a colleague made some changes to the ClearCase configuration for the solution. Take Thy Gun.......

All hell starts to break loose. Each time I start the web project, the objects in the ASP.Net assembly can't be found. Is it ClearCase or Beta 2 at fault????

After pulling my hair out for the 1093850923842314th time ( And Shoot Thy Foot........Repeatedly), I hadn't gotten anywhere. I built the solution several times on multiple machines (with and without Beta 2), using several ClearCase views and differing combinations of everything under the sun. There was no consistency as to whether the web project would run or not. 

I finally discovered that as soon as I referenced a project (which was under ClearCase) to my blank test ASP.Net project (which wasn't under ClearCase), I found that it didn't find the web projects objects and the VSWebCache locking compile errors started again. I removed the ClearCase related projects from the web project and the web project would then work again. This would indicate that ClearCase is the problem.    ......Repeatedly.

In the end, it wasn't ClearCase and it wasn't Beta 2 being installed. It turns out that the ASP.Net project got an upset stomach when the namespace was changed from NiceName to PrefixName.NiceName. I don't know if it didn't like multiple names in the namespace or if it threw wobblies because there was a . character in the namespace. 

Needless to say, I have had a very productive two days. The gun is back in the holster, ready for another day.

blog comments powered by Disqus