Using Lucene .Net to index and search website content

by andrei 15. November 2008 03:49

Lucene is an elegant solution for adding content indexing and search in a .Net application.

 

After doing a few Google searches and also studying this excellent basic sample, I wrote a simple implementation which automatically indexes entities implementing ISearchable when they are saved, and offers a simple way to search the website content.

 

Here it is:

  • in the application infrastructure:
    • an interface and a general class implementing the interface:
public interface ISearchable
{
  string Identifier { get; }
  string Name { get; }
  string Details { get; }
  IEnumerable<ISearchable> GetSearchableList();
  string SearchType { get; }
}

class SearchableItem : ISearchable
{
  public string Identifier { get; set; }
  public string Name { get; set; }
  public string Details { get; set; }
  public string SearchType { get; set; }

  public IEnumerable<ISearchable> GetSearchableList()
  {
    yield return this;
  }
}
    • a few helper classes
public class ApplicationSetting
{
  public static string SearchIndexingDirectory
  {
    get
    {
      return ConfigurationManager.AppSettings["SearchIndexingDirectory"];
    }
  }
}
public class Constants
{
  public class Search
  {
    public const string SearchFieldDetailsDelimiter = " | ";
    public const string TagSearchIdentifier = "T";
  }

  public class Domain
  {
    public const int InvalidId = -1;
  }
}

SearchIndexingDirectory uses an application setting from web.config, but you can replace it with a variable or anything representing the path where you want Lucene to generate its index.

 

    • a search service:
public class SearchService
{
  private static bool _indexDirectoryExists;
  
  private void EnsureIndexExists ( string indexDirectory )
  {
    if (_indexDirectoryExists)
      return;

    if (!IndexReader.IndexExists ( indexDirectory ))
    {
      IndexWriter writer = new IndexWriter ( indexDirectory,
          new StandardAnalyzer ( ), true );
      writer.Close ( );

      _indexDirectoryExists = true;
    }
  }

  public void IndexData ( IEnumerable<ISearchable> listToIndex )
  {
    string indexDirectory = ApplicationSetting.SearchIndexingDirectory;
    EnsureIndexExists ( indexDirectory );

    IList<ISearchable> listWithoutDuplicates = new List<ISearchable>();
    foreach (ISearchable item in listToIndex)
    {
      if (listWithoutDuplicates.FirstOrDefault(p => p.Identifier == item.Identifier) == null)
        listWithoutDuplicates.Add(item);
    }

    IndexModifier modifier = new IndexModifier(indexDirectory, new StandardAnalyzer(), false);
    foreach(ISearchable item in listWithoutDuplicates)
    {
      Term term = new Term ( "id", item.Identifier );
      modifier.DeleteDocuments( term );
      InsertDoc ( modifier, item );
    }
    modifier.Optimize ( );
    modifier.Close ( );
  }

  private void InsertDoc ( IndexModifier modifier, ISearchable dataToIndex )
  {
    Document doc = new Document ( );

    doc.Add ( new Field ( "id", dataToIndex.Identifier, Field.Store.YES, Field.Index.UN_TOKENIZED ) );
    doc.Add ( new Field ( "name", dataToIndex.Name, Field.Store.YES, Field.Index.TOKENIZED ) );
    doc.Add ( new Field ( "details", dataToIndex.Details, Field.Store.YES, Field.Index.TOKENIZED ) );
    doc.Add ( new Field ( "type", dataToIndex.SearchType, Field.Store.YES, Field.Index.UN_TOKENIZED ) );

    modifier.AddDocument ( doc );
  }

  public static string PrepareSearchDetailsFieldFor ( params string[] values )
  {
    string ret = String.Empty;

    foreach(string str in values)
      ret += str + Constants.Search.SearchFieldDetailsDelimiter;

    return ret;
  }

