The Dangers of Hammers (or, Why SRP Isn’t Dangerous)

by A Specializing Generalist 21. November 2009 06:28

If you haven’t noticed, David Cooksey and I are having a bit of a back-and-forth about the Single Responsibility Principle and its use (or misuse) in software development. And I’m obviously not here to talk about the dangers of hammers.   But as David Cooksey has come up with another example of how SRP (the Single Responsibility Principle) is dangerous when completely misunderstood, I figured I had to reply with something.

I’m not going to spend any time discussing the code in his latest example, as I don’t think there’s anything new to discuss.  It’s just another “wrong-headed-application-of-SRP-causes-bad-code” post.

I guess we both agree that many people don’t necessarily understand SRP correctly (and, honestly, I’m sure I don’t always apply it perfectly every time I try either).  However,  it feels to me that David wants us to fear using SRP, and I want to try to help people understand how to apply it properly.

Rather than continue to rehash what SRP means to me, I’d like to defer to those from whom I’ve learned and point you at some good articles on SRP, The Open/Closed principle, and other SOLID topics in the hopes that you can see past the fear and start applying these powerful tools properly and without fear.

Jeremy Miller (of StructureMap fame, if you don’t already know) wrote a nice article in the June 2008 MSDN Magazine entitled The Open Closed Principle, in which he also talks about SRP.  In his words:

The point of the Single Responsibility Principle isn't just to write smaller classes and methods. The point is that each class should implement a cohesive set of related functions. An easy way to follow the Single Responsibility Principle is to constantly ask yourself whether every method and operation of a class is directly related to the name of that class. If you find some methods that do not fit with the name of the class, you should consider moving those methods to another class.

I highly recommend reading his article in its entirety (as well as about anything else I’ve ever read that he has written), as I’m pretty sure that article is the subconscious inspiration for my refactoring of David’s original post.

Another good reference is the originator of the grouping of principles we know as the SOLID principles, Robert Martin.  In his post “Getting a Solid Start” “Uncle Bob” says:

Following the rules on the paint can won’t teach you how to paint

This is an important point. Principles will not turn a bad programmer into a good programmer. Principles have to be applied with judgement. If they are applied by rote it is just as bad as if they are not applied at all.

Having said that, if you want to paint well, I suggest you learn the rules on the paint can. You may not agree with them all. You may not always apply the ones you do agree with. But you’d better know them. Knowledge of the principles and patterns gives you the justification to decide when and where to apply them. If you don’t know them, your decisions are much more arbitrary.

I could make the same arguments David makes about SRP for just about any software development technique.  Have you ever seen a 12-level-deep inheritance structure used to avoid a bunch of nested if statements (or, god-forbid, the 1200-line method filled with those nested if statements) when applying The Chain of Responsibility Pattern or some other form of composition would have been a much better, and more maintainable, solution.  So, is inheritance dangerous?  Yes, if used incorrectly.

In conclusion, hammers are not dangerous when used correctly.  However, most tools are dangerous when they are misused, and thousands of people hit themselves in the thumb with a hammer every year.  Hammers are dangerous - handle with care!

Cross-posted from my blog @ http://weblogs.asp.net/drohrer

In response to "The Dangers of Single Responsibility in Programming"

by A Specializing Generalist 10. November 2009 23:55

