diff --git a/src/Orchard.Tests/ContentManagement/DynamicContentQueryTests.cs b/src/Orchard.Tests/ContentManagement/DynamicContentQueryTests.cs new file mode 100644 index 000000000..51f90a5be --- /dev/null +++ b/src/Orchard.Tests/ContentManagement/DynamicContentQueryTests.cs @@ -0,0 +1,233 @@ +using System.Linq; +using Autofac; +using Moq; +using NHibernate; +using NUnit.Framework; +using Orchard.ContentManagement.MetaData; +using Orchard.Data; +using Orchard.ContentManagement; +using Orchard.ContentManagement.Handlers; +using Orchard.ContentManagement.Records; +using Orchard.DisplayManagement; +using Orchard.DisplayManagement.Descriptors; +using Orchard.Environment.Extensions; +using Orchard.Tests.ContentManagement.Handlers; +using Orchard.Tests.ContentManagement.Records; +using Orchard.Tests.ContentManagement.Models; +using Orchard.DisplayManagement.Implementation; +using Orchard.Tests.Stubs; + +namespace Orchard.Tests.ContentManagement { + [TestFixture] + public class DynamicContentQueryTests { + private IContainer _container; + private IContentManager _manager; + private ISessionFactory _sessionFactory; + private ISession _session; + + [TestFixtureSetUp] + public void InitFixture() { + var databaseFileName = System.IO.Path.GetTempFileName(); + _sessionFactory = DataUtility.CreateSessionFactory( + databaseFileName, + typeof(GammaRecord), + typeof(DeltaRecord), + typeof(EpsilonRecord), + typeof(ContentItemVersionRecord), + typeof(ContentItemRecord), + typeof(ContentTypeRecord)); + } + + [SetUp] + public void Init() { + var builder = new ContainerBuilder(); + + builder.RegisterModule(new ContentModule()); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As(); + builder.RegisterInstance(new Mock().Object); + builder.RegisterInstance(new Mock().Object); + + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + + builder.RegisterGeneric(typeof(Repository<>)).As(typeof(IRepository<>)); + + builder.RegisterType().As(); + builder.RegisterType().As(); + + _session = _sessionFactory.OpenSession(); + builder.RegisterInstance(new DefaultContentManagerTests.TestSessionLocator(_session)).As(); + + _session.Delete(string.Format("from {0}", typeof(GammaRecord).FullName)); + _session.Delete(string.Format("from {0}", typeof(DeltaRecord).FullName)); + _session.Delete(string.Format("from {0}", typeof(EpsilonRecord).FullName)); + _session.Delete(string.Format("from {0}", typeof(ContentItemVersionRecord).FullName)); + _session.Delete(string.Format("from {0}", typeof(ContentItemRecord).FullName)); + _session.Delete(string.Format("from {0}", typeof(ContentTypeRecord).FullName)); + _session.Flush(); + _session.Clear(); + + _container = builder.Build(); + _manager = _container.Resolve(); + + } + + private void AddSampleData() { + _manager.Create("alpha", init => { }); + _manager.Create("beta", init => { }); + _manager.Create("gamma", init => { init.Record.Frap = "the frap value"; }); + _manager.Create("delta", init => { init.Record.Quux = "the quux value"; }); + _session.Flush(); + } + + [Test] + public void SpecificTypeIsReturnedWhenSpecified() { + AddSampleData(); + + var alphaBeta = _manager.Query().Where(x => x.WithRecord("ContentType").In("Name", new [] {"alpha", "beta"})).List(); + + Assert.That(alphaBeta.Count(), Is.EqualTo(2)); + Assert.That(alphaBeta.Count(x => x.Has()), Is.EqualTo(1)); + Assert.That(alphaBeta.Count(x => x.Has()), Is.EqualTo(1)); + Assert.That(alphaBeta.Count(x => x.Has()), Is.EqualTo(0)); + Assert.That(alphaBeta.Count(x => x.Has()), Is.EqualTo(0)); + + var gammaDelta = _manager.Query().Where(x => x.WithRecord("ContentType").In("Name", new[] { "gamma", "delta" })).List(); + + Assert.That(gammaDelta.Count(), Is.EqualTo(2)); + Assert.That(gammaDelta.Count(x => x.Has()), Is.EqualTo(0)); + Assert.That(gammaDelta.Count(x => x.Has()), Is.EqualTo(0)); + Assert.That(gammaDelta.Count(x => x.Has()), Is.EqualTo(1)); + Assert.That(gammaDelta.Count(x => x.Has()), Is.EqualTo(1)); + } + + [Test] + public void WherePredicateRestrictsResults() { + AddSampleData(); + _manager.Create("gamma", init => { init.Record.Frap = "one"; }); + _manager.Create("gamma", init => { init.Record.Frap = "two"; }); + _manager.Create("gamma", init => { init.Record.Frap = "three"; }); + _manager.Create("gamma", init => { init.Record.Frap = "four"; }); + _session.Flush(); + + var twoOrFour = _manager.Query() + .Where(x => x.WithRecord("GammaRecord").Or(a => a.Eq("Frap", "one"), b => b.Eq("Frap", "four"))) + .List(); + + Assert.That(twoOrFour.Count(), Is.EqualTo(2)); + Assert.That(twoOrFour.Count(x => x.Has()), Is.EqualTo(2)); + Assert.That(twoOrFour.Count(x => x.Get().Record.Frap == "one"), Is.EqualTo(1)); + Assert.That(twoOrFour.Count(x => x.Get().Record.Frap == "four"), Is.EqualTo(1)); + } + + + [Test] + public void EmptyWherePredicateRequiresRecord() { + AddSampleData(); + var gammas = _manager.Query().Where(x => x.WithRecord("GammaRecord")).List(); // simulates an inner join + var deltas = _manager.Query().Where(x => x.WithRecord("DeltaRecord")).List(); + + Assert.That(gammas.Count(), Is.EqualTo(1)); + Assert.That(deltas.Count(), Is.EqualTo(1)); + Assert.That(gammas.AsPart().Single().Record.Frap, Is.EqualTo("the frap value")); + Assert.That(deltas.AsPart().Single().Record.Quux, Is.EqualTo("the quux value")); + } + + [Test] + public void OrderMaySortOnJoinedRecord() { + AddSampleData(); + _manager.Create("gamma", init => { init.Record.Frap = "one"; }); + _manager.Create("gamma", init => { init.Record.Frap = "two"; }); + _manager.Create("gamma", init => { init.Record.Frap = "three"; }); + _manager.Create("gamma", init => { init.Record.Frap = "four"; }); + _session.Flush(); + _session.Clear(); + + var ascending = _manager.Query("gamma") + .OrderBy(x => x.WithRecord("GammaRecord").Asc("Frap")) + .List().ToList(); + + Assert.That(ascending.Count(), Is.EqualTo(5)); + Assert.That(ascending.First().Record.Frap, Is.EqualTo("four")); + Assert.That(ascending.Last().Record.Frap, Is.EqualTo("two")); + + _session.Clear(); + + var descending = _manager.Query() + .OrderBy(x => x.WithRecord("GammaRecord").Desc("Frap")) + .List().ToList(); + + Assert.That(descending.Count(), Is.EqualTo(5)); + Assert.That(descending.First().Record.Frap, Is.EqualTo("two")); + Assert.That(descending.Last().Record.Frap, Is.EqualTo("four")); + } + + [Test] + public void SkipAndTakeProvidePagination() { + AddSampleData(); + _manager.Create("gamma", init => { init.Record.Frap = "one"; }); + _manager.Create("gamma", init => { init.Record.Frap = "two"; }); + _manager.Create("gamma", init => { init.Record.Frap = "three"; }); + _manager.Create("gamma", init => { init.Record.Frap = "four"; }); + _session.Flush(); + + var reverseById = _manager.Query() + .OrderBy(x => x.WithRecord("GammaRecord").Desc("Id")) + .List(); + + var subset = _manager.Query() + .OrderBy(x => x.WithRecord("GammaRecord").Desc("Id")) + .Slice(2, 3); + + Assert.That(subset.Count(), Is.EqualTo(3)); + Assert.That(subset.First().Id, Is.EqualTo(reverseById.Skip(2).First().Id)); + Assert.That(subset.Skip(1).First().Id, Is.EqualTo(reverseById.Skip(3).First().Id)); + Assert.That(subset.Skip(2).First().Id, Is.EqualTo(reverseById.Skip(4).First().Id)); + } + + + [Test] + public void QueryShouldJoinVersionedRecords() { + AddSampleData(); + _manager.Create("gamma", init => { + init.Record.Frap = "one"; + init.As().Record.Quad = "1"; + }); + _manager.Create("gamma", init => { + init.Record.Frap = "two"; + init.As().Record.Quad = "2"; + }); + _manager.Create("gamma", init => { + init.Record.Frap = "three"; + init.As().Record.Quad = "3"; + }); + _manager.Create("gamma", init => { + init.Record.Frap = "four"; + init.As().Record.Quad = "4"; + }); + _session.Flush(); + _session.Clear(); + + var results = _manager.Query("gamma") + .Where(x => x.WithVersionRecord("EpsilonRecord").Or(a => a.Eq("Quad", "2"), b => b.Eq("Quad", "3"))) + .OrderBy(x => x.WithVersionRecord("EpsilonRecord").Desc("Quad")) + .List(); + + Assert.That(results.Count(), Is.EqualTo(2)); + Assert.That(results.First().Record, Has.Property("Quad").EqualTo("3")); + Assert.That(results.Last().Record, Has.Property("Quad").EqualTo("2")); + } + } +} + + + diff --git a/src/Orchard.Tests/Orchard.Framework.Tests.csproj b/src/Orchard.Tests/Orchard.Framework.Tests.csproj index 5acf3f855..01d74bd07 100644 --- a/src/Orchard.Tests/Orchard.Framework.Tests.csproj +++ b/src/Orchard.Tests/Orchard.Framework.Tests.csproj @@ -169,6 +169,7 @@ Code + Code diff --git a/src/Orchard/ContentManagement/DefaultContentQuery.cs b/src/Orchard/ContentManagement/DefaultContentQuery.cs index 94411e2bb..36900be4f 100644 --- a/src/Orchard/ContentManagement/DefaultContentQuery.cs +++ b/src/Orchard/ContentManagement/DefaultContentQuery.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; @@ -30,37 +31,36 @@ namespace Orchard.ContentManagement { return _session; } - ICriteria BindCriteriaByPath(ICriteria criteria, string path) { + internal ICriteria BindCriteriaByPath(ICriteria criteria, string path) { return criteria.GetCriteriaByPath(path) ?? criteria.CreateCriteria(path); } - ICriteria BindTypeCriteria() { + internal ICriteria BindTypeCriteria() { // ([ContentItemVersionRecord] >join> [ContentItemRecord]) >join> [ContentType] return BindCriteriaByPath(BindItemCriteria(), "ContentType"); } - ICriteria BindItemCriteria() { + internal ICriteria BindItemCriteria() { // [ContentItemVersionRecord] >join> [ContentItemRecord] return BindCriteriaByPath(BindItemVersionCriteria(), "ContentItemRecord"); } - ICriteria BindItemVersionCriteria() { + internal ICriteria BindItemVersionCriteria() { if (_itemVersionCriteria == null) { _itemVersionCriteria = BindSession().CreateCriteria(); } return _itemVersionCriteria; } - ICriteria BindPartCriteria() where TRecord : ContentPartRecord { + internal ICriteria BindPartCriteria() where TRecord : ContentPartRecord { if (typeof(TRecord).IsSubclassOf(typeof(ContentPartVersionRecord))) { return BindCriteriaByPath(BindItemVersionCriteria(), typeof(TRecord).Name); } return BindCriteriaByPath(BindItemCriteria(), typeof(TRecord).Name); } - private void ForType(params string[] contentTypeNames) { if (contentTypeNames != null && contentTypeNames.Length != 0) BindTypeCriteria().Add(Restrictions.InG("Name", contentTypeNames)); @@ -92,6 +92,15 @@ namespace Orchard.ContentManagement { } } + private void Where(Action expression) { + var expressionFactory = new DefaultExpressionFactory(this); + + expression(expressionFactory); + if (expressionFactory.Criterion != null) { + expressionFactory.Criteria.Add(expressionFactory.Criterion); + } + } + private void OrderBy(Expression> keySelector) where TRecord : ContentPartRecord { // build a linq to nhibernate expression var options = new QueryOptions(); @@ -108,6 +117,15 @@ namespace Orchard.ContentManagement { } } + private void OrderBy(Action expression) { + var sortFactory = new DefaultSortFactory(this); + + expression(sortFactory); + if (sortFactory.Order != null) { + sortFactory.Criteria.AddOrder(sortFactory.Order); + } + } + private void OrderByDescending(Expression> keySelector) where TRecord : ContentPartRecord { // build a linq to nhibernate expression var options = new QueryOptions(); @@ -126,7 +144,7 @@ namespace Orchard.ContentManagement { private IEnumerable Slice(int skip, int count) { var criteria = BindItemVersionCriteria(); - + criteria.ApplyVersionOptionsRestrictions(_versionOptions); // TODO: put 'removed false' filter in place @@ -197,6 +215,11 @@ namespace Orchard.ContentManagement { return new ContentQuery(_query); } + IContentQuery IContentQuery.Where(Action predicate) { + _query.Where(predicate); + return new ContentQuery(_query); + } + IContentQuery IContentQuery.Where(Expression> predicate) { _query.Where(predicate); return new ContentQuery(_query); @@ -211,8 +234,12 @@ namespace Orchard.ContentManagement { _query.OrderByDescending(keySelector); return new ContentQuery(_query); } - } + IContentQuery IContentQuery.OrderBy(Action order) { + _query.OrderBy(order); + return new ContentQuery(_query); + } + } class ContentQuery : ContentQuery, IContentQuery where T : IContent @@ -242,6 +269,200 @@ namespace Orchard.ContentManagement { } } + public class DefaultExpressionFactory : IExpressionFactory { + private readonly DefaultContentQuery _query; + public ICriterion Criterion { get; private set; } + public ICriteria Criteria { get; private set; } + + public DefaultExpressionFactory(DefaultContentQuery query) { + _query = query; + } + + public IExpressionFactory WithRecord(string path) { + Criteria = _query.BindCriteriaByPath(_query.BindItemCriteria(), path); + return this; + } + + public IExpressionFactory WithVersionRecord(string path) { + Criteria = _query.BindCriteriaByPath(_query.BindItemVersionCriteria(), path); + return this; + } + + public void Eq(string propertyName, object value) { + Criterion = Restrictions.Eq(propertyName, value); + } + + public void Like(string propertyName, object value) { + Criterion = Restrictions.Like(propertyName, value); + } + + public void Like(string propertyName, string value, MatchMode matchMode, char? escapeChar) { + Criterion = Restrictions.Like(propertyName, value); + } + + public void Like(string propertyName, string value, MatchMode matchMode) { + Criterion = Restrictions.Like(propertyName, value); + } + + public void InsensitiveLike(string propertyName, string value, MatchMode matchMode) { + Criterion = Restrictions.InsensitiveLike(propertyName, value); + } + + public void InsensitiveLike(string propertyName, object value) { + Criterion = Restrictions.InsensitiveLike(propertyName, value); + } + + public void Gt(string propertyName, object value) { + Criterion = Restrictions.Gt(propertyName, value); + } + + public void Lt(string propertyName, object value) { + Criterion = Restrictions.Lt(propertyName, value); + } + + public void Le(string propertyName, object value) { + Criterion = Restrictions.Le(propertyName, value); + } + + public void Ge(string propertyName, object value) { + Criterion = Restrictions.Ge(propertyName, value); + } + + public void Between(string propertyName, object lo, object hi) { + Criterion = Restrictions.Between(propertyName, lo, hi); + } + + public void In(string propertyName, object[] values) { + Criterion = Restrictions.In(propertyName, values); + } + + public void In(string propertyName, ICollection values) { + Criterion = Restrictions.In(propertyName, values); + } + + public void InG(string propertyName, ICollection values) { + Criterion = Restrictions.InG(propertyName, values); + } + + public void IsNull(string propertyName) { + Criterion = Restrictions.IsNull(propertyName); + } + + public void EqProperty(string propertyName, string otherPropertyName) { + Criterion = Restrictions.EqProperty(propertyName, otherPropertyName); + } + + public void NotEqProperty(string propertyName, string otherPropertyName) { + Criterion = Restrictions.NotEqProperty(propertyName, otherPropertyName); + } + + public void GtProperty(string propertyName, string otherPropertyName) { + Criterion = Restrictions.GtProperty(propertyName, otherPropertyName); + } + + public void GeProperty(string propertyName, string otherPropertyName) { + Criterion = Restrictions.GeProperty(propertyName, otherPropertyName); + } + + public void LtProperty(string propertyName, string otherPropertyName) { + Criterion = Restrictions.LtProperty(propertyName, otherPropertyName); + } + + public void LeProperty(string propertyName, string otherPropertyName) { + Criterion = Restrictions.LeProperty(propertyName, otherPropertyName); + } + + public void IsNotNull(string propertyName) { + Criterion = Restrictions.IsNotNull(propertyName); + } + + public void IsNotEmpty(string propertyName) { + Criterion = Restrictions.IsNotEmpty(propertyName); + } + + public void IsEmpty(string propertyName) { + Criterion = Restrictions.IsEmpty(propertyName); + } + + public void And(Action lhs, Action rhs) { + lhs(this); + ICriterion a = Criterion; + rhs(this); + ICriterion b = Criterion; + Criterion = Restrictions.And(a, b); + } + + public void Or(Action lhs, Action rhs) { + lhs(this); + ICriterion a = Criterion; + rhs(this); + ICriterion b = Criterion; + Criterion = Restrictions.Or(a, b); + } + + public void Not(Action expression) { + expression(this); + ICriterion a = Criterion; + Criterion = Restrictions.Not(a); + } + + public void Conjunction(Action expression, params Action[] otherExpressions) { + var junction = Restrictions.Conjunction(); + foreach (var exp in Enumerable.Empty>().Union(new[] { expression }).Union(otherExpressions)) { + exp(this); + junction.Add(Criterion); + } + + Criterion = junction; + } + + public void Disjunction(Action expression, params Action[] otherExpressions) { + var junction = Restrictions.Disjunction(); + foreach (var exp in Enumerable.Empty>().Union(new[] { expression }).Union(otherExpressions)) { + exp(this); + junction.Add(Criterion); + } + + Criterion = junction; + } + + public void AllEq(IDictionary propertyNameValues) { + Criterion = Restrictions.AllEq(propertyNameValues); + } + + public void NaturalId() { + Criterion = Restrictions.NaturalId(); + } + + } + + public class DefaultSortFactory : ISortFactory { + private readonly DefaultContentQuery _query; + public Order Order { get; private set; } + public ICriteria Criteria { get; private set; } + + public DefaultSortFactory(DefaultContentQuery query) { + _query = query; + } + + public ISortFactory WithRecord(string path) { + Criteria = _query.BindCriteriaByPath(_query.BindItemCriteria(), path); + return this; + } + + public ISortFactory WithVersionRecord(string path) { + Criteria = _query.BindCriteriaByPath(_query.BindItemVersionCriteria(), path); + return this; + } + + public void Asc(string propertyName) { + Order = Order.Asc(propertyName); + } + + public void Desc(string propertyName) { + Order = Order.Desc(propertyName); + } + } } internal static class CriteriaExtensions { diff --git a/src/Orchard/ContentManagement/IContentQuery.cs b/src/Orchard/ContentManagement/IContentQuery.cs index b436f1d4d..deadb7d2e 100644 --- a/src/Orchard/ContentManagement/IContentQuery.cs +++ b/src/Orchard/ContentManagement/IContentQuery.cs @@ -5,7 +5,7 @@ using Orchard.ContentManagement.Records; namespace Orchard.ContentManagement { - public interface IContentQuery { + public interface IContentQuery{ IContentManager ContentManager { get; } IContentQuery ForPart() where TPart : IContent; } @@ -20,7 +20,10 @@ namespace Orchard.ContentManagement { IContentQuery Join() where TRecord : ContentPartRecord; + IContentQuery Where(Action predicate); IContentQuery Where(Expression> predicate) where TRecord : ContentPartRecord; + + IContentQuery OrderBy(Action order); IContentQuery OrderBy(Expression> keySelector) where TRecord : ContentPartRecord; IContentQuery OrderByDescending(Expression> keySelector) where TRecord : ContentPartRecord; } diff --git a/src/Orchard/ContentManagement/IExpressionFactory.cs b/src/Orchard/ContentManagement/IExpressionFactory.cs new file mode 100644 index 000000000..53fc01d7a --- /dev/null +++ b/src/Orchard/ContentManagement/IExpressionFactory.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Orchard.ContentManagement { + public interface IExpressionFactory { + IExpressionFactory WithRecord(string recordName); + IExpressionFactory WithVersionRecord(string recordName); + + void Eq(string propertyName, object value); + void Like(string propertyName, object value); + void Like(string propertyName, string value, MatchMode matchMode, char? escapeChar); + void Like(string propertyName, string value, MatchMode matchMode); + void InsensitiveLike(string propertyName, string value, MatchMode matchMode); + void InsensitiveLike(string propertyName, object value); + void Gt(string propertyName, object value); + void Lt(string propertyName, object value); + void Le(string propertyName, object value); + void Ge(string propertyName, object value); + void Between(string propertyName, object lo, object hi); + void In(string propertyName, object[] values); + void In(string propertyName, ICollection values); + void InG(string propertyName, ICollection values); + void IsNull(string propertyName); + void EqProperty(string propertyName, string otherPropertyName); + void NotEqProperty(string propertyName, string otherPropertyName); + void GtProperty(string propertyName, string otherPropertyName); + void GeProperty(string propertyName, string otherPropertyName); + void LtProperty(string propertyName, string otherPropertyName); + void LeProperty(string propertyName, string otherPropertyName); + void IsNotNull(string propertyName); + void IsNotEmpty(string propertyName); + void IsEmpty(string propertyName); + void And(Action lhs, Action rhs); + void Or(Action lhs, Action rhs); + void Not(Action expression); + void Conjunction(Action expression, params Action[] otherExpressions); + void Disjunction(Action expression, params Action[] otherExpressions); + void AllEq(IDictionary propertyNameValues); + void NaturalId(); + } + + public enum MatchMode { + Exact, + Start, + End, + Anywhere + } +} diff --git a/src/Orchard/ContentManagement/ISortFactory.cs b/src/Orchard/ContentManagement/ISortFactory.cs new file mode 100644 index 000000000..d230f3216 --- /dev/null +++ b/src/Orchard/ContentManagement/ISortFactory.cs @@ -0,0 +1,9 @@ +namespace Orchard.ContentManagement { + public interface ISortFactory { + ISortFactory WithRecord(string recordName); + ISortFactory WithVersionRecord(string recordName); + + void Asc(string propertyName); + void Desc(string propertyName); + } +} diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index 7612a1482..8b11b9ea9 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -169,6 +169,7 @@ + @@ -178,6 +179,7 @@ +