  public IList<ISearchable> Search ( string searchString )
  {
    IList<ISearchable> ret = new List<ISearchable>();

    IndexSearcher searcher = new IndexSearcher(ApplicationSetting.SearchIndexingDirectory);

    QueryParser parser = new QueryParser("name", new StandardAnalyzer());

    string searchQuery = "(" + searchString + " OR (details: " + searchString + "))";

    Hits hitColl = searcher.Search(parser.Parse(searchQuery));
    for (int i = 0; i < hitColl.Length ( ); i++)
    {
      Document doc = hitColl.Doc(i);
      ret.Add ( new SearchableItem { Details = doc.Get ( "details" ), Identifier = doc.Get ( "id" ), Name = doc.Get ( "name" ), SearchType = doc.Get ( "type" ) } );
    }

    searcher.Close();

    return ret;
  }

  public static string[] SeparateDetailsFrom(string details)
  {
    return details.Split(new[] {Constants.Search.SearchFieldDetailsDelimiter},
                         StringSplitOptions.RemoveEmptyEntries);
  }

  public static int RetrieveIdFromIdentifier(string input, string searchTypeIdentifier)
  {
    int ret;
    if (Int32.TryParse ( input.Replace( searchTypeIdentifier, "" ), out ret ))
      return ret;

    return Constants.Domain.InvalidId;
  }
}

 

    • a search type visitor, which will be used to select results of a certain type from a list of search results (this can be easily replaced with simple Linq selects):
internal class SearchTypeVisitor : IValueReturningVisitor<IList<ISearchable>,ISearchable>
{
  private readonly string _searchType;
  private readonly IList<ISearchable> _results = new List<ISearchable>();

  public SearchTypeVisitor(string searchType)
  {
    _searchType = searchType;
  }

  public void Visit(ISearchable item)
  {
    if (item.SearchType == _searchType)
      _results.Add(item);
  }

  public IList<ISearchable> GetResult()
  {
    return _results;
  }
}
  • in the Domain:
    • in the repository, when we save an entity we are also indexing the information:
public void Save ( EntityType entity )
{
  SessionHolder.Current.SaveOrUpdate(entity);

  ISearchable searchable = entity as ISearchable;
  if (searchable != null)
    _searchService.IndexData(searchable.GetSearchableList());
}

If you are using a BaseRepository or a generic repository, you will only need to write this once.

 

    • in the entities which should be searchable, we implement ISearchable:
public class Tag: ISearchable
{
  public virtual string Identifier
  {
    get { return Constants.Search.TagSearchIdentifier + Id; }
  }

  public virtual string Name
  {
    get { return TagName; }
  }

  public virtual string Details
  {
    get
    {
      return SearchService.PrepareSearchDetailsFieldFor(
        TagCount.ToString(),
        ((int) TagType).ToString()
        );
    }
  }

  public virtual IEnumerable<ISearchable> GetSearchableList ( )
  {
    yield return this;
  }

  public virtual string SearchType
  {
    get { return Constants.Search.TagSearchIdentifier; }
  }
}

 

If the entity is an aggregate containing subentities which should be indexed, we can also add those in GetSearchableList:

public virtual IEnumerable<ISearchable> GetSearchableList ( )
{
  yield return this;
  foreach (Subentity s in Subentities)
    yield return s;
}
    • we can also implement mappers, if we want to generate entities from the search results:
internal class TagFromSearchableMapper : IMapper<ISearchable, Tag>
{
  public Tag MapFrom ( ISearchable input )
  {
    string[] details = SearchService.SeparateDetailsFrom ( input.Details );

    int tagCount;
    Int32.TryParse ( details[0], out tagCount );

    return new Tag ( input.Name, (enumTagType)Enum.Parse ( typeof(enumTagType), details[1] ) )
             {
               TagCount = tagCount,
               Id =
                 SearchService.RetrieveIdFromIdentifier ( input.Identifier, Constants.Search.TagSearchIdentifier )
             };
  }
}

Otherwise, we will display the information directly from the list of search results.

 

  • in the application layer, a SearchTask which can return the result (a search data transfer object) when the user runs a search:
public class SearchTask
{
  readonly SearchService _searchService = new SearchService ( );