David Cooksey writes an interesting article titled "The Dangers of Single Responsibility in Programming" in which he proposes that there is a certain level of granularity below which SRP is not appropriate.  Although I understand where he's coming from, I tend to disagree with his conclusion, and I think it's because his hypothetical programmer didn't actually find the right responsibilities, not because the method was already granular enough.  I'd propose a slightly different breakdown of the responsibilities, leveraging Inversion of Control and creating several Pricing Calculators that each handle a different kind of discount (In his scenario, there are Sales prices and Gold Level Discounts).  I would see each of these items as their own calculator, and a separate "Pricer" class that would use each of these.  Note that in a real application I would probably leverage something like StructureMap to find all classes that implement IPriceCalculator and have some way to control the orderring of these (probably a Priority field on the IPriceCalculator interface to sort by) but to keep things simple I'm hard-coding the calculators list in this example.  So, something like this:

   public interface IProduct

    {

        decimal BasePrice { get; }

        decimal SalesPrice { get; }

        decimal GoldLevelDiscount { get; }

        bool IsOnSale { get; }

    }

 

    public interface ICustomer

    {

        bool HasFixedDiscountAgreement { get; }

        decimal FixedDiscount { get; }

        bool IsGoldLevelCustomer { get; }

    }

 

    public class Pricer

    {

        private List<IPriceCalculator> _calculators = new List<IPriceCalculator>

          {

              new GoldLevelPriceCalculator(),

              new SalePriceCalculator()

          };

        public decimal GetPrice(IProduct product, ICustomer customer)

        {

            decimal price = product.BasePrice;

            foreach (IPriceCalculator calculator in _calculators)

            {

                price = calculator.CalculatePrice(price, customer, product);

            }

            return price;

        }

    }

 

    public interface IPriceCalculator

    {

        decimal CalculatePrice(decimal price, ICustomer customer, IProduct product);

    }

 

    public class GoldLevelPriceCalculator: IPriceCalculator

    {

        public decimal CalculatePrice(decimal price, ICustomer customer, IProduct product)

        {

            if (customer.IsGoldLevelCustomer)

            {

                price = price*(1 - product.GoldLevelDiscount);

            }

            return price;

        }

    }

 

    public class SalePriceCalculator: IPriceCalculator

    {

        public decimal CalculatePrice(decimal price, ICustomer customer, IProduct product)

        {

            if (product.IsOnSale && !customer.HasFixedDiscountAgreement)

            {

                price = product.SalesPrice < price ? product.SalesPrice : price;

            }

            return price;

        }

    }

Notice that now, each type of discount is contained in its own class which takes the customer, product, and current price and applies its business rules to the price.  In this way, the GetPrice method isn't going to turn into a Monster Method as new business rules are added. Also notice that it's easy to add a new pricing calculator.   So, for grins, let's add the "FixedDiscount" version alluded to in David's example.  First, create a new price calculator:

public class FixedDiscountCalculator: IPriceCalculator

    {

        public decimal CalculatePrice(decimal price, ICustomer customer, IProduct product)

        {

            if (customer.HasFixedDiscountAgreement)

            {

                price = price*(1 - customer.FixedDiscount);

            }

            return price;

        }

    }

Now, we simply add this to the collection of price calculator used by our pricer (again, this would be handled by our DI container of choice in the real world):

private List<IPriceCalculator> _calculators = new List<IPriceCalculator>

                                                          {

                                                              new GoldLevelPriceCalculator(),

                                                              new SalePriceCalculator(),

                                                              new FixedDiscountCalculator()

                                                          };

And we're done.  Now, we've got a well-factored system which follows SRP and is easily extendable with new pricing rules (which, I would assume, could become significantly more complex than the examples given).

Well, we're not really done - if the theoretical developer had actually modified this original functionality in a Test-driven manner, you'd have a large number of unit tests to prove that you didn't break anything when you added the new FixedDiscountCalculator.  Note that I'm a fan of RhinoMocks, and leveraged it so I don't actually care about where my customer or product information comes from (there's actually no implementation of these interfaces at all - they should be loaded using your favorite repository-pattern-ORM-leveraging-code).  For the sake of keeping this post short, I've uploaded the complete source with unit tests as PricingCalculator.txt - rename to .cs, add NUnit and RhinoMocks references, and you're good to go.

In conclusion, Is SRP dangerous?  IMHO, not when properly applied.  But that's true for most SOLID principles - you need to know how to apply them appropriately.

 

Cross-posted from my blog @ http://weblogs.asp.net/drohrer

Tags:

SOLID

Silverlight ChartHelper revisited...

by Doug 11. June 2009 09:59

For my current project, I'm integrating the Silverlight Toolkit's charting functionality with our application.  As part of that effort, I came across this excellent post by Jeremiah Morrill on binding multiple chart series, where he provided the code to an attached behavior that supports this scenario.  However, I had a few additional requirements that his original didn't handle.  Most notably, we needed the ability to set the X and Y axis titles and to set the minimum and maximum values on each axis.  Although I'm not going to copy all of the code here, there were a few interesting challenges in implementing these changes that I'd like to discuss.  The complete source code for my updated example is available for download. (Updated to rename the tile .txt so it is downloadable)

