* compiled my own version of Lucene (2.9 RC1)
* got search working
This commit is contained in:
parent
94481265fa
commit
866964a764
Binary file not shown.
32164
Lib/Lucene.Net.xml
32164
Lib/Lucene.Net.xml
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -203,12 +203,6 @@
|
|||||||
not be null.
|
not be null.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:Portoa.Web.Unity.ConfigureInterception">
|
|
||||||
<summary>
|
|
||||||
Configures interception to occur for any subsequent container registrations
|
|
||||||
that are not part of NHibernate
|
|
||||||
</summary>
|
|
||||||
</member>
|
|
||||||
<member name="M:Portoa.Web.FilterInfoExtensions.Flatten(System.Web.Mvc.FilterInfo)">
|
<member name="M:Portoa.Web.FilterInfoExtensions.Flatten(System.Web.Mvc.FilterInfo)">
|
||||||
<summary>
|
<summary>
|
||||||
Flattens a <c>FilterInfo</c> object into a single <c>IEnumerable</c> containing
|
Flattens a <c>FilterInfo</c> object into a single <c>IEnumerable</c> containing
|
||||||
@ -298,11 +292,6 @@
|
|||||||
<param name="key">The key of the item to retrieve</param>
|
<param name="key">The key of the item to retrieve</param>
|
||||||
<returns>The value stored in the specified key, or null if no such key exists</returns>
|
<returns>The value stored in the specified key, or null if no such key exists</returns>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:Portoa.Web.Unity.UnityContainerExtensions">
|
|
||||||
<summary>
|
|
||||||
Extension methods for <c>UnityContainer</c>
|
|
||||||
</summary>
|
|
||||||
</member>
|
|
||||||
<member name="M:Portoa.Web.Unity.UnityContainerExtensions.AddExtensionOnce``1(Microsoft.Practices.Unity.IUnityContainer)">
|
<member name="M:Portoa.Web.Unity.UnityContainerExtensions.AddExtensionOnce``1(Microsoft.Practices.Unity.IUnityContainer)">
|
||||||
<summary>
|
<summary>
|
||||||
Adds an extension if it hasn't already been registered with the container
|
Adds an extension if it hasn't already been registered with the container
|
||||||
@ -321,6 +310,45 @@
|
|||||||
<param name="types"></param>
|
<param name="types"></param>
|
||||||
<returns></returns>
|
<returns></returns>
|
||||||
</member>
|
</member>
|
||||||
|
<member name="M:Portoa.Web.Unity.UnityContainerExtensions.RegisterInterfaceAndIntercept``2(Microsoft.Practices.Unity.IUnityContainer,Microsoft.Practices.Unity.LifetimeManager,Microsoft.Practices.Unity.InjectionMember[])">
|
||||||
|
<summary>
|
||||||
|
Registers the type and configures an <see cref="T:Microsoft.Practices.Unity.InterceptionExtension.InterfaceInterceptor"/> for
|
||||||
|
<typeparamref name="TFrom"/>
|
||||||
|
</summary>
|
||||||
|
<typeparam name="TFrom">This must be an interface type</typeparam>
|
||||||
|
<typeparam name="TTo">The type to resolve the interface to</typeparam>
|
||||||
|
</member>
|
||||||
|
<member name="M:Portoa.Web.Unity.UnityContainerExtensions.RegisterInterfaceAndIntercept``1(Microsoft.Practices.Unity.IUnityContainer,Microsoft.Practices.Unity.LifetimeManager,Microsoft.Practices.Unity.InjectionMember[])">
|
||||||
|
<summary>
|
||||||
|
Registers the interface type and configures interception for it
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Portoa.Web.Unity.UnityContainerExtensions.RegisterAndIntercept``2(Microsoft.Practices.Unity.IUnityContainer,Microsoft.Practices.Unity.LifetimeManager,Microsoft.Practices.Unity.InjectionMember[])">
|
||||||
|
<summary>
|
||||||
|
Registers the type and configures interception for it
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Portoa.Web.Unity.UnityContainerExtensions.RegisterAndIntercept``1(Microsoft.Practices.Unity.IUnityContainer,Microsoft.Practices.Unity.LifetimeManager,Microsoft.Practices.Unity.InjectionMember[])">
|
||||||
|
<summary>
|
||||||
|
Registers the type and configures interception for it
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Portoa.Web.Unity.UnityContainerExtensions.RegisterAndIntercept``1(Microsoft.Practices.Unity.IUnityContainer,``0,Microsoft.Practices.Unity.LifetimeManager)">
|
||||||
|
<summary>
|
||||||
|
Registers the instance and configures interception for it
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Portoa.Web.Unity.UnityContainerExtensions.RegisterInterfaceAndIntercept(Microsoft.Practices.Unity.IUnityContainer,System.Type,System.Type,Microsoft.Practices.Unity.LifetimeManager,Microsoft.Practices.Unity.InjectionMember[])">
|
||||||
|
<summary>
|
||||||
|
Registers the type and configures interception for it
|
||||||
|
</summary>
|
||||||
|
<param name="typeFrom">This must be an interface type</param>
|
||||||
|
</member>
|
||||||
|
<member name="M:Portoa.Web.Unity.UnityContainerExtensions.RegisterAndIntercept(Microsoft.Practices.Unity.IUnityContainer,System.Type,System.Type,Microsoft.Practices.Unity.LifetimeManager,Microsoft.Practices.Unity.InjectionMember[])">
|
||||||
|
<summary>
|
||||||
|
Registers the type and configures interception for it
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
<member name="T:Portoa.Web.ErrorHandling.ErrorWithUserResultFactory`1">
|
<member name="T:Portoa.Web.ErrorHandling.ErrorWithUserResultFactory`1">
|
||||||
<summary>
|
<summary>
|
||||||
<c cref="T:Portoa.Web.ErrorHandling.IErrorResultFactory">Error result factory</c> that returns a result suitable
|
<c cref="T:Portoa.Web.ErrorHandling.IErrorResultFactory">Error result factory</c> that returns a result suitable
|
||||||
@ -518,6 +546,18 @@
|
|||||||
does nothing
|
does nothing
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
|
<member name="M:Portoa.Web.MvcApplicationBase`1.ConfigureUnityExtensions">
|
||||||
|
<summary>
|
||||||
|
Adds extensions to the container; default implementation does nothing
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Portoa.Web.MvcApplicationBase`1.OnApplicationEnd">
|
||||||
|
<summary>
|
||||||
|
Performs any needed cleanup when the application ends; default implementation
|
||||||
|
does nothing. Be aware that the <c cref="F:Portoa.Web.MvcApplicationBase`1.Container">container</c> gets disposed of
|
||||||
|
later.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
<member name="T:Portoa.Web.IInjectableControllerFactory">
|
<member name="T:Portoa.Web.IInjectableControllerFactory">
|
||||||
<summary>
|
<summary>
|
||||||
<c cref="T:System.Web.Mvc.IControllerFactory">IControllerFactory</c> that provides a mechanism to perform injection
|
<c cref="T:System.Web.Mvc.IControllerFactory">IControllerFactory</c> that provides a mechanism to perform injection
|
||||||
@ -624,19 +664,15 @@
|
|||||||
Controller factory that uses a service provider to resolve controllers
|
Controller factory that uses a service provider to resolve controllers
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:Portoa.Web.Results.StatusOverrideResult">
|
<member name="T:Portoa.Web.Validation.FileTypeAttribute">
|
||||||
<summary>
|
<summary>
|
||||||
<c>ActionResult</c> decorator that enables you to override the HTTP status code
|
Verifies that an uploaded file has a certain mimetype
|
||||||
</summary>
|
</summary>
|
||||||
<see cref="T:Portoa.Web.Filters.OverrideStatusCodeFilter"/>
|
|
||||||
</member>
|
</member>
|
||||||
<member name="M:Portoa.Web.HttpRequestBaseExtensions.Get``1(System.Web.HttpRequestBase,System.String)">
|
<member name="P:Portoa.Web.Validation.FileTypeAttribute.MimeType">
|
||||||
<summary>
|
<summary>
|
||||||
Gets an object from the request variables, or its default value if
|
Gets or sets the expected mimetype (i.e. text/plain, image/png, etc.)
|
||||||
the key does not exist
|
|
||||||
</summary>
|
</summary>
|
||||||
<typeparam name="T">The type to convert the value to</typeparam>
|
|
||||||
<param name="key">The request key of the object to retrieve</param>
|
|
||||||
</member>
|
</member>
|
||||||
<member name="T:Portoa.Web.Validation.FileLengthAttribute">
|
<member name="T:Portoa.Web.Validation.FileLengthAttribute">
|
||||||
<summary>
|
<summary>
|
||||||
@ -656,14 +692,49 @@
|
|||||||
Gets or sets the (inclusive) minimum length of the uploaded file
|
Gets or sets the (inclusive) minimum length of the uploaded file
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:Portoa.Web.Validation.FileTypeAttribute">
|
<member name="T:Portoa.Web.Results.StatusOverrideResult">
|
||||||
<summary>
|
<summary>
|
||||||
Verifies that an uploaded file has a certain mimetype
|
<c>ActionResult</c> decorator that enables you to override the HTTP status code
|
||||||
|
</summary>
|
||||||
|
<see cref="T:Portoa.Web.Filters.OverrideStatusCodeFilter"/>
|
||||||
|
</member>
|
||||||
|
<member name="M:Portoa.Web.HttpRequestBaseExtensions.Get``1(System.Web.HttpRequestBase,System.String)">
|
||||||
|
<summary>
|
||||||
|
Gets an object from the request variables, or its default value if
|
||||||
|
the key does not exist
|
||||||
|
</summary>
|
||||||
|
<typeparam name="T">The type to convert the value to</typeparam>
|
||||||
|
<param name="key">The request key of the object to retrieve</param>
|
||||||
|
</member>
|
||||||
|
<member name="T:Portoa.Web.Unity.Matching.AlwaysMatches">
|
||||||
|
<summary>
|
||||||
|
A matching rule that always matches
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="P:Portoa.Web.Validation.FileTypeAttribute.MimeType">
|
<member name="T:Portoa.Web.Unity.Matching.InstanceOf`1">
|
||||||
<summary>
|
<summary>
|
||||||
Gets or sets the expected mimetype (i.e. text/plain, image/png, etc.)
|
Matching rule where a member is match if it is assignable from <typeparamref name="T"/>
|
||||||
|
</summary>
|
||||||
|
<typeparam name="T">The base class/interface to match against</typeparam>
|
||||||
|
</member>
|
||||||
|
<member name="T:Portoa.Web.Unity.Matching.Not`1">
|
||||||
|
<summary>
|
||||||
|
Negates a matching rule
|
||||||
|
</summary>
|
||||||
|
<typeparam name="T">The type of matching rule to negate</typeparam>
|
||||||
|
</member>
|
||||||
|
<member name="T:Portoa.Web.Unity.Matching.Not">
|
||||||
|
<summary>
|
||||||
|
Negates a matching rule
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="M:Portoa.Web.Unity.Matching.Not.#ctor(Microsoft.Practices.Unity.InterceptionExtension.IMatchingRule)">
|
||||||
|
<param name="ruleToNegate">The matching rule to negate</param>
|
||||||
|
</member>
|
||||||
|
<member name="T:Portoa.Web.Unity.Matching.PropertyGetOrSet">
|
||||||
|
<summary>
|
||||||
|
Matching rule that matches compiler-generated methods for property
|
||||||
|
getters and setters
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
</members>
|
</members>
|
||||||
|
BIN
Lib/Portoa.dll
BIN
Lib/Portoa.dll
Binary file not shown.
@ -72,12 +72,11 @@
|
|||||||
to the <paramref name="source"/> collection
|
to the <paramref name="source"/> collection
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:Portoa.Persistence.Entity`2">
|
<member name="T:Portoa.Persistence.IdentifiableDto">
|
||||||
<summary>
|
<summary>
|
||||||
Represents a domain object that can be persisted by a <c cref="T:Portoa.Persistence.IRepository`2">repository</c>
|
Convenience class for data transfer objects that have an integral
|
||||||
|
identifier
|
||||||
</summary>
|
</summary>
|
||||||
<typeparam name="T">The entity type</typeparam>
|
|
||||||
<typeparam name="TId">The entity's identifier type</typeparam>
|
|
||||||
</member>
|
</member>
|
||||||
<member name="T:Portoa.Persistence.IIdentifiable`1">
|
<member name="T:Portoa.Persistence.IIdentifiable`1">
|
||||||
<summary>
|
<summary>
|
||||||
@ -90,6 +89,13 @@
|
|||||||
The unique identifier of this object
|
The unique identifier of this object
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
|
<member name="T:Portoa.Persistence.Entity`2">
|
||||||
|
<summary>
|
||||||
|
Represents a domain object that can be persisted by a <c cref="T:Portoa.Persistence.IRepository`2">repository</c>
|
||||||
|
</summary>
|
||||||
|
<typeparam name="T">The entity type</typeparam>
|
||||||
|
<typeparam name="TId">The entity's identifier type</typeparam>
|
||||||
|
</member>
|
||||||
<member name="M:Portoa.Util.LinqExtensions.Implode``1(System.Collections.Generic.IEnumerable{``0},System.Func{``0,System.String},System.String)">
|
<member name="M:Portoa.Util.LinqExtensions.Implode``1(System.Collections.Generic.IEnumerable{``0},System.Func{``0,System.String},System.String)">
|
||||||
<summary>
|
<summary>
|
||||||
Implodes an enumeration given a selector function and a separator
|
Implodes an enumeration given a selector function and a separator
|
||||||
@ -627,11 +633,5 @@
|
|||||||
Signifies that this object should not be logged
|
Signifies that this object should not be logged
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:Portoa.Persistence.IdentifiableDto">
|
|
||||||
<summary>
|
|
||||||
Convenience class for data transfer objects that have an integral
|
|
||||||
identifier
|
|
||||||
</summary>
|
|
||||||
</member>
|
|
||||||
</members>
|
</members>
|
||||||
</doc>
|
</doc>
|
||||||
|
Binary file not shown.
32
Src/VideoGameQuotes.Api/Persistence/SearchService.cs
Normal file
32
Src/VideoGameQuotes.Api/Persistence/SearchService.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Portoa.Persistence;
|
||||||
|
|
||||||
|
namespace VideoGameQuotes.Api.Persistence {
|
||||||
|
|
||||||
|
public interface ISearchService<T> where T : Entity<T, int> {
|
||||||
|
IEnumerable<T> FindByIds(IEnumerable<int> ids);
|
||||||
|
IEnumerable<T> GetAllIndexableRecords();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SearchService<T> : ISearchService<T> where T : Entity<T, int> {
|
||||||
|
private readonly IRepository<T> repository;
|
||||||
|
|
||||||
|
public SearchService(IRepository<T> repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnitOfWork]
|
||||||
|
public IEnumerable<T> FindByIds(IEnumerable<int> ids) {
|
||||||
|
|
||||||
|
return repository
|
||||||
|
.Records
|
||||||
|
.Where(entity => ids.ToArray().Contains(entity.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
[UnitOfWork]
|
||||||
|
public IEnumerable<T> GetAllIndexableRecords() {
|
||||||
|
return repository.Records;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,6 @@ using JetBrains.Annotations;
|
|||||||
using Portoa.Persistence;
|
using Portoa.Persistence;
|
||||||
|
|
||||||
namespace VideoGameQuotes.Api.Persistence {
|
namespace VideoGameQuotes.Api.Persistence {
|
||||||
|
|
||||||
public interface IUserService {
|
public interface IUserService {
|
||||||
User Save(User user);
|
User Save(User user);
|
||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
|
17
Src/VideoGameQuotes.Api/Search/ISearchIndexBuilder.cs
Normal file
17
Src/VideoGameQuotes.Api/Search/ISearchIndexBuilder.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
namespace VideoGameQuotes.Api.Search {
|
||||||
|
/// <summary>
|
||||||
|
/// Exposes an interface to build and update a search index
|
||||||
|
/// </summary>
|
||||||
|
public interface ISearchIndexBuilder<T> {
|
||||||
|
/// <summary>
|
||||||
|
/// (Re)builds the search index
|
||||||
|
/// </summary>
|
||||||
|
void BuildIndex();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates the index for the specified <paramref name="indexableObject"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="indexableObject">The object that needs its index updated</param>
|
||||||
|
void UpdateIndex(T indexableObject);
|
||||||
|
}
|
||||||
|
}
|
15
Src/VideoGameQuotes.Api/Search/ISearcher.cs
Normal file
15
Src/VideoGameQuotes.Api/Search/ISearcher.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace VideoGameQuotes.Api.Search {
|
||||||
|
/// <summary>
|
||||||
|
/// Exposes an interface to perform a full-text search
|
||||||
|
/// </summary>
|
||||||
|
public interface ISearcher<T> {
|
||||||
|
/// <summary>
|
||||||
|
/// Searches for records based on the given search query
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">The search term(s) to search for</param>
|
||||||
|
/// <param name="maxResults">The maximum number of results to return (<c>0</c> is unlimited); the default is <c>10</c></param>
|
||||||
|
IEnumerable<SearchResult<T>> Search(string query, int maxResults = 10);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
using Lucene.Net.Documents;
|
||||||
|
using Lucene.Net.Index;
|
||||||
|
|
||||||
|
namespace VideoGameQuotes.Api.Search.Lucene {
|
||||||
|
public interface ILuceneDocumentHandler<T> {
|
||||||
|
Document BuildDocument(T source);
|
||||||
|
Term GetIdTerm(T source);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
using Lucene.Net.Index;
|
||||||
|
using Portoa.Logging;
|
||||||
|
using Portoa.Persistence;
|
||||||
|
using VideoGameQuotes.Api.Persistence;
|
||||||
|
|
||||||
|
namespace VideoGameQuotes.Api.Search.Lucene {
|
||||||
|
public class LuceneEntityIndexBuilder<T> : ISearchIndexBuilder<T> where T : Entity<T, int> {
|
||||||
|
private readonly ILogger logger;
|
||||||
|
private readonly IndexWriter indexWriter;
|
||||||
|
private readonly ISearchService<T> searchService;
|
||||||
|
private readonly ILuceneDocumentHandler<T> documentHandler;
|
||||||
|
|
||||||
|
public LuceneEntityIndexBuilder(IndexWriter indexWriter, ISearchService<T> searchService, ILuceneDocumentHandler<T> documentHandler, ILogger logger) {
|
||||||
|
this.indexWriter = indexWriter;
|
||||||
|
this.searchService = searchService;
|
||||||
|
this.documentHandler = documentHandler;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BuildIndex() {
|
||||||
|
logger.Info("Building lucene index");
|
||||||
|
foreach (var quote in searchService.GetAllIndexableRecords()) {
|
||||||
|
indexWriter.AddDocument(documentHandler.BuildDocument(quote));
|
||||||
|
}
|
||||||
|
|
||||||
|
indexWriter.Optimize();
|
||||||
|
indexWriter.Commit();
|
||||||
|
logger.Info("Finished building lucene index");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateIndex(T entity) {
|
||||||
|
if (entity.IsTransient()) {
|
||||||
|
throw new SearchIndexException(string.Format("Cannot add a transient entity to the index ({0})", entity));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info(string.Format("Updating index for {0}", entity));
|
||||||
|
//delete current document, if it exists
|
||||||
|
indexWriter.DeleteDocuments(documentHandler.GetIdTerm(entity));
|
||||||
|
indexWriter.AddDocument(documentHandler.BuildDocument(entity));
|
||||||
|
|
||||||
|
indexWriter.Commit();
|
||||||
|
logger.Info(string.Format("Finished updating index for {0}", entity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Lucene.Net.QueryParsers;
|
||||||
|
using Lucene.Net.Search;
|
||||||
|
using Portoa.Persistence;
|
||||||
|
using VideoGameQuotes.Api.Persistence;
|
||||||
|
using Directory = Lucene.Net.Store.Directory;
|
||||||
|
|
||||||
|
namespace VideoGameQuotes.Api.Search.Lucene {
|
||||||
|
/// <summary>
|
||||||
|
/// <see cref="ISearcher{T}"/> implementation for entities based on <c>Lucene.NET</c>
|
||||||
|
/// </summary>
|
||||||
|
public class LuceneEntitySearcher<T> : ISearcher<T> where T : Entity<T, int> {
|
||||||
|
private readonly QueryParser queryParser;
|
||||||
|
private readonly Directory indexDirectory;
|
||||||
|
private readonly ISearchService<T> searchService;
|
||||||
|
|
||||||
|
public LuceneEntitySearcher(QueryParser queryParser, Directory indexDirectory, ISearchService<T> searchService) {
|
||||||
|
this.queryParser = queryParser;
|
||||||
|
this.indexDirectory = indexDirectory;
|
||||||
|
this.searchService = searchService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<SearchResult<T>> Search(string searchString, int maxResults = 10) {
|
||||||
|
if (string.IsNullOrWhiteSpace(searchString)) {
|
||||||
|
return Enumerable.Empty<SearchResult<T>>();
|
||||||
|
}
|
||||||
|
if (maxResults < 0) {
|
||||||
|
throw new ArgumentOutOfRangeException("maxResults", maxResults, "Maximum number of results must be greater than or equal to zero");
|
||||||
|
}
|
||||||
|
if (maxResults == 0) {
|
||||||
|
maxResults = int.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var query = queryParser.Parse(QueryParser.Escape(searchString));
|
||||||
|
var searcher = new IndexSearcher(indexDirectory, true);
|
||||||
|
try {
|
||||||
|
var docs = searcher
|
||||||
|
.Search(query, maxResults)
|
||||||
|
.scoreDocs;
|
||||||
|
|
||||||
|
var quotes = searchService.FindByIds(docs.Select(doc => int.Parse(searcher.Doc(doc.doc).GetField("id").StringValue())));
|
||||||
|
return quotes.Zip(docs, (entity, doc) => new SearchResult<T> { Entity = entity, Score = doc.score }).ToArray();
|
||||||
|
} finally {
|
||||||
|
searcher.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
using Lucene.Net.Documents;
|
||||||
|
using Lucene.Net.Index;
|
||||||
|
|
||||||
|
namespace VideoGameQuotes.Api.Search.Lucene {
|
||||||
|
public class QuoteDocumentHandler : ILuceneDocumentHandler<Quote> {
|
||||||
|
|
||||||
|
public Document BuildDocument(Quote quote) {
|
||||||
|
var document = new Document();
|
||||||
|
document.Add(new Field("id", quote.Id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
|
||||||
|
document.Add(new Field("text", quote.Text, Field.Store.YES, Field.Index.ANALYZED));
|
||||||
|
return document;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Term GetIdTerm(Quote quote) {
|
||||||
|
return new Term("id", quote.Id.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
10
Src/VideoGameQuotes.Api/Search/SearchIndexException.cs
Normal file
10
Src/VideoGameQuotes.Api/Search/SearchIndexException.cs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace VideoGameQuotes.Api.Search {
|
||||||
|
/// <summary>
|
||||||
|
/// Raised when an error occurs while reading/writing a search index
|
||||||
|
/// </summary>
|
||||||
|
public class SearchIndexException : Exception {
|
||||||
|
public SearchIndexException(string message = null, Exception innerException = null) : base(message, innerException) { }
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a search result
|
/// Represents a search result
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SearchResult {
|
public class SearchResult<T> {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A value (between 0 and 1, the higher the better) representing how good
|
/// A value (between 0 and 1, the higher the better) representing how good
|
||||||
/// the match is between the search query and the value
|
/// the match is between the search query and the value
|
||||||
@ -12,6 +12,6 @@
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The matched quote
|
/// The matched quote
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Quote Quote { get; set; }
|
public T Entity { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -69,10 +69,13 @@
|
|||||||
<Compile Include="Category.cs" />
|
<Compile Include="Category.cs" />
|
||||||
<Compile Include="CriterionHandler.cs" />
|
<Compile Include="CriterionHandler.cs" />
|
||||||
<Compile Include="Game.cs" />
|
<Compile Include="Game.cs" />
|
||||||
<Compile Include="Search\IQuoteSearcher.cs" />
|
<Compile Include="Persistence\SearchService.cs" />
|
||||||
|
<Compile Include="Search\ISearcher.cs" />
|
||||||
|
<Compile Include="Search\ISearchIndexBuilder.cs" />
|
||||||
<Compile Include="Search\ISearchIndexLocator.cs" />
|
<Compile Include="Search\ISearchIndexLocator.cs" />
|
||||||
<Compile Include="Search\Lucene\LuceneQuoteSearcher.cs" />
|
<Compile Include="Search\Lucene\ILuceneDocumentHandler.cs" />
|
||||||
<Compile Include="Search\Lucene\LuceneExtensions.cs" />
|
<Compile Include="Search\Lucene\LuceneEntityIndexBuilder.cs" />
|
||||||
|
<Compile Include="Search\Lucene\LuceneEntitySearcher.cs" />
|
||||||
<Compile Include="Persistence\IUserRepository.cs" />
|
<Compile Include="Persistence\IUserRepository.cs" />
|
||||||
<Compile Include="Persistence\UserService.cs" />
|
<Compile Include="Persistence\UserService.cs" />
|
||||||
<Compile Include="QuoteFlag.cs" />
|
<Compile Include="QuoteFlag.cs" />
|
||||||
@ -82,6 +85,8 @@
|
|||||||
<Compile Include="QuoteFlagType.cs" />
|
<Compile Include="QuoteFlagType.cs" />
|
||||||
<Compile Include="Region.cs" />
|
<Compile Include="Region.cs" />
|
||||||
<Compile Include="GamingSystem.cs" />
|
<Compile Include="GamingSystem.cs" />
|
||||||
|
<Compile Include="Search\Lucene\QuoteDocumentHandler.cs" />
|
||||||
|
<Compile Include="Search\SearchIndexException.cs" />
|
||||||
<Compile Include="Search\SearchResult.cs" />
|
<Compile Include="Search\SearchResult.cs" />
|
||||||
<Compile Include="User.cs" />
|
<Compile Include="User.cs" />
|
||||||
<Compile Include="UserGroup.cs" />
|
<Compile Include="UserGroup.cs" />
|
||||||
@ -104,6 +109,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Mappings\Category.hbm.xml" />
|
<EmbeddedResource Include="Mappings\Category.hbm.xml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup />
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||||
Other similar extension points exist, see Microsoft.Common.targets.
|
Other similar extension points exist, see Microsoft.Common.targets.
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
using Microsoft.Practices.Unity;
|
||||||
|
using Microsoft.Practices.Unity.InterceptionExtension;
|
||||||
|
using Portoa.Persistence;
|
||||||
|
using VideoGameQuotes.Api;
|
||||||
|
using VideoGameQuotes.Api.Search;
|
||||||
|
|
||||||
|
namespace VideoGameQuotes.Web.Configuration {
|
||||||
|
|
||||||
|
public class UpdateSearchIndex : UnityContainerExtension {
|
||||||
|
protected override void Initialize() {
|
||||||
|
Container
|
||||||
|
.Configure<Interception>()
|
||||||
|
.AddPolicy("UpdateSearchIndexPolicy")
|
||||||
|
.AddCallHandler<UpdateSearchIndexCallHandler>()
|
||||||
|
.AddMatchingRule<QuoteUpdatedMatchingRule>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QuoteUpdatedMatchingRule : IMatchingRule {
|
||||||
|
private static readonly MethodBase saveMethod = typeof(IRepository<Quote, int>).GetMethod("Save", new[] { typeof(Quote) });
|
||||||
|
|
||||||
|
public bool Matches(MethodBase member) {
|
||||||
|
return member == saveMethod;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateSearchIndexCallHandler : ICallHandler {
|
||||||
|
private readonly IUnityContainer container;
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// Can't inject ISearchIndexBuilder because it causes an infinite loop
|
||||||
|
/// while trying to instantiate the call handler. So we do a later resolve
|
||||||
|
/// on the index builder so that this shit fucking works.
|
||||||
|
/// </remarks>
|
||||||
|
public UpdateSearchIndexCallHandler(IUnityContainer container) {
|
||||||
|
this.container = container;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext) {
|
||||||
|
var returnValue = getNext()(input, getNext);
|
||||||
|
if (returnValue.Exception != null) {
|
||||||
|
//if the update failed then just return
|
||||||
|
return returnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var quote = returnValue.ReturnValue as Quote;
|
||||||
|
if (quote == null) {
|
||||||
|
return returnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.Resolve<ISearchIndexBuilder<Quote>>().UpdateIndex(quote);
|
||||||
|
|
||||||
|
return returnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Order { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -18,9 +18,9 @@ namespace VideoGameQuotes.Web.Controllers {
|
|||||||
public class QuoteController : Controller {
|
public class QuoteController : Controller {
|
||||||
private readonly IQuoteService quoteService;
|
private readonly IQuoteService quoteService;
|
||||||
private readonly ICurrentUserProvider<User> currentUserProvider;
|
private readonly ICurrentUserProvider<User> currentUserProvider;
|
||||||
private readonly IQuoteSearcher quoteSearcher;
|
private readonly ISearcher<Quote> quoteSearcher;
|
||||||
|
|
||||||
public QuoteController(IQuoteService quoteService, ICurrentUserProvider<User> currentUserProvider, IQuoteSearcher quoteSearcher) {
|
public QuoteController(IQuoteService quoteService, ICurrentUserProvider<User> currentUserProvider, ISearcher<Quote> quoteSearcher) {
|
||||||
this.quoteService = quoteService;
|
this.quoteService = quoteService;
|
||||||
this.currentUserProvider = currentUserProvider;
|
this.currentUserProvider = currentUserProvider;
|
||||||
this.quoteSearcher = quoteSearcher;
|
this.quoteSearcher = quoteSearcher;
|
||||||
|
@ -1,9 +1,19 @@
|
|||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Configuration;
|
using System.Configuration;
|
||||||
|
using System.IO;
|
||||||
using System.Web.Mvc;
|
using System.Web.Mvc;
|
||||||
using System.Web.Routing;
|
using System.Web.Routing;
|
||||||
|
using Lucene.Net.Analysis;
|
||||||
|
using Lucene.Net.Analysis.Standard;
|
||||||
|
using Lucene.Net.Index;
|
||||||
|
using Lucene.Net.QueryParsers;
|
||||||
|
using Lucene.Net.Search;
|
||||||
|
using Lucene.Net.Store;
|
||||||
|
using Lucene.Net.Util;
|
||||||
using Microsoft.Practices.Unity;
|
using Microsoft.Practices.Unity;
|
||||||
|
using Microsoft.Practices.Unity.InterceptionExtension;
|
||||||
using Portoa.Logging;
|
using Portoa.Logging;
|
||||||
|
using Portoa.Persistence;
|
||||||
using Portoa.Web;
|
using Portoa.Web;
|
||||||
using Portoa.Web.Models;
|
using Portoa.Web.Models;
|
||||||
using Portoa.Web.Security;
|
using Portoa.Web.Security;
|
||||||
@ -13,10 +23,12 @@ using VideoGameQuotes.Api;
|
|||||||
using VideoGameQuotes.Api.Persistence;
|
using VideoGameQuotes.Api.Persistence;
|
||||||
using VideoGameQuotes.Api.Search;
|
using VideoGameQuotes.Api.Search;
|
||||||
using VideoGameQuotes.Api.Search.Lucene;
|
using VideoGameQuotes.Api.Search.Lucene;
|
||||||
|
using VideoGameQuotes.Web.Configuration;
|
||||||
using VideoGameQuotes.Web.Controllers;
|
using VideoGameQuotes.Web.Controllers;
|
||||||
using VideoGameQuotes.Web.Models;
|
using VideoGameQuotes.Web.Models;
|
||||||
using VideoGameQuotes.Web.Security;
|
using VideoGameQuotes.Web.Security;
|
||||||
using VideoGameQuotes.Web.Services;
|
using VideoGameQuotes.Web.Services;
|
||||||
|
using Directory = Lucene.Net.Store.Directory;
|
||||||
|
|
||||||
namespace VideoGameQuotes.Web {
|
namespace VideoGameQuotes.Web {
|
||||||
public class MvcApplication : MvcApplicationBase<User> {
|
public class MvcApplication : MvcApplicationBase<User> {
|
||||||
@ -28,41 +40,67 @@ namespace VideoGameQuotes.Web {
|
|||||||
.Add<ApiModel, ApiModelBinder>();
|
.Add<ApiModel, ApiModelBinder>();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void ConfigureUnity() {
|
protected override void ConfigureUnityExtensions() {
|
||||||
Container
|
Container
|
||||||
.AddNewExtension<ConfigureLog4Net>()
|
.AddNewExtension<ConfigureLog4Net>()
|
||||||
.Configure<ILog4NetConfigurator>()
|
.Configure<ILog4NetConfigurator>()
|
||||||
.SetName("VideoGameQuotes.Web")
|
.SetName("VideoGameQuotes.Web")
|
||||||
.UseXml();
|
.UseXml();
|
||||||
|
|
||||||
Container
|
Container.AddNewExtension<LogAllMethodCalls>();
|
||||||
.AddNewExtension<LogAllMethodCalls>()
|
Container.AddNewExtension<UpdateSearchIndex>();
|
||||||
.RegisterType<ICurrentUserProvider<User>, SessionBasedUserProvider>()
|
|
||||||
.RegisterType<VerifyUserAttribute>(new InjectionProperty<VerifyUserAttribute>(attr => attr.UserProvider))
|
|
||||||
.RegisterType<IUserService, UserService>()
|
|
||||||
.RegisterType<IAdministrationService, AdministrationService>()
|
|
||||||
.RegisterType<IQuoteService, QuoteService>()
|
|
||||||
.RegisterType<ISystemService, SystemService>()
|
|
||||||
.RegisterType<ICategoryService, CategoryService>()
|
|
||||||
.RegisterType<IPublisherService, PublisherService>()
|
|
||||||
.RegisterType<IGameService, GameService>()
|
|
||||||
.RegisterType<IApiService, ApiService>()
|
|
||||||
.RegisterType<IAuthenticationService, FormsAuthenticationService>()
|
|
||||||
.RegisterType<IQuoteSearcher, LuceneQuoteSearcher>()
|
|
||||||
.RegisterType<ISearchIndexLocator, SearchIndexLocator>(new ContainerControlledLifetimeManager(), new InjectionFactory(CreateIndexLocator))
|
|
||||||
.RegisterType<IUserRepository, UserRepository>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SearchIndexLocator CreateIndexLocator(IUnityContainer container) {
|
protected override void ConfigureUnity() {
|
||||||
var indexDirectory = ((NameValueCollection)ConfigurationManager.GetSection("vgquotes"))["luceneIndexDirectory"];
|
Container
|
||||||
return new SearchIndexLocator(indexDirectory);
|
.RegisterType<VerifyUserAttribute>(new InjectionProperty<VerifyUserAttribute>(attr => attr.UserProvider))
|
||||||
|
.RegisterAndIntercept<ICurrentUserProvider<User>, SessionBasedUserProvider>()
|
||||||
|
.RegisterAndIntercept<IUserService, UserService>()
|
||||||
|
.RegisterAndIntercept<IAdministrationService, AdministrationService>()
|
||||||
|
.RegisterAndIntercept<IQuoteService, QuoteService>()
|
||||||
|
.RegisterAndIntercept<ISystemService, SystemService>()
|
||||||
|
.RegisterAndIntercept<ICategoryService, CategoryService>()
|
||||||
|
.RegisterAndIntercept<IPublisherService, PublisherService>()
|
||||||
|
.RegisterAndIntercept<IGameService, GameService>()
|
||||||
|
.RegisterAndIntercept<IApiService, ApiService>()
|
||||||
|
.RegisterAndIntercept<IAuthenticationService, FormsAuthenticationService>()
|
||||||
|
.RegisterAndIntercept<IUserRepository, UserRepository>();
|
||||||
|
|
||||||
|
//search stuff
|
||||||
|
Container
|
||||||
|
.RegisterType<Directory>(new ContainerControlledLifetimeManager(), new InjectionFactory(CreateIndexDirectory))
|
||||||
|
.RegisterType<IndexWriter>(new ContainerControlledLifetimeManager(), new InjectionFactory(CreateIndexWriter))
|
||||||
|
.RegisterInstance(Version.LUCENE_29)
|
||||||
|
.RegisterType<Analyzer, StandardAnalyzer>(new InjectionConstructor(typeof(Version)))
|
||||||
|
.RegisterType<QueryParser>(new InjectionFactory(CreateQueryParser))
|
||||||
|
.RegisterAndIntercept(typeof(ISearcher<>), typeof(LuceneEntitySearcher<>))
|
||||||
|
.RegisterAndIntercept(typeof(ISearchService<>), typeof(SearchService<>))
|
||||||
|
.RegisterAndIntercept<ILuceneDocumentHandler<Quote>, QuoteDocumentHandler>()
|
||||||
|
.RegisterAndIntercept(typeof(ISearchIndexBuilder<>), typeof(LuceneEntityIndexBuilder<>));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region lucene-related factories
|
||||||
|
private static QueryParser CreateQueryParser(IUnityContainer container) {
|
||||||
|
return new QueryParser(container.Resolve<Version>(), "text", container.Resolve<Analyzer>());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Directory CreateIndexDirectory(IUnityContainer container) {
|
||||||
|
var indexDirectory = ((NameValueCollection)ConfigurationManager.GetSection("vgquotes"))["luceneIndexDirectory"];
|
||||||
|
return new SimpleFSDirectory(new DirectoryInfo(indexDirectory));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IndexWriter CreateIndexWriter(IUnityContainer container) {
|
||||||
|
return new IndexWriter(
|
||||||
|
container.Resolve<Directory>(),
|
||||||
|
new StandardAnalyzer(Version.LUCENE_29),
|
||||||
|
true,
|
||||||
|
IndexWriter.MaxFieldLength.UNLIMITED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
protected override void AfterStartUp() {
|
protected override void AfterStartUp() {
|
||||||
var logger = Container.Resolve<ILogger>();
|
Container.Resolve<ISearchIndexBuilder<Quote>>().BuildIndex();
|
||||||
logger.Info("Building lucene index");
|
|
||||||
Container.Resolve<IQuoteSearcher>().BuildIndex();
|
|
||||||
logger.Info("Done building lucene index");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void RegisterRoutes(RouteCollection routes) {
|
protected override void RegisterRoutes(RouteCollection routes) {
|
||||||
|
@ -5,7 +5,7 @@ using VideoGameQuotes.Api.Search;
|
|||||||
namespace VideoGameQuotes.Web.Models {
|
namespace VideoGameQuotes.Web.Models {
|
||||||
public class SearchModel {
|
public class SearchModel {
|
||||||
public User User { get; set; }
|
public User User { get; set; }
|
||||||
public IEnumerable<SearchResult> Results { get; set; }
|
public IEnumerable<SearchResult<Quote>> Results { get; set; }
|
||||||
public string SearchQuery { get; set; }
|
public string SearchQuery { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -33,11 +33,17 @@
|
|||||||
<WarningLevel>4</WarningLevel>
|
<WarningLevel>4</WarningLevel>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Reference Include="Lucene.Net, Version=2.9.2.2, Culture=neutral, processorArchitecture=MSIL" />
|
||||||
<Reference Include="Microsoft.CSharp" />
|
<Reference Include="Microsoft.CSharp" />
|
||||||
<Reference Include="Microsoft.Practices.Unity">
|
<Reference Include="Microsoft.Practices.Unity">
|
||||||
<HintPath>..\..\Lib\Microsoft.Practices.Unity.dll</HintPath>
|
<HintPath>..\..\Lib\Microsoft.Practices.Unity.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="Portoa">
|
<Reference Include="Microsoft.Practices.Unity.Interception, Version=2.0.414.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||||
|
<SpecificVersion>False</SpecificVersion>
|
||||||
|
<HintPath>..\..\Lib\Microsoft.Practices.Unity.Interception.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Portoa, Version=1.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||||
|
<SpecificVersion>False</SpecificVersion>
|
||||||
<HintPath>..\..\Lib\Portoa.dll</HintPath>
|
<HintPath>..\..\Lib\Portoa.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="Portoa.Log4Net">
|
<Reference Include="Portoa.Log4Net">
|
||||||
@ -84,6 +90,7 @@
|
|||||||
</Reference>
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="Configuration\UpdateSearchIndexCallHandler.cs" />
|
||||||
<Compile Include="Controllers\AdminController.cs" />
|
<Compile Include="Controllers\AdminController.cs" />
|
||||||
<Compile Include="Controllers\ApiController.cs" />
|
<Compile Include="Controllers\ApiController.cs" />
|
||||||
<Compile Include="Controllers\CategoryController.cs" />
|
<Compile Include="Controllers\CategoryController.cs" />
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<%
|
<%
|
||||||
foreach (var result in Model.Results) {
|
foreach (var result in Model.Results) {
|
||||||
Html.RenderPartial("SingleQuote", new QuoteModel {Quote = result.Quote, User = Model.User });
|
Html.RenderPartial("SingleQuote", new QuoteModel {Quote = result.Entity, User = Model.User });
|
||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
</asp:Content>
|
</asp:Content>
|
BIN
Src/VideoGameQuotes.Web/media/images/flag_red.png
Normal file
BIN
Src/VideoGameQuotes.Web/media/images/flag_red.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 665 B |
BIN
Src/VideoGameQuotes.Web/media/images/link.png
Normal file
BIN
Src/VideoGameQuotes.Web/media/images/link.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 343 B |
@ -66,7 +66,6 @@
|
|||||||
<Reference Include="System.Xml" />
|
<Reference Include="System.Xml" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="LuceneTests.cs" />
|
|
||||||
<Compile Include="QuoteTests.cs" />
|
<Compile Include="QuoteTests.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="NHibernate\SchemaExporter.cs" />
|
<Compile Include="NHibernate\SchemaExporter.cs" />
|
||||||
|
Loading…
Reference in New Issue
Block a user