  public SearchDTO Search ( string text )
  {
    IList<ISearchable> searchResults = _searchService.Search(text);

    IEnumerable<Tag> tags =
      searchResults.GetResultOfVisitingAllItemsWith(new SearchTypeVisitor(Constants.Search.TagSearchIdentifier)).
        MapAllUsing(new TagFromSearchableMapper());

    return new SearchDTO
             {
               Tags = tags.ToList()
             };
  }
}
public class SearchDTO
{
  public IList<Tag> Tags { get; set; }
}

 

This class uses the SearchTypeVisitor to filter the tags, and maps the list of ISearchable instances into an IEnumerable<Tag> with the help of the TagFromSearchableMapper class.

GetResultOfVisitingAllItemsWith and MapAllUsing are extensions which help in applying a visitor or mapper to an entire list of items. To obtain the same result, just go over the list manually and apply the desired action on each of the items.

The SearchTask and the SearchDTO from this example are very simple, but they can contain more logic or information depending on the case.

 

  • in the UI, calls to the SearchTask will bring the desired information:
readonly SearchTask _searchTask = new SearchTask();
SearchDTO dto = _searchTask.Search(searchText);

 

 

This solution is only a starting point, and it can surely be improved.

One of the things which should change is the fact that the domain contains information about the search infrastructure, which is not a very good thing. A better place for this information are DTO classes in the application layer.

Maybe I will talk more about this in a future post.

 

 

Enjoy programming!

 

Base fluent repository

by andrei 15. November 2008 01:39

In some previous posts (here and here) I was describing some ideas related to fluent repositories.

 

Here is an idea to bring some of that functionality into a base repository, and obtain less repetitive code:

public class BaseRepository<EntityType, PKType, RepositoryType> where RepositoryType : class
{
  protected ICriteria _criteria;

  protected ISession Session
  {
    get
    {
      return SessionHolder.Current;
    }
  }

  public RepositoryType InitialiseCriteriaFor ( PKType id )
  {
    InitialiseCriteria();
    _criteria.Add(Restrictions.Eq("Id", id));

    return this as RepositoryType;
  }

  public RepositoryType InitialiseCriteria ( )
  {
    return InitialiseCriteria("");
  }

  public virtual RepositoryType InitialiseCriteria ( string alias )
  {
    if (IEnumerableExtensions.IsEmpty(alias))
      _criteria = Session.CreateCriteria(typeof(EntityType));
    else
    {
      _criteria = Session.CreateCriteria(typeof(EntityType), alias);
    }
    return this as RepositoryType;
  }

  private BaseRepository<EntityType, PKType, RepositoryType> OnlyActive ( )
  {
    IActivable activable = this as IActivable;

    if (activable == null) return this;

    _criteria.Add(Restrictions.Eq(activable.IsActiveField, true));
    return this;
  }

  private BaseRepository<EntityType, PKType, RepositoryType> Sorted ( )
  {
    ISortable sortable = this as ISortable;

    if (sortable == null) return this;

    _criteria.AddOrder(new Order(sortable.SortField, sortable.Ascending));
    return this;
  }

  public RepositoryType InitialiseListCriteria ( )
  {
    InitialiseCriteria();

    return OnlyActive().Sorted() as RepositoryType;
  }

  public EntityType LoadEntity ( )
  {
    return _criteria.UniqueResult<EntityType>();
  }

  public IList<EntityType> LoadList ( )
  {
    return _criteria.List<EntityType>();
  }

  public ICriteria GetCurrentCriteria ( )
  {
    return _criteria;
  }

  public RepositoryType ByIds ( IEnumerable<PKType> ids )
  {
    _criteria.Add(Restrictions.In("Id", (ICollection)ids));

    return this as RepositoryType;
  }

  public RepositoryType MaxResults ( int maxResults )
  {
    _criteria.SetMaxResults(maxResults);
    return this as RepositoryType;
  }

  public virtual bool Ascending
  {
    get
    {
      return true;
    }
  }
}

 

The generic signature contains the type of the repository, but the same result could be obtained with reflection and the parameter would dissapear. I am also using 2 sample interfaces, which bring default sorting and filtering behavior into the BaseRepository:

public interface ISortable
{
  string SortField { get; }
  bool Ascending { get; }
}
public interface IActivable
{
  string IsActiveField { get; }
}

 

Here is how a simple repository would look:

public class TagRepository: BaseRepository<Tag, int, TagRepository>, ISortable
{
  public TagRepository ByTagNames(IList<string> tags)
  {
    _criteria.Add(Restrictions.InG("TagName", tags));
    return this;
  }

  public string SortField
  {
    get { return "TagCount"; }
  }
}

 

and here is how it can be used:

IList<Tag> existingTags = _tagRepository.InitialiseListCriteria().ByTagNames(crtTagNames).LoadList();

This will load all tags which have a name from the given list, and sort them by TagCount.

 

The BaseRepository above contains a few more options, and others can easily be added.

 

 

Enjoy programming!

 

DDD

Another DDD sample

by andrei 14. November 2008 19:56

Here is another DDD learning resource: http://timeandmoney.domainlanguage.com/.

 

 

Enjoy programming!

 

DDD

DDD2 module evaluation

by andrei 4. November 2008 10:14

Here is the evaluation exercise for the DDD2 training module. I also added the information at the module's page.

 

We will use:

  • Clients
  • Products
  • Orders
  • OrderDetails

Also, please read the specification from the DDD1 evaluation to find out any other elements (like DiscountLevel) that are needed, based on what the current specification requires.

 

  • consider that you have a set of orders (like the ones described in the evaluation for DDD1) that are already launched: a few paid orders, and a few bonus orders (you can keep them in the application session)
  • you start on a page where you see the list of orders
    • in the list, you are shown the details of each order
    • you can also see the value of each validated order
      • the value of a validated order is equal to the value of all validated products in it
    • you can filter the orders by type, status (Launched, Validated, Rejected), launch date, validation date, client
    • you can sort the list by clicking on at least one of the column names
    • you can click on a Validate button which exists next to each order
      • when you click on Validate, you are redirected to a page where you see the details of that order and you can validate it
        • you can validate or reject the products one by one (through the corresponding button next to each product), and click on Validate Order outside the products list when you are done
          • by clicking Validate Order, all the products which have not been manually validated or rejected automatically become validated, and the order also becomes validated
        • you can validate or reject the entire order directly, and all its products become validated or rejected automatically

 

  • when you validate an order
  • the initial page also contains the list of clients and their available bonus
    • this should reflect the changes that happen when an order is validated / rejected

 

 

Enjoy programming!

 

Steps for learning object oriented programming

by andrei 4. November 2008 05:01

I think the steps for learning OOP should be:

  • learn OOP basics
  • learn DDD (domain driven design)
  • learn advanced OOP

DDD is an incredible tool for putting OOP in a practical context.

 

 

Enjoy programming!

 

Value objects - practical example

by andrei 4. November 2008 00:22

DDD elements like entities and repositories are easy to understand, and you will be able to use them quite well as soon as you find out about them.

Aggregates and value objects are different: they are a bit more subtle, and it takes some practice to start using them in an efficient way.

The key, as always, is the context: each piece of business logic should be in a place which feels right, and should not demand strange code changes as soon as you need to add something or modify the business flow.

 

Here is an example related to value objects.

Let's say we have this situation: our application has Products and Orders. An Order has OrderDetails, each OrderDetail representing the instance of a Product in that particular Order. Some products (let's call them Profiles) contain other products.

A profile has a fixed number of products, but it also has an attribute which defines how many of its products can be in one OrderDetail. For example, a profile can be 3 of 5, which means that it has 5 products and you can choose any 3 of them when you order it. 3 is the number of SelectableSubproducts.

Each of the products could be ordered independently (without the profile), but the Profile has a better price. Considering our example, if you order any 3 of those products you will pay more than if you order the profile for the 3 products. Let's call this the profile Gain. This means that each profile can have a set of valid product combinations, and each combination has its own Gain.

 

How do we implement this in our code?

To map the concept of a valid profile combination, I created 2 value objects: one for a valid combination of products, and one for a profile combination.

 

This is the combination of products, just a simple list of products:

public class ProductCombinationForProfile
{
  private readonly IList<Product> _products = new List<Product>();

  public IList<Product> Products
  {
    get { return new ReadOnlyCollection<Product>(_products); }
  }

  public void AddProductInCombination(Product p)
  {
    _products.Add(p);
  }

  public override string ToString()
  {
    string ret = string.Empty;

    foreach (Product product in _products)
    {
      ret += product.Id + ", ";
    }

    return ret;
  }
}

 

and this is the profile combination:

public class ProfileCombination
{
  private readonly Product _profile;
  private readonly ProductCombinationForProfile _combinationOfProducts;

  public ProfileCombination(Product profile, ProductCombinationForProfile combinationOfProducts)
  {
    _profile = profile;
    _combinationOfProducts = combinationOfProducts;
  }

  public ProductCombinationForProfile ValidCombinationOfProducts
  {
    get { return _combinationOfProducts; }
  }

  public Product Profile
  {
    get { return _profile; }
  }

  public override string ToString()
  {
    return _profile.Id + "   Subproducts=" + _profile.SelectableSubproducts + "(" + _combinationOfProducts + ")";
  }

  public decimal Gain
  {
    get
    {
      IList<Product> products = _combinationOfProducts.Products;

      decimal individualProductPrice = products.Sum(p => p.ProductDiscountPrice);
      decimal profileProductPrice = _profile.ProductDiscountPrice;

      return individualProductPrice - profileProductPrice;
    }
  }
}

The ToString() overrides are for debugging purposes.

 

Initially, I placed the Gain logic inside a CalculateGain method in the ProfileService class, but I soon realized that the value object is a more appropriate place and I moved it here. The ProfileCombination can calculate its gain. This feels quite right.

 