The Silverlight toolkit's charting module is incredibly modularized, being broken up into axes, series, etc. object hierarchies which together display the chart data. This allows for a great deal of flexibility, but also makes for some "hoop-jumping" to figure out when in the lifecycle of the chart/series/axes you need to set certain items.

When you are creating your series manually via XAML, it is obvious that these options are easily available.  However, when utilizing the ChartHelper, there was no obvious way to get access to the axes when adding the series, as the chart selects the axes based on the series type you add.

            // NOTE: In order to change the Title, Maximum, and Minimum properties of the axes, you must handle the Axes_CollectionChanged event.
            // However, you only want this event to fire once, so we always remove our handler here, and then re-add it to make sure we don't have
            // an ever-increasing number of calls to Axes_CollectionChanged
            ((ISeriesHost)chart).Axes.CollectionChanged -= new NotifyCollectionChangedEventHandler(Axes_CollectionChanged);
            ((ISeriesHost)chart).Axes.CollectionChanged +=new NotifyCollectionChangedEventHandler(Axes_CollectionChanged);
        }
 
        /// <summary>
        /// Handles the CollectionChanged event of the Axes control.  Here, X and Y axis titles and min/max properties are set once the graph creates or assigns the axes we need.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="ccEventArgs">The <see cref="System.Collections.Specialized.NotifyCollectionChangedEventArgs"/> instance containing the event data.</param>
        static void Axes_CollectionChanged(object sender, NotifyCollectionChangedEventArgs ccEventArgs)
        {
            if (ccEventArgs.Action != NotifyCollectionChangedAction.Remove)
            {
                Chart chart = null;
                foreach (DisplayAxis axis in ccEventArgs.NewItems)
                {
                    chart = (Chart)axis.SeriesHost;
                    if ((axis.Orientation == AxisOrientation.X && GetSeriesType(chart)!=SeriesType.Bar) || (axis.Orientation == AxisOrientation.Y && GetSeriesType(chart)==SeriesType.Bar))
                    {
                        axis.SetBinding(DisplayAxis.TitleProperty, new Binding(GetXAxisTitle(chart)));
                        if (axis is LinearAxis)
                        {
                            axis.SetBinding(LinearAxis.MinimumProperty, new Binding(GetXAxisMinimum(chart)));
                            axis.SetBinding(LinearAxis.MaximumProperty, new Binding(GetXAxisMaximum(chart)));
                        }
                        else if (axis is DateTimeAxis)
                        {
                            axis.SetBinding(DateTimeAxis.MinimumProperty, new Binding(GetXAxisMinimum(chart)));
                            axis.SetBinding(DateTimeAxis.MaximumProperty, new Binding(GetXAxisMaximum(chart)));
                        }
                    }
                    else
                    {
                        axis.SetBinding(DisplayAxis.TitleProperty, new Binding(GetYAxisTitle(chart)));
                        if (axis is LinearAxis)
                        {
                            axis.SetBinding(LinearAxis.MinimumProperty, new Binding(GetYAxisMinimum(chart)));
                            axis.SetBinding(LinearAxis.MaximumProperty, new Binding(GetYAxisMaximum(chart)));
                        }
                        else if (axis is DateTimeAxis)
                        {
                            axis.SetBinding(DateTimeAxis.MinimumProperty, new Binding(GetYAxisMinimum(chart)));
                            axis.SetBinding(DateTimeAxis.MaximumProperty, new Binding(GetYAxisMaximum(chart)));
                        }
                    }
                }
            }    
        }

The trick (at least as far as I understand so far) is to attach a handler to the chart's Axes collection's CollectionChanged event and, in that event, apply the appropriate bindings to the axes.  So, at the end of Jeremiah's SeriesSourceChanged event, I get a reference to the chart's axes collection and add the event handler.  Note that I first remove, and then re-add the event handler.  The reason for this is that there's no other way I could find to remove the previous delegate before adding the new one when the series source had multiple changes (which it often does in my case).  The relevant code is below:

Note that I've handled Linear and DateTime axes so far - this handles the situations I've run into at this point, but it may need to be extended.  Also note that the only reason I've got these if statements is because the minimum and maximum dependency properties are defined separately on each axis type, which is something of a pain.  perhaps DateTime and Linear axes could at some point derive from a common base where the Min and Max properties are defined, so this kind of code would be unnecessary.

The other trick is that, for Bar-type charts, you really need to treat X and Y axes opposite of every other chart type, as the bar type switches the dependent and independent axes.

I hope this is helpful to someone out there, and thanks again to Jeremiah (Jer?) for the initial implementation.

P.S. - I apologize for the long line lengths - I have 1924608 monitors everywhere and have a tendency to let my code get wider than it should be, which I've now seen quite dramatically in this post.

Doug 

Tags:

Silverlight

Name change (and what is this place about anyway?)

by A Specializing Generalist 14. May 2009 20:58

So, I kind of rushed to get this site set up after my bicycle accident so that I could let people know I was OK, so my original site title was a bit lame.  As I've been thinking about what it is my blog is going to  be about, I realized that trying to pick a specific topic wasn't going to work very well.  My career as a software developer started when I was 16, when I did some (fairly simple) digital signal processing work on a MASSCOMP unix-based system in C.  From there, I worked in C, Assembly Language, Microsoft Access (yes, Access), Delphi, VB6, VB.Net, and C# (with a few other lesser known languages and IDEs in between).

One of the things this broad range of experiences brought me was the ability to quickly get up to speed on new languages/technologies, and in many cases this ability has influenced my work such that I end up working on lots of different things.  For example, in the last two months I've worked on Silverlight 3.0, Community Server 2008, Lucene, WCF, and our internal HTML/JavaScript client-side framework.

Given the wide variety of development work I do, "A Specializing Generalist" seemed to make the most sense, as that's always what I've been.  I believe that having a strong understanding of basic and advanced computer science concepts along with the ability to take "deep dives" into specific areas allows a software developer to easily adapt to new circumstances and more easily identify opportunities for improvements.

Therefore, "A Specializing Generalist" it is.  I hope some of my random ramblings will be helpful for others.i

Tags:

Technology

The crash site...

by A Specializing Generalist 30. April 2009 10:48

In case you're interested in understanding where Adam and I crashed...

http://maps.live.com/default.aspx?v=2&FORM=LMLTCC&cp=qj8wz47yym0s&style=b&lvl=1&tilt=-90&dir=0&alt=-1000&phx=0&phy=0&phscl=1&scene=34992346&sp=Point.qj8wqf7yykt5_Crash%20Site____&encType=1

Note the raised curb in the middle of the sidewalk - unfortunately, they didn' cut the sidewalk area out, so there's about a 6 inch rise in the middle of the sidewalk because of the now-useless "don't turn left out of this empty field" curb.  It appears that there use to be an office complex of some kind at this location.

 

Tags:

Personal

Yup - it's broke...

by A Specializing Generalist 28. April 2009 08:16

So, it seems that we have a winner in the "is it or isn't it broken" race... broken.  I have a "radial head fracture" in my left arm.  Thankfully, it's not bad (more like a hairline fracture than anything more serious) and I'll only be in a splint for about 2 weeks.  After that, I have another 5-6 weeks before the bone is completely healed, so I'm going to have to stay off of the martial arts mats for at least a little while.

All in all, not so bad given how much damage could have been done in an accident like this.

See if you can spot the break in the x-ray (I think I know where it is, but I'm not sure).  Click for a larger image.

Elbow x-ray

Tags:

Personal

Community Server Enterprise Search Integration (Part 1)

by A Specializing Generalist 28. April 2009 02:19

As I've been working on integrating our internal help-desk product PowerHelp with Community Server, one of the big challenges was to figure out how to get our internal wiki to not only display as an integrated part of Community Server 2008, but also to allow users to search our wiki content from the main Community Server search system.

Internally, Community Server's "Enterprise Search" functionality leverages Lucene.net to handle full-text searches.  Lucene (and it's .Net port) are very well known in the search world and Lucene.net makes a great platform for supporting full-text + meta data searches across almost any kind of content.

After spending some time learning the basics of Lucene and how it works, I dove into Google's full text index to see if there was any easily-accessible examples of how to integrate with Community Server's enterprise search.  No such luck.

The rest of this article will describe what I did to get my data indexed, and some of the techniques I used to discover the specifics of Community Server's Enterprise Search's interaction with Lucene to get my data out of the index and onto the screen.