One interesting twist appeared when we decided to show the gain based on the previous profiles in that order (I won't go into the business details for this part). It seemed that the ProfileCombination wasn't able to handle this well (and calculate its Gain anymore), since it had to work with external entities. A few minutes later, the simple solution was clear:

 

- I did not remove or modify the current Gain method (the cause was the "show" keyword from the previous paragraph: the need was not to calculate the gain in a different way, it was just to show it in a different way)

- I added a new method in the ProfileCombination class:

public decimal GainConsideringPreviousProfiles(IEnumerable<Product> previousProfiles)
{
  decimal previousPrice = PreviousProductsFor(previousProfiles, _combinationOfProducts.Products, _profile).Sum(p => p.ProductDiscountPrice);
  decimal newPrice = _profile.ProductDiscountPrice;

  return previousPrice - newPrice;
}

Based on a list of previous profiles in that order (and using a PreviousProductsFor method which is not relevant here), the ProfileCombination can calculate its gain. It does not care how you get the previous profiles. Again, it feels quite right.

In the end, a simple set of classes proved to be very useful, and it was able to do that only because it created a clear context for the business concept that it describes.

 

Technorati Tags: ,

 

 

Enjoy programming!

 

DDD

Repository with caching

by andrei 25. October 2008 05:45

Here is an idea for a repository which automatically does caching. ProductDetailColumn is a table with very few entries and they are not editable, so it is a very good candidate for caching.

public class ProductDetailColumnRepository
{
  private readonly Repository<ProductDetailColumn> _repository = new Repository<ProductDetailColumn>();
  readonly CacheItemDictionary cache = new CacheItemDictionary();

  private int? _idFilter;
  private int? _columnTypeIdFilter;
  private bool _orderByPosition;

  public ProductDetailColumnRepository InitialiseCriteriaFor(int id)
  {
    ResetVariables();
    _idFilter = id;

    return this;
  }

  private void ResetVariables()
  {
    _idFilter = null;
    _columnTypeIdFilter = null;
    _orderByPosition = false;
  }

  public ProductDetailColumnRepository InitialiseListCriteria()
  {
    ResetVariables();
    return OrderByPosition();
  }

  public ProductDetailColumnRepository OfType(int columnTypeId)
  {
    _columnTypeIdFilter = columnTypeId;

    return this;
  }

  public ProductDetailColumn LoadItem()
  {
    IList<ProductDetailColumn> allProductDetailColumns = GetAllUsingCache();

    return FilterAndSortList(allProductDetailColumns).FirstOrDefault();
  }

  private IList<ProductDetailColumn> GetAllUsingCache()
  {
    IList<ProductDetailColumn> allProductDetailColumns = cache.GetItemIdentifiedBy(CacheKeys.AllProductDetailColumns);

    if (allProductDetailColumns == null)
    {
      allProductDetailColumns = _repository.LoadAll();
      cache.Add(CacheKeys.AllProductDetailColumns, allProductDetailColumns);
    }
    return allProductDetailColumns;
  }

  private IEnumerable<ProductDetailColumn> FilterAndSortList(IEnumerable<ProductDetailColumn> list)
  {
    IEnumerable<ProductDetailColumn> ret = list.ToList();

    if (_columnTypeIdFilter.HasValue)
      ret = ret.Where(p => (int)p.ColumnType == _columnTypeIdFilter.Value);

    if (_idFilter.HasValue)
      ret = ret.Where(p => p.Id == _idFilter);

    if (_orderByPosition)
      ret.OrderBy(p => p.Position);

    return ret;
  }

  public IEnumerable<ProductDetailColumn> LoadList()
  {
    IList<ProductDetailColumn> allProductDetailColumns = GetAllUsingCache();

    return FilterAndSortList(allProductDetailColumns);
  }

  public ProductDetailColumnRepository OrderByPosition()
  {
    _orderByPosition = true;

    return this;
  }
}

 

Here is some sample usage:

ProductDetailColumn col = _productDetailColumnRepository
  .InitialiseListCriteria()
  .OfType((int) enumProductDetailColumn.SubProductColumn)
  .LoadItem();

We are also able to do this:

IEnumerable<ProductDetailColumn> columns = _productDetailColumnRepository.InitialiseListCriteria ( )
  .LoadList ( );

No matter what calls we make and in what order, the repository will make a database call only the first time and use the cache for the following requests.

 

There is one interesting thing to observe. Because of this:

return OrderByPosition();

the returned list will always be ordered by Position.

 

I am using a CacheItemDictionary class, based on a generic dictionary idea from Jean-Paul S. Boodhoo's NothinButDotNetStore application. You can get more implementation details (for IItemKey, for example) from there.

public class CacheItemDictionary
{
    private readonly Cache cache = HttpRuntime.Cache;
    private readonly int _cachingTimeInHours = Constants.Caching.DefaultCachingTimeInHours;

    public CacheItemDictionary ( )
    {
        
    }

    public CacheItemDictionary(int cachingTimeInMinutes)
    {
        _cachingTimeInHours = cachingTimeInMinutes;
    }

    public void Add<ItemToPutInContext> ( IItemKey<ItemToPutInContext> key, ItemToPutInContext item )
    {
        cache.Insert( key.Key, item, null, DateTime.Now.AddHours(_cachingTimeInHours),
            Cache.NoSlidingExpiration );
    }

  public void Add<ItemToPutInContext>(IItemKey<ItemToPutInContext> key, ItemToPutInContext item, int cachingTimeInHours)
  {
    cache.Insert(key.Key, item, null, DateTime.Now.AddHours(cachingTimeInHours), Cache.NoSlidingExpiration);
  }

    public Item GetItemIdentifiedBy<Item> ( IItemKey<Item> key )
    {
        return (Item)cache[key.Key ];
    }
}
 
Technorati Tags: ,,

 

 

Enjoy programming!

 

DDD

DDD sample application

by andrei 25. October 2008 02:59

Here is a new and amazing resource for DDD learning and programming in general: http://dddsample.sourceforge.net/index.html.

 

Be sure to check out the documentation also, starting from here:

http://dddsample.sourceforge.net/characterization.html

http://dddsample.sourceforge.net/patterns-reference.html

 

It is written in Java, but don't worry if you are not a Java developer. If you are doing object oriented programming, taking a dive into good quality Java code will be a pleasure and a great opportunity to learn new things.

 

Technorati Tags: ,,

 

 

Enjoy programming!

 

DDD

DDD2 training module

by andrei 25. October 2008 02:36

DDD2

  • concepts that you need to grasp after this phase

what is a specification, how can it be used in the domain, what are the main variations?
what are the types of classes that can represent business processes in the domain?
what are intention-revealing interfaces, why are they good?
what are side-effect free functions, why are they good?
what are assertions, why are they good, how can they be implemented?
what does Conceptual Contour mean, how can you apply it when designing classes?
what is a Standalone Class, is it good or bad?
what does Closure of Operation mean, is it good or bad?
what is a strategy, how can it be applied in DDD?
what is a composite, how can it be applied in DDD?
what does it mean, that the domain should be free of non-business aspects
what is the Identity Operation?
how is equality useful in the domain?
how does the template method pattern work?
how does the state pattern work?
how does the composite pattern work?
how does the null object pattern work?
what does immutability mean?
what is a fluent interface?
what does a dependency represent in the domain?
how much of the domain is a communication mechanism?
what are predicates?
what is a "find or create" functionality, is it good?

 

  • resources


starting points
http://domaindrivendesign.org/
http://www.akcedo.com/andrei/post/DDD-Concepts.aspx

 

books
http://www.infoq.com/minibooks/domain-driven-design-quickly (free book)
http://domaindrivendesign.org/books/PatternSummariesUnderCreativeCommons.doc (free book)

Applying Domain-Driven Design and Patterns: With Examples in C# and .NET, By Jimmy Nilsson
.NET Domain-Driven Design with C#, by Tim McCarthy           
Domain-Driven Design: Tackling Complexity in the Heart of Software, By Eric Evans

 

blog posts
http://www.lostechies.com/blogs/jimmy_bogard/archive/2008/05/15/domain-driven-design-supple-design-patterns-series.aspx
http://www.lostechies.com/blogs/jimmy_bogard/archive/2008/10/22/where-are-the-ddd-sample-applications.aspx
http://www.lostechies.com/blogs/joe_ocampo/archive/2007/04/02/a-discussion-on-domain-driven-design.aspx
http://www.udidahan.com/2007/03/06/better-domain-driven-design-implementation/
http://debasishg.blogspot.com/2007/07/domain-driven-design-handling-alternate_3757.html
http://codebetter.com/blogs/david_laribee/archive/2008/06/15/super-models-part-1-sexy-specifications.aspx
http://www.lostechies.com/blogs/jason_meridth/archive/2007/09/25/domain-model-overuse-post-by-david-laribee-on-codebetter.aspx
http://ayende.com/Blog/archive/2007/10/23/Specifying-Specifications.aspx
http://devlicio.us/blogs/jeff_perrin/archive/2006/12/13/the-specification-pattern.aspx
http://colinjack.blogspot.com/2007/06/specification-pattern-implementation.html
http://www.codeinsanity.com/2008/08/implementing-repository-and.html
http://debasishg.blogspot.com/2006/10/domain-classes-or-interfaces.html
http://codebetter.com/blogs/matthew.podwysocki/archive/2008/05/01/side-effecting-functions-are-code-smells-revisited.aspx
http://ubik.com.au/article/named/implementing_the_specification_pattern_with_linq
http://www.mostlyclean.com/post/2008/08/Linq-Expressions-The-Specification-Pattern-and-Repositories-Part-1.aspx

 

articles & presentations
http://www.cs.colorado.edu/~kena/classes/6448/s05/lectures/lecture30.pdf

 

video
http://www.parleys.com/display/PARLEYS/Home#slide=1;title=Get%20Value%20Objects%20right%20for%20Domain%20Driven%20Design;talk=8099
http://www.parleys.com/display/PARLEYS/Home#slide=5;title=Domain%20Driven%20Design%20with%20AOP%20and%20DI;talk=6295
http://www.parleys.com/display/PARLEYS/Home#slide=5;title=Domain%20Driven%20Design%20with%20AOP%20and%20DI;talk=6295
http://www.parleys.com/display/PARLEYS/Home#slide=1;title=Improving%20Application%20Design%20with%20a%20Rich%20Domain%20Model;talk=2555906

 

code
http://sourceforge.net/projects/dddsample/
http://dddsample.sourceforge.net/

 

  • evaluation
  • We will use:

    • Clients
    • Products
    • Orders
    • OrderDetails

    Also, please read the specification from the DDD1 evaluation to find out any other elements (like DiscountLevel) that are needed, based on what the current specification requires.

     

    • consider that you have a set of orders (like the ones described in the evaluation for DDD1) that are already launched: a few paid orders, and a few bonus orders (you can keep them in the application session)
    • you start on a page where you see the list of orders
      • in the list, you are shown the details of each order
      • you can also see the value of each validated order
        • the value of a validated order is equal to the value of all validated products in it
      • you can filter the orders by type, status (Launched, Validated, Rejected), launch date, validation date, client
      • you can sort the list by clicking on at least one of the column names
      • you can click on a Validate button which exists next to each order
        • when you click on Validate, you are redirected to a page where you see the details of that order and you can validate it
          • you can validate or reject the products one by one (through the corresponding button next to each product), and click on Validate Order outside the products list when you are done
            • by clicking Validate Order, all the products which have not been manually validated or rejected automatically become validated, and the order also becomes validated
          • you can validate or reject the entire order directly, and all its products become validated or rejected automatically

     

    • when you validate an order
    • the initial page also contains the list of clients and their available bonus
      • this should reflect the changes that happen when an order is validated / rejected

 

Technorati Tags: ,,

 

 

Enjoy programming!

 

Application layer Mappers play well with domain Services

by andrei 23. September 2008 18:14

I needed a mapper which would transform a domain entity (Product) into a display DTO (ProductProfileDisplayDTO).

 

The DTO looks like this:

public class ProductProfileDisplayDTO
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Gain { get; set; }
}

 