The best information I found in my search was at Marc Mecca's blog, which had some basic information about what class you may have to derive from in order to get your index included in the Community Server index (it's CommunityServer.Enterprise.Search.IndexTask if you're impatient).

Undeterred, and armed with my favorite code spelunking tool (Reflector, now by RedGate Software) I began my efforts to get my custom wiki content indexed.

At first glance, this doesn't seem so hard, and the implementation of an IndexTask-derived class is pretty simple.  Simply override GetDocuments() and return a collection of Lucene Document objects, one for each article you want indexed.  Telligent has provided a task scheduler component that will run your indexer (along with the out-of-box indexers) by simply adding your component to the correct configuration file, restart the indexing service, and you're off to the races.  Or so it seamed at first.

My first attempt 

Being naive in the ways of Lucene.Net and Community Server, I thought I would start by looking at what Telligent did for their WIki post indexing task and see if I could do something similar to get my data into the index.  So, I created my own WikiSearchTask, overrode GetDocuments(), and looped through my data adding each document to the return collection, something like this:

public class WikiSearchTask: IndexTask

    {

        public WikiSearchTask(int settingsid, int itemsToIndex)

            : base(settingsid, itemsToIndex)

        {

        }

        public override DocumentCollection GetDocuments()

        {

            DocumentCollection dc = new DocumentCollection();

            foreach (WikiArticle a in GetWikiArticles())

            {

                Document doc = CreateDocument(a);

                dc.Add(doc);

            {

            return dc;

        }

    }

}

 Seems simple enough (I've omitted the CreateDocument() method for brevity here - we'll get to that later), and it looked at first glance like it might actually work.  I added my type to the tasks.config file in the "Telligent.Tasks for Enterprise Search" folder, like so:

 

<task name = "ES.SearchJob" type = "CommunityServer.Enterprise.Search.SearchJob, CommunityServer.Enterprise.Search" count = "100" enabled = "true" optimize = "false" enableShutDown = "false">

        <!-- <add type = "CommunityServer.Enterprise.Search.WeblogIndexTask, CommunityServer.Enterprise.Search" />

        <add type = "CommunityServer.Enterprise.Search.ForumsIndexTask, CommunityServer.Enterprise.Search" />

        <add type = "CommunityServer.Enterprise.Search.HubIndexTask, CommunityServer.Enterprise.Search" /> -->

        <!--<add type = "CommunityServer.Enterprise.Search.WikiIndexTask, CommunityServer.Enterprise.Search" /> -->

        <add type = "VSI.CommunityServer.EnterpriseSearch.WikiSearchTask, VSI.CommunityServer.EnterpriseSearch" />

        <!-- <add type = "CommunityServer.Enterprise.Search.MediaGalleriesIndexTask, CommunityServer.Enterprise.Search" /> -->

      </task>

I commented out all of the other tasks so I could see if my task was doing something useful and, sure enough, I saw the Lucene index get created the next time the indexing service ran.  However, when I did a search for text I knew was in the documents I was indexing, nothing came up in Community Server's search results.

Knowing that I had Enterprise Search working before I started this exercise, I uncommented the other Community Server tasks above and re-ran the indexing service.  This time, all of my CS content showed up when I searched, so I knew that fundamentally the indexing service was working.  I also attached a debugger to Telligent.Tasks.Console.exe to make sure that my task was running.  Sure enough, I saw my document collection being created and returned from the GetDocuments method, so I knew I was handing back the data to be indexed.  My next trick was to figure out why none of my information was getting into the index.

When all else fails, unit test...

Next, I created a fairly simple set of NUnit tests that would call my GetDocuments method, use Lucene.Net itself to create the index, and then search to make sure my data returned correctly.  This gave me some more insight into how the individual pieces all fit together, and I came up with a few test methods (note I have recreated this code from memory and it may not be exactly correct or even compile):

    [TestFixture]

    public class SearchTests

    {

        private static FileInfo INDEX_DIR = new FileInfo(GetSearchDirectory());

        [Test]

        public void CreateIndex()

        {

            // First, create a new index

            WikiSearchTask task = new WikiSearchTask(1, 100);

            DocumentCollection dc = task.GetDocuments();

            IndexWriter writer = new IndexWriter(INDEX_DIR, new StandardAnalyzer(), true);

            foreach (Document d in dc)

            {

                writer.AddDocument(d);

            }

            task.ResetIndexStatus();

            // Optimize the index

            // writer.Optimize();

            // Close the writer

            writer.Close();

            // no asserts - at this point we just want to set up the test

        }

 

        [Test]

        public void SearchContent()

        {

            // Searcher searcher = new IndexSearcher(@"Z:\CommunityServer\SearchIndex");

            Searcher searcher = new IndexSearcher(GetSearchDirectory());

            Analyzer analyzer = new StandardAnalyzer();

 

            string queryStr = "wiki";

            QueryParser qp = new QueryParser("body", analyzer);

            Hits hits = searcher.Search(qp.Parse(queryStr), new VspFilter(es));

            Assert.IsTrue(hits.Length() > 0, "Should have been at least one hit");

            for (int i=0;i<hits.Length();i++)

            {

                Document doc = hits.Doc(i);

                DumpDoc(doc);

            }

            searcher.Close();

        }

 

        private void DumpDoc(Document doc)

        {

            doc.GetField(Fields.GroupID).StringValue();

            Console.WriteLine("-----------------------------");

            foreach (Field f in doc.GetFields())

            {

                Console.WriteLine(string.Format("{0}:{1}", f.Name(), f.StringValue()));

            }

            Console.WriteLine("-----------------------------");

        }

    }

}