I can get the Id and Name from the entity easily, but the profile Gain is calculated in comparison with another list of products. This information is not part of the Product entity alone, instead it is a domain service method:

public class ProductProfileService : IProductProfileService
{
  public decimal CalculateGainFor(Product profile, IList<Product> productsToCompareWith)
  {
    IList<Product> occurences = FindOccurrencesOfProductsInProfileUsing(profile, productsToCompareWith);
    decimal individualProductPrice = occurences.Sum(p => p.ProductDiscountPrice) / occurences.Count;
    decimal profileProductPrice = profile.ProductDiscountPrice / profile.SubProducts.Count;
    return individualProductPrice - profileProductPrice;
  }
}

I think it's ok to let the mapper use the domain service to obtain this information when preparing data for the UI, so here it is:

public class ProductProfileDisplayDTOMapper : IMapper<Product, ProductProfileDisplayDTO>
{
    private readonly IProductProfileService _productProfileService = new ProductProfileService ( );
    private readonly IList<Product> _products;

    public ProductProfileDisplayDTOMapper(IList<Product> products)
    {
        _products = products;
    }

    public void CopyFrom(Product source, ProductProfileDisplayDTO destination)
    {
        throw new System.NotImplementedException();
    }

    public ProductProfileDisplayDTO MapFrom(Product input)
    {
        return new ProductProfileDisplayDTO()
                   {
                       Gain = StringFormatter.FormatPrice(_productProfileService.CalculateGainFor(input, _products)),
                       Id = input.Id,
                       Name = input.ProductName
                   };
    }
}

 

And here is how the the application layer task gets a list of ProductProfileDisplayDTOs:

IEnumerable<Product> possibleProfiles = _productProfileService.LoadAllPossibleProfilesFor ( products );

IEnumerable<ProductProfileDisplayDTO> possibleProfileDTOs = possibleProfiles
    .MapAllUsing ( new ProductProfileDisplayDTOMapper(products) );

The list of possibleProfiles does not contain information about the Gain, but the possibleProfileDTO list has it as soon as it is created.

 

 

Enjoy programming!

 

Powered by BlogEngine.NET 1.4.5.0