I Fire up NUnit, run CreateIndex, run SearchContent, and, sure enough, my documents show up in the Hits returned from Lucene.net.  So, where is Community Server disconnecting my index results from the rest of its results.  The key is in a few specific document fields you must include in the Document objects you return from GetDocuments().  Specifically, Enterprise Search uses a Lucene.Net class called Filter, in a derived obfuscated class that handles, from what I can tell, all of the post-search security features of Enterprise Search to exclude documents to which the currently searching user has no access rights.  Although I didn't completely analyze the post-search filtering, I did find that there are several fields that are included in that filtering and, in my case, the one that had caused my search results to be excluded was HubSectionID.  This is an internal ID that links parent/child hubs/sections together in Community Server.  Thankfully, my indexing task already filtered out the non-public documents before they were added to the search (as we only want to expose public documents at this time) so I could simply set HubSectionID to 0, which allowed my search documents to show up in the results.

Almost there...

Once I got this far, I started seeing the dreaded NullReferenceException show up when I did a search.  Now, I was sure my documents were in the result set because I was blowing up the search results screen.  As Community Server is really designed around the concept of a Post, everything that is displayed on the search results screen appears to be converted to an ESIndexPost object before being bound to the result list, and if you don't include all of the fields that are expected to be there in your Lucene document, it will throw exceptions trying to build the results.  I found through reflector that the following fields in the Lucene document are used to build the search results, and this code (from Reflector), as you can see, does not confirm that the field data is actually there before trying to convert the fields to the appropriate data type:

    private ESIndexPost CreateESIndexPost(Document document1)

        {

            ESIndexPost post = new ESIndexPost();

            post.ESId = 1;

            post.ApplicationKey = document1.GetField(Fields.ApplicationKey).StringValue();

            post.ApplicationType = (ApplicationType)Enum.Parse(typeof(ApplicationType),

                document1.GetField(Fields.ApplicationType).StringValue(), true);

            post.Body = document1.GetField(Fields.RawBody).StringValue();

            post.GroupID = int.Parse(document1.GetField(Fields.GroupID).StringValue());

            post.Name = document1.GetField(Fields.Name).StringValue();

            post.PostDate = DateTools.StringToDate(document1.GetField(Fields.PostDate).StringValue());

            post.PostID = int.Parse(document1.GetField(Fields.PostID).StringValue());

            post.SectionID = int.Parse(document1.GetField(Fields.SectionID).StringValue());

            post.SettingsID = int.Parse(document1.GetField(Fields.SettingsID).StringValue());

            post.Title = document1.GetField(Fields.Title).StringValue();

            post.Url = document1.GetField(Fields.Url).StringValue();

            post.UserName = document1.GetField(Fields.Author).StringValue();

            post.ThreadID = int.Parse(document1.GetField(Fields.ThreadID).StringValue());

            post.SectionName = document1.GetField(Fields.Name).StringValue();

            post.PostCategories = document1.GetValues(Fields.Tag);

            return post;

 

        }

 So, with all of this spelunking done, here's what my final CreateDocument() method (previously omitted) contained:

        private Document CreateDocument(WikiArticle a)

        {

            Document doc = new Document();

            HtmlDocument html = new HtmlDocument();

            html.ContentType = "text/html";

            html.Extension = "aspx";

            html.Encoding = "utf-8";

            html.Html = a.ArticleHtml;

            doc.Add(new Field(Fields.Body, html.WordsOnly, Field.Store.YES, Field.Index.TOKENIZED));

            doc.Add(new Field(Fields.Name, a.Descr, Field.Store.YES, Field.Index.TOKENIZED));

            doc.Add(new Field(Fields.Title, a.Descr, Field.Store.YES, Field.Index.TOKENIZED));

            doc.Add(new Field(Fields.Url, string.Format("/cs/PowerHelpWiki/WikiPage.aspx?id={0}", a.ArticleID),

                Field.Store.YES, Field.Index.UN_TOKENIZED));

            doc.Add(new Field(Fields.ApplicationType, ApplicationType.Unknown.ToString(), Field.Store.YES,

                Field.Index.UN_TOKENIZED));

            doc.Add(new Field(Fields.ApplicationKey, a.ArticleID, Field.Store.YES, Field.Index.UN_TOKENIZED));

            doc.Add(new Field(Fields.RawBody, a.ArticleHtml, Field.Store.YES, Field.Index.NO));

            doc.Add(new Field(Fields.Author, "admin", Field.Store.YES, Field.Index.UN_TOKENIZED));

            doc.Add(new Field(Fields.AuthorID, "admin", Field.Store.YES, Field.Index.UN_TOKENIZED));

            doc.Add(new Field(Fields.PostDate, DateTools.DateToString(a.UpdateDate, DateTools.Resolution.MINUTE),

                Field.Store.YES, Field.Index.UN_TOKENIZED));

            // Add "Everyone" permission so anonymous can search

            doc.Add(new Field(Fields.Role, "Everyone", Field.Store.YES, Field.Index.UN_TOKENIZED));

            // Add required CS fields that aren't available in VSP Wiki but will fail the search

            doc.Add(new Field(Fields.GroupID, "0", Field.Store.YES, Field.Index.UN_TOKENIZED));

            doc.Add(new Field(Fields.PostID, "0", Field.Store.YES, Field.Index.UN_TOKENIZED));

            doc.Add(new Field(Fields.SectionID, "0", Field.Store.YES, Field.Index.UN_TOKENIZED));

            doc.Add(new Field(Fields.HubSectionID, "0", Field.Store.YES, Field.Index.UN_TOKENIZED));

            doc.Add(new Field(Fields.SettingsID, this.SettingsID.ToString(), Field.Store.YES, Field.Index.UN_TOKENIZED));

            doc.Add(new Field(Fields.ThreadID, "0", Field.Store.YES, Field.Index.UN_TOKENIZED));

            // Todo: deal with tags once we have them in VSP

            // Do a foreach on each tag and add it to the document (Lucene supports multiple entries for a single field)

            // doc.Add(new Field(Fields.Tag, "", Field.Store.YES, Field.Index.UN_TOKENIZED));

            doc.SetBoost(CalculateBoost(a));

            return doc;

        }

This should get anyone interested in integrating their content into Telligent's Enterprise Search results well on their way toward their goal.  Next time I'll talk about some additional gotchas I ran into once the basic functionality was working, including things like deleting updated or removed documents from the index and only indexing articles when they have been updated, as your index fills up with duplicates fairly quickly if you don't pay attention to these things.  Additionally, I'll be writing about my efforts to integrate our web-services based security system with Community Server, which makes some assumptions about data being in specific tables even when it isn't the membership provider, and how I worked around them to get our two systems working together.

I hope this helps somebody else avoid some of the pain I went through getting this all to work.

Tags:

Community Server

About Doug

I am a father, husband, and software developer.  I've been slinging code professionally since I was 16, and many years before that.

I aspire to be a Software Craftsman, which requires quite a bit of re-education.

Page List