From d2d2020573888e728a2a584f86e340b776b80e34 Mon Sep 17 00:00:00 2001 From: Qstick Date: Tue, 17 Dec 2019 21:23:16 -0500 Subject: [PATCH] Revert "Remove Marr.Data" This reverts commit 7b17c3e36cf44ebdd7cbc5a89ff1233480041667. --- src/Common/CodeAnalysisDictionary.xml | 22 + src/Common/CommonAssemblyInfo.cs | 20 + src/Common/CommonVersionInfo.cs | 5 + src/Common/GlobalSuppressions.cs | 18 + src/Common/Microsoft.AspNet.SignalR.ruleset | 227 +++++ src/Common/Microsoft.AspNet.SignalR.targets | 40 + .../Converters/BooleanIntConverter.cs | 74 ++ .../Converters/BooleanYNConverter.cs | 74 ++ src/Marr.Data/Converters/CastConverter.cs | 53 + .../Converters/ConversionException.cs | 26 + src/Marr.Data/Converters/ConverterContext.cs | 13 + src/Marr.Data/Converters/EnumIntConverter.cs | 50 + .../Converters/EnumStringConverter.cs | 50 + src/Marr.Data/Converters/IConverter.cs | 30 + src/Marr.Data/DataHelper.cs | 166 +++ src/Marr.Data/DataMapper.cs | 958 ++++++++++++++++++ src/Marr.Data/DataMappingException.cs | 22 + src/Marr.Data/EntityGraph.cs | 419 ++++++++ src/Marr.Data/EntityMerger.cs | 34 + src/Marr.Data/EntityReference.cs | 28 + src/Marr.Data/ExtensionMethods.cs | 19 + src/Marr.Data/GroupingKeyCollection.cs | 85 ++ src/Marr.Data/IDataMapper.cs | 219 ++++ src/Marr.Data/LazyLoaded.cs | 131 +++ src/Marr.Data/MapRepository.cs | 250 +++++ src/Marr.Data/Mapping/ColumnAttribute.cs | 115 +++ src/Marr.Data/Mapping/ColumnInfo.cs | 32 + src/Marr.Data/Mapping/ColumnMap.cs | 70 ++ src/Marr.Data/Mapping/ColumnMapBuilder.cs | 235 +++++ src/Marr.Data/Mapping/ColumnMapCollection.cs | 172 ++++ src/Marr.Data/Mapping/EnumConversionType.cs | 24 + src/Marr.Data/Mapping/FluentMappings.cs | 234 +++++ src/Marr.Data/Mapping/IColumnInfo.cs | 32 + src/Marr.Data/Mapping/IRelationshipInfo.cs | 32 + src/Marr.Data/Mapping/MapBuilder.cs | 206 ++++ src/Marr.Data/Mapping/MappingHelper.cs | 194 ++++ src/Marr.Data/Mapping/Relationship.cs | 98 ++ .../Mapping/RelationshipAttribute.cs | 75 ++ src/Marr.Data/Mapping/RelationshipBuilder.cs | 172 ++++ .../Mapping/RelationshipCollection.cs | 35 + src/Marr.Data/Mapping/RelationshipInfo.cs | 11 + .../Strategies/AttributeMapStrategy.cs | 70 ++ .../Strategies/ConventionMapStrategy.cs | 48 + .../Mapping/Strategies/IMapStrategy.cs | 45 + .../Mapping/Strategies/PropertyMapStrategy.cs | 83 ++ .../Strategies/ReflectionMapStrategyBase.cs | 145 +++ src/Marr.Data/Mapping/TableAttribute.cs | 40 + src/Marr.Data/Mapping/TableBuilder.cs | 58 ++ src/Marr.Data/Marr.Data.csproj | 9 + src/Marr.Data/Parameters/DbTypeBuilder.cs | 69 ++ src/Marr.Data/Parameters/IDbTypeBuilder.cs | 30 + .../Parameters/ParameterChainMethods.cs | 133 +++ src/Marr.Data/QGen/DeleteQuery.cs | 28 + src/Marr.Data/QGen/Dialects/Dialect.cs | 80 ++ .../QGen/Dialects/FirebirdDialect.cs | 17 + src/Marr.Data/QGen/Dialects/OracleDialect.cs | 35 + .../QGen/Dialects/SqlServerCeDialect.cs | 21 + .../QGen/Dialects/SqlServerDialect.cs | 13 + src/Marr.Data/QGen/Dialects/SqliteDialect.cs | 28 + src/Marr.Data/QGen/ExpressionVisitor.cs | 146 +++ src/Marr.Data/QGen/IQuery.cs | 11 + src/Marr.Data/QGen/IQueryBuilder.cs | 12 + src/Marr.Data/QGen/InsertQuery.cs | 67 ++ src/Marr.Data/QGen/InsertQueryBuilder.cs | 203 ++++ src/Marr.Data/QGen/JoinBuilder.cs | 36 + src/Marr.Data/QGen/PagingQueryDecorator.cs | 232 +++++ src/Marr.Data/QGen/QueryBuilder.cs | 635 ++++++++++++ src/Marr.Data/QGen/QueryFactory.cs | 113 +++ src/Marr.Data/QGen/QueryQueueItem.cs | 19 + src/Marr.Data/QGen/RowCountQueryDecorator.cs | 121 +++ src/Marr.Data/QGen/SelectQuery.cs | 159 +++ src/Marr.Data/QGen/SortBuilder.cs | 262 +++++ src/Marr.Data/QGen/SortColumn.cs | 46 + .../QGen/SqlitePagingQueryDecorator.cs | 156 +++ .../QGen/SqliteRowCountQueryDecorator.cs | 45 + src/Marr.Data/QGen/Table.cs | 47 + src/Marr.Data/QGen/TableCollection.cs | 95 ++ src/Marr.Data/QGen/UpdateQuery.cs | 56 + src/Marr.Data/QGen/UpdateQueryBuilder.cs | 175 ++++ src/Marr.Data/QGen/View.cs | 90 ++ src/Marr.Data/QGen/WhereBuilder.cs | 325 ++++++ .../Reflection/IReflectionStrategy.cs | 17 + src/Marr.Data/Reflection/ReflectionHelper.cs | 67 ++ .../Reflection/SimpleReflectionStrategy.cs | 145 +++ src/Marr.Data/SqlModesEnum.cs | 23 + src/Marr.Data/UnitOfWork.cs | 127 +++ src/Marr.Data/UnitOfWorkSharedContext.cs | 38 + src/Radarr.sln | 13 + 88 files changed, 9203 insertions(+) create mode 100644 src/Common/CodeAnalysisDictionary.xml create mode 100644 src/Common/CommonAssemblyInfo.cs create mode 100644 src/Common/CommonVersionInfo.cs create mode 100644 src/Common/GlobalSuppressions.cs create mode 100644 src/Common/Microsoft.AspNet.SignalR.ruleset create mode 100644 src/Common/Microsoft.AspNet.SignalR.targets create mode 100644 src/Marr.Data/Converters/BooleanIntConverter.cs create mode 100644 src/Marr.Data/Converters/BooleanYNConverter.cs create mode 100644 src/Marr.Data/Converters/CastConverter.cs create mode 100644 src/Marr.Data/Converters/ConversionException.cs create mode 100644 src/Marr.Data/Converters/ConverterContext.cs create mode 100644 src/Marr.Data/Converters/EnumIntConverter.cs create mode 100644 src/Marr.Data/Converters/EnumStringConverter.cs create mode 100644 src/Marr.Data/Converters/IConverter.cs create mode 100644 src/Marr.Data/DataHelper.cs create mode 100644 src/Marr.Data/DataMapper.cs create mode 100644 src/Marr.Data/DataMappingException.cs create mode 100644 src/Marr.Data/EntityGraph.cs create mode 100644 src/Marr.Data/EntityMerger.cs create mode 100644 src/Marr.Data/EntityReference.cs create mode 100644 src/Marr.Data/ExtensionMethods.cs create mode 100644 src/Marr.Data/GroupingKeyCollection.cs create mode 100644 src/Marr.Data/IDataMapper.cs create mode 100644 src/Marr.Data/LazyLoaded.cs create mode 100644 src/Marr.Data/MapRepository.cs create mode 100644 src/Marr.Data/Mapping/ColumnAttribute.cs create mode 100644 src/Marr.Data/Mapping/ColumnInfo.cs create mode 100644 src/Marr.Data/Mapping/ColumnMap.cs create mode 100644 src/Marr.Data/Mapping/ColumnMapBuilder.cs create mode 100644 src/Marr.Data/Mapping/ColumnMapCollection.cs create mode 100644 src/Marr.Data/Mapping/EnumConversionType.cs create mode 100644 src/Marr.Data/Mapping/FluentMappings.cs create mode 100644 src/Marr.Data/Mapping/IColumnInfo.cs create mode 100644 src/Marr.Data/Mapping/IRelationshipInfo.cs create mode 100644 src/Marr.Data/Mapping/MapBuilder.cs create mode 100644 src/Marr.Data/Mapping/MappingHelper.cs create mode 100644 src/Marr.Data/Mapping/Relationship.cs create mode 100644 src/Marr.Data/Mapping/RelationshipAttribute.cs create mode 100644 src/Marr.Data/Mapping/RelationshipBuilder.cs create mode 100644 src/Marr.Data/Mapping/RelationshipCollection.cs create mode 100644 src/Marr.Data/Mapping/RelationshipInfo.cs create mode 100644 src/Marr.Data/Mapping/Strategies/AttributeMapStrategy.cs create mode 100644 src/Marr.Data/Mapping/Strategies/ConventionMapStrategy.cs create mode 100644 src/Marr.Data/Mapping/Strategies/IMapStrategy.cs create mode 100644 src/Marr.Data/Mapping/Strategies/PropertyMapStrategy.cs create mode 100644 src/Marr.Data/Mapping/Strategies/ReflectionMapStrategyBase.cs create mode 100644 src/Marr.Data/Mapping/TableAttribute.cs create mode 100644 src/Marr.Data/Mapping/TableBuilder.cs create mode 100644 src/Marr.Data/Marr.Data.csproj create mode 100644 src/Marr.Data/Parameters/DbTypeBuilder.cs create mode 100644 src/Marr.Data/Parameters/IDbTypeBuilder.cs create mode 100644 src/Marr.Data/Parameters/ParameterChainMethods.cs create mode 100644 src/Marr.Data/QGen/DeleteQuery.cs create mode 100644 src/Marr.Data/QGen/Dialects/Dialect.cs create mode 100644 src/Marr.Data/QGen/Dialects/FirebirdDialect.cs create mode 100644 src/Marr.Data/QGen/Dialects/OracleDialect.cs create mode 100644 src/Marr.Data/QGen/Dialects/SqlServerCeDialect.cs create mode 100644 src/Marr.Data/QGen/Dialects/SqlServerDialect.cs create mode 100644 src/Marr.Data/QGen/Dialects/SqliteDialect.cs create mode 100644 src/Marr.Data/QGen/ExpressionVisitor.cs create mode 100644 src/Marr.Data/QGen/IQuery.cs create mode 100644 src/Marr.Data/QGen/IQueryBuilder.cs create mode 100644 src/Marr.Data/QGen/InsertQuery.cs create mode 100644 src/Marr.Data/QGen/InsertQueryBuilder.cs create mode 100644 src/Marr.Data/QGen/JoinBuilder.cs create mode 100644 src/Marr.Data/QGen/PagingQueryDecorator.cs create mode 100644 src/Marr.Data/QGen/QueryBuilder.cs create mode 100644 src/Marr.Data/QGen/QueryFactory.cs create mode 100644 src/Marr.Data/QGen/QueryQueueItem.cs create mode 100644 src/Marr.Data/QGen/RowCountQueryDecorator.cs create mode 100644 src/Marr.Data/QGen/SelectQuery.cs create mode 100644 src/Marr.Data/QGen/SortBuilder.cs create mode 100644 src/Marr.Data/QGen/SortColumn.cs create mode 100644 src/Marr.Data/QGen/SqlitePagingQueryDecorator.cs create mode 100644 src/Marr.Data/QGen/SqliteRowCountQueryDecorator.cs create mode 100644 src/Marr.Data/QGen/Table.cs create mode 100644 src/Marr.Data/QGen/TableCollection.cs create mode 100644 src/Marr.Data/QGen/UpdateQuery.cs create mode 100644 src/Marr.Data/QGen/UpdateQueryBuilder.cs create mode 100644 src/Marr.Data/QGen/View.cs create mode 100644 src/Marr.Data/QGen/WhereBuilder.cs create mode 100644 src/Marr.Data/Reflection/IReflectionStrategy.cs create mode 100644 src/Marr.Data/Reflection/ReflectionHelper.cs create mode 100644 src/Marr.Data/Reflection/SimpleReflectionStrategy.cs create mode 100644 src/Marr.Data/SqlModesEnum.cs create mode 100644 src/Marr.Data/UnitOfWork.cs create mode 100644 src/Marr.Data/UnitOfWorkSharedContext.cs diff --git a/src/Common/CodeAnalysisDictionary.xml b/src/Common/CodeAnalysisDictionary.xml new file mode 100644 index 000000000..857f46c4e --- /dev/null +++ b/src/Common/CodeAnalysisDictionary.xml @@ -0,0 +1,22 @@ + + + + + Ack + Minifier + Jsonp + Linktionary + Scaleout + Redis + Owin + Stringify + Unminify + Unminified + Stateful + SignalR + Hubservable + Sse + GitHub + + + \ No newline at end of file diff --git a/src/Common/CommonAssemblyInfo.cs b/src/Common/CommonAssemblyInfo.cs new file mode 100644 index 000000000..c245672c6 --- /dev/null +++ b/src/Common/CommonAssemblyInfo.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System; +using System.Reflection; +using System.Resources; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyCompany("Microsoft Open Technologies, Inc.")] +[assembly: AssemblyCopyright("© Microsoft Open Technologies, Inc. All rights reserved.")] + +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: AssemblyConfiguration("")] +[assembly: ComVisible(false)] +[assembly: CLSCompliant(false)] + +[assembly: NeutralResourcesLanguage("en-US")] \ No newline at end of file diff --git a/src/Common/CommonVersionInfo.cs b/src/Common/CommonVersionInfo.cs new file mode 100644 index 000000000..d674c376f --- /dev/null +++ b/src/Common/CommonVersionInfo.cs @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +using System.Reflection; + +[assembly: AssemblyVersion("10.0.0.*")] diff --git a/src/Common/GlobalSuppressions.cs b/src/Common/GlobalSuppressions.cs new file mode 100644 index 000000000..ec9dfe318 --- /dev/null +++ b/src/Common/GlobalSuppressions.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.md in the project root for license information. + +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. +// +// To add a suppression to this file, right-click the message in the +// Code Analysis results, point to "Suppress Message", and click +// "In Suppression File". +// You do not need to add suppressions to this file manually. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Microsoft.Design", "CA2210:AssembliesShouldHaveValidStrongNames", Justification = "Strong naming is done on the CI.")] +[assembly: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", MessageId = "param", Scope = "resource", Target = "Microsoft.AspNet.SignalR.Resources.resources")] +[assembly: SuppressMessage("Microsoft.Usage", "CA2243:AttributeStringLiteralsShouldParseCorrectly", Justification = "We use semver")] +[assembly: SuppressMessage("Microsoft.Design", "CA1065:DoNotRaiseExceptionsInUnexpectedLocations", Scope = "member", Target = "Microsoft.AspNet.SignalR.Messaging.ScaleoutTaskQueue.#.cctor()", Justification = "The task is cached")] diff --git a/src/Common/Microsoft.AspNet.SignalR.ruleset b/src/Common/Microsoft.AspNet.SignalR.ruleset new file mode 100644 index 000000000..38ad3e9ec --- /dev/null +++ b/src/Common/Microsoft.AspNet.SignalR.ruleset @@ -0,0 +1,227 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Common/Microsoft.AspNet.SignalR.targets b/src/Common/Microsoft.AspNet.SignalR.targets new file mode 100644 index 000000000..291b985a8 --- /dev/null +++ b/src/Common/Microsoft.AspNet.SignalR.targets @@ -0,0 +1,40 @@ + + + + $(ArtifactsDir)\$(MSBuildProjectName) + $(ArtifactsDir)\$(MSBuildProjectName)\bin + + + + $(MSBuildThisFileDirectory)Microsoft.AspNet.SignalR.ruleset + false + 1591 + true + + + + $(DefineConstants);CODE_ANALYSIS + 11.0 + + + + $(DefineConstants);MONO + + + + $(DefineConstants);SIGNED + true + true + $(KeyFile) + + + + + GlobalSuppressions.cs + + + + + + + \ No newline at end of file diff --git a/src/Marr.Data/Converters/BooleanIntConverter.cs b/src/Marr.Data/Converters/BooleanIntConverter.cs new file mode 100644 index 000000000..18c964d15 --- /dev/null +++ b/src/Marr.Data/Converters/BooleanIntConverter.cs @@ -0,0 +1,74 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using Marr.Data.Mapping; + +namespace Marr.Data.Converters +{ + public class BooleanIntConverter : IConverter + { + public object FromDB(ConverterContext context) + { + if (context.DbValue == DBNull.Value) + { + return DBNull.Value; + } + + int val = (int)context.DbValue; + + if (val == 1) + { + return true; + } + if (val == 0) + { + return false; + } + throw new ConversionException( + string.Format( + "The BooleanCharConverter could not convert the value '{0}' to a boolean.", + context.DbValue)); + } + + public object FromDB(ColumnMap map, object dbValue) + { + return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); + } + + public object ToDB(object clrValue) + { + bool? val = (bool?)clrValue; + + if (val == true) + { + return 1; + } + if (val == false) + { + return 0; + } + return DBNull.Value; + } + + public Type DbType + { + get + { + return typeof(int); + } + } + } +} diff --git a/src/Marr.Data/Converters/BooleanYNConverter.cs b/src/Marr.Data/Converters/BooleanYNConverter.cs new file mode 100644 index 000000000..38003939c --- /dev/null +++ b/src/Marr.Data/Converters/BooleanYNConverter.cs @@ -0,0 +1,74 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using Marr.Data.Mapping; + +namespace Marr.Data.Converters +{ + public class BooleanYNConverter : IConverter + { + public object FromDB(ConverterContext context) + { + if (context.DbValue == DBNull.Value) + { + return DBNull.Value; + } + + string val = context.DbValue.ToString(); + + if (val == "Y") + { + return true; + } + if (val == "N") + { + return false; + } + throw new ConversionException( + string.Format( + "The BooleanYNConverter could not convert the value '{0}' to a boolean.", + context.DbValue)); + } + + public object FromDB(ColumnMap map, object dbValue) + { + return FromDB(new ConverterContext {ColumnMap = map, DbValue = dbValue}); + } + + public object ToDB(object clrValue) + { + bool? val = (bool?)clrValue; + + if (val == true) + { + return "Y"; + } + if (val == false) + { + return "N"; + } + return DBNull.Value; + } + + public Type DbType + { + get + { + return typeof(string); + } + } + } +} diff --git a/src/Marr.Data/Converters/CastConverter.cs b/src/Marr.Data/Converters/CastConverter.cs new file mode 100644 index 000000000..2fa3b8eca --- /dev/null +++ b/src/Marr.Data/Converters/CastConverter.cs @@ -0,0 +1,53 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using System.Globalization; +using Marr.Data.Mapping; + +namespace Marr.Data.Converters +{ + public class CastConverter : IConverter + where TClr : IConvertible + where TDb : IConvertible + { + #region IConversion Members + + public Type DbType + { + get { return typeof(TDb); } + } + + public object FromDB(ConverterContext context) + { + TDb val = (TDb)context.DbValue; + return val.ToType(typeof(TClr), CultureInfo.InvariantCulture); + } + + public object FromDB(ColumnMap map, object dbValue) + { + return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); + } + + public object ToDB(object clrValue) + { + TClr val = (TClr)clrValue; + return val.ToType(typeof(TDb), CultureInfo.InvariantCulture); + } + + #endregion + } +} + diff --git a/src/Marr.Data/Converters/ConversionException.cs b/src/Marr.Data/Converters/ConversionException.cs new file mode 100644 index 000000000..095d48f41 --- /dev/null +++ b/src/Marr.Data/Converters/ConversionException.cs @@ -0,0 +1,26 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; + +namespace Marr.Data.Converters +{ + public class ConversionException : Exception + { + public ConversionException(string message) + : base(message) + { } + } +} diff --git a/src/Marr.Data/Converters/ConverterContext.cs b/src/Marr.Data/Converters/ConverterContext.cs new file mode 100644 index 000000000..341925077 --- /dev/null +++ b/src/Marr.Data/Converters/ConverterContext.cs @@ -0,0 +1,13 @@ +using System.Data; +using Marr.Data.Mapping; + +namespace Marr.Data.Converters +{ + public class ConverterContext + { + public ColumnMap ColumnMap { get; set; } + public object DbValue { get; set; } + public ColumnMapCollection MapCollection { get; set; } + public IDataRecord DataRecord { get; set; } + } +} \ No newline at end of file diff --git a/src/Marr.Data/Converters/EnumIntConverter.cs b/src/Marr.Data/Converters/EnumIntConverter.cs new file mode 100644 index 000000000..5fe88a411 --- /dev/null +++ b/src/Marr.Data/Converters/EnumIntConverter.cs @@ -0,0 +1,50 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using Marr.Data.Mapping; + +namespace Marr.Data.Converters +{ + public class EnumIntConverter : IConverter + { + public object FromDB(ConverterContext context) + { + if (context.DbValue == null || context.DbValue == DBNull.Value) + return null; + return Enum.ToObject(context.ColumnMap.FieldType, (int)context.DbValue); + } + + public object FromDB(ColumnMap map, object dbValue) + { + return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); + } + + public object ToDB(object clrValue) + { + if (clrValue == null) + return DBNull.Value; + return (int)clrValue; + } + + public Type DbType + { + get + { + return typeof(int); + } + } + } +} diff --git a/src/Marr.Data/Converters/EnumStringConverter.cs b/src/Marr.Data/Converters/EnumStringConverter.cs new file mode 100644 index 000000000..eb4f8b01a --- /dev/null +++ b/src/Marr.Data/Converters/EnumStringConverter.cs @@ -0,0 +1,50 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using Marr.Data.Mapping; + +namespace Marr.Data.Converters +{ + public class EnumStringConverter : IConverter + { + public object FromDB(ConverterContext context) + { + if (context.DbValue == null || context.DbValue == DBNull.Value) + return null; + return Enum.Parse(context.ColumnMap.FieldType, (string)context.DbValue); + } + + public object FromDB(ColumnMap map, object dbValue) + { + return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); + } + + public object ToDB(object clrValue) + { + if (clrValue == null) + return DBNull.Value; + return clrValue.ToString(); + } + + public Type DbType + { + get + { + return typeof(string); + } + } + } +} diff --git a/src/Marr.Data/Converters/IConverter.cs b/src/Marr.Data/Converters/IConverter.cs new file mode 100644 index 000000000..f2e9685a9 --- /dev/null +++ b/src/Marr.Data/Converters/IConverter.cs @@ -0,0 +1,30 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using Marr.Data.Mapping; + +namespace Marr.Data.Converters +{ + public interface IConverter + { + object FromDB(ConverterContext context); + + [Obsolete("use FromDB(ConverterContext context) instead")] + object FromDB(ColumnMap map, object dbValue); + object ToDB(object clrValue); + Type DbType { get; } + } +} diff --git a/src/Marr.Data/DataHelper.cs b/src/Marr.Data/DataHelper.cs new file mode 100644 index 000000000..3c6e450f5 --- /dev/null +++ b/src/Marr.Data/DataHelper.cs @@ -0,0 +1,166 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using System.Collections.Generic; +using System.Data; +using System.Reflection; +using Marr.Data.Mapping; +using System.Linq.Expressions; + +namespace Marr.Data +{ + /// + /// This class contains misc. extension methods that are used throughout the project. + /// + internal static class DataHelper + { + public static bool HasColumn(this IDataReader dr, string columnName) + { + for (int i=0; i < dr.FieldCount; i++) + { + if (dr.GetName(i).Equals(columnName, StringComparison.InvariantCultureIgnoreCase)) + return true; + } + return false; + } + + public static string ParameterPrefix(this IDbCommand command) + { + string commandType = command.GetType().Name.ToLower(); + return commandType.Contains("oracle") ? ":" : "@"; + } + + /// + /// Returns the mapped name, or the member name. + /// + /// + /// + public static string GetTableName(this MemberInfo member) + { + string tableName = MapRepository.Instance.GetTableName(member.DeclaringType); + return tableName ?? member.DeclaringType.Name; + } + + public static string GetTableName(this Type memberType) + { + return MapRepository.Instance.GetTableName(memberType); + } + + public static string GetColumName(this IColumnInfo col, bool useAltName) + { + if (useAltName) + { + return col.TryGetAltName(); + } + return col.Name; + } + + /// + /// Returns the mapped column name, or the member name. + /// + /// + /// + public static string GetColumnName(Type declaringType, string propertyName, bool useAltName) + { + // Initialize column name as member name + string columnName = propertyName; + + var columnMap = MapRepository.Instance.GetColumns(declaringType).GetByFieldName(propertyName); + + if (columnMap == null) + { + throw new InvalidOperationException(string.Format("Column map missing for field {0}.{1}", declaringType.FullName, propertyName)); + } + + if (useAltName) + { + columnName = columnMap.ColumnInfo.TryGetAltName(); + } + else + { + columnName = columnMap.ColumnInfo.Name; + } + + return columnName; + } + + /// + /// Determines a property name from a passed in expression. + /// Ex: p => p.FirstName -> "FirstName + /// + /// + /// + /// + public static string GetMemberName(this Expression> member) + { + var memberExpression = (member.Body as MemberExpression); + if (memberExpression == null) + { + memberExpression = (member.Body as UnaryExpression).Operand as MemberExpression; + } + + return memberExpression.Member.Name; + } + + public static string GetMemberName(this LambdaExpression exp) + { + var memberExpression = (exp.Body as MemberExpression); + if (memberExpression == null) + { + memberExpression = (exp.Body as UnaryExpression).Operand as MemberExpression; + } + + return memberExpression.Member.Name; + } + + public static bool ContainsMember(this List list, MemberInfo member) + { + foreach (var m in list) + { + if (m.EqualsMember(member)) + return true; + } + + return false; + } + + public static bool EqualsMember(this MemberInfo member, MemberInfo otherMember) + { + return member.Name == otherMember.Name && member.DeclaringType == otherMember.DeclaringType; + } + + /// + /// Determines if a type is not a complex object. + /// + public static bool IsSimpleType(Type type) + { + Type underlyingType = !IsNullableType(type) ? type : type.GetGenericArguments()[0]; + + return + underlyingType.IsPrimitive || + underlyingType.Equals(typeof(string)) || + underlyingType.Equals(typeof(DateTime)) || + underlyingType.Equals(typeof(decimal)) || + underlyingType.IsEnum; + } + + public static bool IsNullableType(Type theType) + { + return (theType.IsGenericType && theType.GetGenericTypeDefinition().Equals(typeof(Nullable<>))); + } + + } +} diff --git a/src/Marr.Data/DataMapper.cs b/src/Marr.Data/DataMapper.cs new file mode 100644 index 000000000..a68d050e3 --- /dev/null +++ b/src/Marr.Data/DataMapper.cs @@ -0,0 +1,958 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Data; +using System.Data.Common; +using System.Reflection; +using System.Collections; +using Marr.Data.Mapping; +using Marr.Data.Parameters; +using Marr.Data.QGen; +using System.Linq.Expressions; +using System.Diagnostics; + +namespace Marr.Data +{ + /// + /// This class is the main access point for making database related calls. + /// + public class DataMapper : IDataMapper + { + + #region - Contructor, Members - + + private DbCommand _command; + + /// + /// A database provider agnostic initialization. + /// + /// The database connection string. + public DataMapper(DbProviderFactory dbProviderFactory, string connectionString) + { + SqlMode = SqlModes.StoredProcedure; + if (dbProviderFactory == null) + throw new ArgumentNullException("dbProviderFactory"); + + if (string.IsNullOrEmpty(connectionString)) + throw new ArgumentNullException("connectionString"); + + ProviderFactory = dbProviderFactory; + + ConnectionString = connectionString; + } + + public string ConnectionString { get; private set; } + + public DbProviderFactory ProviderFactory { get; private set; } + + /// + /// Creates a new command utilizing the connection string. + /// + private DbCommand CreateNewCommand() + { + DbConnection conn = ProviderFactory.CreateConnection(); + conn.ConnectionString = ConnectionString; + DbCommand cmd = conn.CreateCommand(); + SetSqlMode(cmd); + return cmd; + } + + /// + /// Creates a new command utilizing the connection string with a given SQL command. + /// + private DbCommand CreateNewCommand(string sql) + { + DbCommand cmd = CreateNewCommand(); + cmd.CommandText = sql; + return cmd; + } + + /// + /// Gets or creates a DbCommand object. + /// + public DbCommand Command + { + get + { + // Lazy load + if (_command == null) + _command = CreateNewCommand(); + else + SetSqlMode(_command); // Set SqlMode every time. + + return _command; + } + } + + #endregion + + #region - Parameters - + + public DbParameterCollection Parameters + { + get + { + return Command.Parameters; + } + } + + public ParameterChainMethods AddParameter(string name, object value) + { + return new ParameterChainMethods(Command, name, value); + } + + public IDbDataParameter AddParameter(IDbDataParameter parameter) + { + // Convert null values to DBNull.Value + if (parameter.Value == null) + parameter.Value = DBNull.Value; + + Parameters.Add(parameter); + return parameter; + } + + #endregion + + #region - SP / SQL Mode - + + /// + /// Gets or sets a value that determines whether the DataMapper will + /// use a stored procedure or a sql text command to access + /// the database. The default is stored procedure. + /// + public SqlModes SqlMode { get; set; } + + /// + /// Sets the DbCommand objects CommandType to the current SqlMode. + /// + /// The DbCommand object we are modifying. + /// Returns the same DbCommand that was passed in. + private DbCommand SetSqlMode(DbCommand command) + { + if (SqlMode == SqlModes.StoredProcedure) + command.CommandType = CommandType.StoredProcedure; + else + command.CommandType = CommandType.Text; + + return command; + } + + #endregion + + #region - ExecuteScalar, ExecuteNonQuery, ExecuteReader - + + /// + /// Executes a stored procedure that returns a scalar value. + /// + /// The SQL command to execute. + /// A scalar value + public object ExecuteScalar(string sql) + { + if (string.IsNullOrEmpty(sql)) + throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required"); + Command.CommandText = sql; + + try + { + OpenConnection(); + return Command.ExecuteScalar(); + } + finally + { + CloseConnection(); + } + } + + /// + /// Executes a non query that returns an integer. + /// + /// The SQL command to execute. + /// An integer value + public int ExecuteNonQuery(string sql) + { + if (string.IsNullOrEmpty(sql)) + throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required"); + Command.CommandText = sql; + + try + { + OpenConnection(); + return Command.ExecuteNonQuery(); + } + finally + { + CloseConnection(); + } + } + + /// + /// Executes a DataReader that can be controlled using a Func delegate. + /// (Note that reader.Read() will be called automatically). + /// + /// The type that will be return in the result set. + /// The sql statement that will be executed. + /// The function that will build the the TResult set. + /// An IEnumerable of TResult. + public IEnumerable ExecuteReader(string sql, Func func) + { + if (string.IsNullOrEmpty(sql)) + throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required"); + Command.CommandText = sql; + + try + { + OpenConnection(); + + var list = new List(); + DbDataReader reader = null; + try + { + reader = Command.ExecuteReader(); + + while (reader.Read()) + { + list.Add(func(reader)); + } + + return list; + } + finally + { + if (reader != null) reader.Close(); + } + } + finally + { + CloseConnection(); + } + } + + /// + /// Executes a DataReader that can be controlled using an Action delegate. + /// + /// The sql statement that will be executed. + /// The delegate that will work with the result set. + public void ExecuteReader(string sql, Action action) + { + if (string.IsNullOrEmpty(sql)) + throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required"); + + Command.CommandText = sql; + + try + { + OpenConnection(); + + DbDataReader reader = null; + try + { + reader = Command.ExecuteReader(); + + while (reader.Read()) + { + action(reader); + } + } + finally + { + if (reader != null) reader.Close(); + } + } + finally + { + CloseConnection(); + } + } + + #endregion + + #region - DataSets - + + public DataSet GetDataSet(string sql) + { + return GetDataSet(sql, new DataSet(), null); + } + + public DataSet GetDataSet(string sql, DataSet ds, string tableName) + { + if (string.IsNullOrEmpty(sql)) + throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required"); + + try + { + using (DbDataAdapter adapter = ProviderFactory.CreateDataAdapter()) + { + Command.CommandText = sql; + adapter.SelectCommand = Command; + + if (ds == null) + ds = new DataSet(); + + OpenConnection(); + + if (string.IsNullOrEmpty(tableName)) + adapter.Fill(ds); + else + adapter.Fill(ds, tableName); + + return ds; + } + } + finally + { + CloseConnection(); // Clears parameters + } + } + + public DataTable GetDataTable(string sql) + { + return GetDataTable(sql, null, null); + } + + public DataTable GetDataTable(string sql, DataTable dt, string tableName) + { + if (string.IsNullOrEmpty(sql)) + throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required"); + + try + { + using (DbDataAdapter adapter = ProviderFactory.CreateDataAdapter()) + { + Command.CommandText = sql; + adapter.SelectCommand = Command; + + if (dt == null) + dt = new DataTable(); + + adapter.Fill(dt); + + if (!string.IsNullOrEmpty(tableName)) + dt.TableName = tableName; + + return dt; + } + } + finally + { + CloseConnection(); // Clears parameters + } + } + + public int UpdateDataSet(DataSet ds, string sql) + { + if (string.IsNullOrEmpty(sql)) + throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required"); + + if (ds == null) + throw new ArgumentNullException("ds", "DataSet cannot be null."); + + DbDataAdapter adapter = null; + + try + { + adapter = ProviderFactory.CreateDataAdapter(); + + adapter.UpdateCommand = Command; + adapter.UpdateCommand.CommandText = sql; + + return adapter.Update(ds); + } + finally + { + if (adapter.UpdateCommand != null) + adapter.UpdateCommand.Dispose(); + + adapter.Dispose(); + } + } + + public int InsertDataTable(DataTable table, string insertSP) + { + return InsertDataTable(table, insertSP, UpdateRowSource.None); + } + + public int InsertDataTable(DataTable dt, string sql, UpdateRowSource updateRowSource) + { + if (string.IsNullOrEmpty(sql)) + throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required"); + + if (dt == null) + throw new ArgumentNullException("dt", "DataTable cannot be null."); + + DbDataAdapter adapter = null; + + try + { + adapter = ProviderFactory.CreateDataAdapter(); + + adapter.InsertCommand = Command; + adapter.InsertCommand.CommandText = sql; + + adapter.InsertCommand.UpdatedRowSource = updateRowSource; + + return adapter.Update(dt); + } + finally + { + if (adapter.InsertCommand != null) + adapter.InsertCommand.Dispose(); + + adapter.Dispose(); + } + } + + public int DeleteDataTable(DataTable dt, string sql) + { + if (string.IsNullOrEmpty(sql)) + throw new ArgumentNullException("sql", "A SQL query or stored procedure name is required"); + + if (dt == null) + throw new ArgumentNullException("dt", "DataSet cannot be null."); + + DbDataAdapter adapter = null; + + try + { + adapter = ProviderFactory.CreateDataAdapter(); + + adapter.DeleteCommand = Command; + adapter.DeleteCommand.CommandText = sql; + + return adapter.Update(dt); + } + finally + { + if (adapter.DeleteCommand != null) + adapter.DeleteCommand.Dispose(); + + adapter.Dispose(); + } + } + + #endregion + + #region - Find - + + public T Find(string sql) + { + return Find(sql, default(T)); + } + + /// + /// Returns an entity of type T. + /// + /// The type of entity that is to be instantiated and loaded with values. + /// The SQL command to execute. + /// An instantiated and loaded entity of type T. + public T Find(string sql, T ent) + { + if (string.IsNullOrEmpty(sql)) + throw new ArgumentNullException("sql", "A stored procedure name has not been specified for 'Find'."); + + Type entityType = typeof(T); + Command.CommandText = sql; + + MapRepository repository = MapRepository.Instance; + ColumnMapCollection mappings = repository.GetColumns(entityType); + + bool isSimpleType = DataHelper.IsSimpleType(typeof(T)); + + try + { + OpenConnection(); + var mappingHelper = new MappingHelper(this); + + using (DbDataReader reader = Command.ExecuteReader()) + { + if (reader.Read()) + { + if (isSimpleType) + { + return mappingHelper.LoadSimpleValueFromFirstColumn(reader); + } + else + { + if (ent == null) + ent = (T)mappingHelper.CreateAndLoadEntity(mappings, reader, false); + else + mappingHelper.LoadExistingEntity(mappings, reader, ent, false); + } + } + } + } + finally + { + CloseConnection(); + } + + return ent; + } + + #endregion + + #region - Query - + + /// + /// Creates a QueryBuilder that allows you to build a query. + /// + /// The type of object that will be queried. + /// Returns a QueryBuilder of T. + public QueryBuilder Query() + { + var dialect = QueryFactory.CreateDialect(this); + return new QueryBuilder(this, dialect); + } + + /// + /// Returns the results of a query. + /// Uses a List of type T to return the data. + /// + /// Returns a list of the specified type. + public List Query(string sql) + { + return (List)Query(sql, new List()); + } + + /// + /// Returns the results of a SP query. + /// + /// Returns a list of the specified type. + public ICollection Query(string sql, ICollection entityList) + { + return Query(sql, entityList, false); + } + + internal ICollection Query(string sql, ICollection entityList, bool useAltName) + { + if (entityList == null) + throw new ArgumentNullException("entityList", "ICollection instance cannot be null."); + + if (string.IsNullOrEmpty(sql)) + throw new ArgumentNullException("sql", "A query or stored procedure has not been specified for 'Query'."); + + var mappingHelper = new MappingHelper(this); + Type entityType = typeof(T); + Command.CommandText = sql; + ColumnMapCollection mappings = MapRepository.Instance.GetColumns(entityType); + + bool isSimpleType = DataHelper.IsSimpleType(typeof(T)); + + try + { + OpenConnection(); + using (DbDataReader reader = Command.ExecuteReader()) + { + while (reader.Read()) + { + if (isSimpleType) + { + entityList.Add(mappingHelper.LoadSimpleValueFromFirstColumn(reader)); + } + else + { + entityList.Add((T)mappingHelper.CreateAndLoadEntity(mappings, reader, useAltName)); + } + } + } + } + finally + { + CloseConnection(); + } + + return entityList; + } + + #endregion + + #region - Query to Graph - + + public List QueryToGraph(string sql) + { + return (List)QueryToGraph(sql, new List()); + } + + public ICollection QueryToGraph(string sql, ICollection entityList) + { + EntityGraph graph = new EntityGraph(typeof(T), (IList)entityList); + return QueryToGraph(sql, graph, new List()); + } + + /// + /// Queries a view that joins multiple tables and returns an object graph. + /// + /// + /// + /// + /// Coordinates loading all objects in the graph.. + /// + internal ICollection QueryToGraph(string sql, EntityGraph graph, List childrenToLoad) + { + if (string.IsNullOrEmpty(sql)) + throw new ArgumentNullException("sql", "sql"); + + var mappingHelper = new MappingHelper(this); + Type parentType = typeof(T); + Command.CommandText = sql; + + try + { + OpenConnection(); + using (DbDataReader reader = Command.ExecuteReader()) + { + while (reader.Read()) + { + // The entire EntityGraph is traversed for each record, + // and multiple entities are created from each view record. + foreach (EntityGraph lvl in graph) + { + if (lvl.IsParentReference) + { + // A child specified a circular reference to its previously loaded parent + lvl.AddParentReference(); + } + else if (childrenToLoad.Count > 0 && !lvl.IsRoot && !childrenToLoad.ContainsMember(lvl.Member)) + { + // A list of relationships-to-load was specified and this relationship was not included + continue; + } + else if (lvl.IsNewGroup(reader)) + { + // Create a new entity with the data reader + var newEntity = mappingHelper.CreateAndLoadEntity(lvl.EntityType, lvl.Columns, reader, true); + + // Add entity to the appropriate place in the object graph + lvl.AddEntity(newEntity); + } + } + } + } + } + finally + { + CloseConnection(); + } + + return (ICollection)graph.RootList; + } + + #endregion + + #region - Update - + + public UpdateQueryBuilder Update() + { + return new UpdateQueryBuilder(this); + } + + public int Update(T entity, Expression> filter) + { + return Update() + .Entity(entity) + .Where(filter) + .Execute(); + } + + public int Update(string tableName, T entity, Expression> filter) + { + return Update() + .TableName(tableName) + .Entity(entity) + .Where(filter) + .Execute(); + } + + public int Update(T entity, string sql) + { + return Update() + .Entity(entity) + .QueryText(sql) + .Execute(); + } + + #endregion + + #region - Insert - + + /// + /// Creates an InsertQueryBuilder that allows you to build an insert statement. + /// This method gives you the flexibility to manually configure all options of your insert statement. + /// Note: You must manually call the Execute() chaining method to run the query. + /// + public InsertQueryBuilder Insert() + { + return new InsertQueryBuilder(this); + } + + /// + /// Generates and executes an insert query for the given entity. + /// This overload will automatically run an identity query if you have mapped an auto-incrementing column, + /// and if an identity query has been implemented for your current database dialect. + /// + public object Insert(T entity) + { + var columns = MapRepository.Instance.GetColumns(typeof(T)); + var dialect = QueryFactory.CreateDialect(this); + var builder = Insert().Entity(entity); + + // If an auto-increment column exists and this dialect has an identity query... + if (columns.Exists(c => c.ColumnInfo.IsAutoIncrement) && dialect.HasIdentityQuery) + { + builder.GetIdentity(); + } + + return builder.Execute(); + } + + /// + /// Generates and executes an insert query for the given entity. + /// This overload will automatically run an identity query if you have mapped an auto-incrementing column, + /// and if an identity query has been implemented for your current database dialect. + /// + public object Insert(string tableName, T entity) + { + var columns = MapRepository.Instance.GetColumns(typeof(T)); + var dialect = QueryFactory.CreateDialect(this); + var builder = Insert().Entity(entity).TableName(tableName); + + // If an auto-increment column exists and this dialect has an identity query... + if (columns.Exists(c => c.ColumnInfo.IsAutoIncrement) && dialect.HasIdentityQuery) + { + builder.GetIdentity(); + } + + return builder.Execute(); + } + + /// + /// Executes an insert query for the given entity using the given sql insert statement. + /// This overload will automatically run an identity query if you have mapped an auto-incrementing column, + /// and if an identity query has been implemented for your current database dialect. + /// + public object Insert(T entity, string sql) + { + var columns = MapRepository.Instance.GetColumns(typeof(T)); + var dialect = QueryFactory.CreateDialect(this); + var builder = Insert().Entity(entity).QueryText(sql); + + // If an auto-increment column exists and this dialect has an identity query... + if (columns.Exists(c => c.ColumnInfo.IsAutoIncrement) && dialect.HasIdentityQuery) + { + builder.GetIdentity(); + } + + return builder.Execute(); + } + + #endregion + + #region - Delete - + + public int Delete(Expression> filter) + { + return Delete(null, filter); + } + + public int Delete(string tableName, Expression> filter) + { + // Remember sql mode + var previousSqlMode = SqlMode; + SqlMode = SqlModes.Text; + + var mappingHelper = new MappingHelper(this); + if (tableName == null) + { + tableName = MapRepository.Instance.GetTableName(typeof(T)); + } + var dialect = QueryFactory.CreateDialect(this); + TableCollection tables = new TableCollection(); + tables.Add(new Table(typeof(T))); + var where = new WhereBuilder(Command, dialect, filter, tables, false, false); + IQuery query = QueryFactory.CreateDeleteQuery(dialect, tables[0], where.ToString()); + Command.CommandText = query.Generate(); + + int rowsAffected = 0; + + try + { + OpenConnection(); + rowsAffected = Command.ExecuteNonQuery(); + } + finally + { + CloseConnection(); + } + + // Return to previous sql mode + SqlMode = previousSqlMode; + + return rowsAffected; + } + + #endregion + + #region - Events - + + public event EventHandler OpeningConnection; + + public event EventHandler ClosingConnection; + + #endregion + + #region - Connections / Transactions - + + protected virtual void OnOpeningConnection() + { + if (OpeningConnection != null) + OpeningConnection(this, EventArgs.Empty); + } + + protected virtual void OnClosingConnection() + { + WriteToTraceLog(); + + if (ClosingConnection != null) + ClosingConnection(this, EventArgs.Empty); + } + + protected internal void OpenConnection() + { + OnOpeningConnection(); + + if (Command.Connection.State != ConnectionState.Open) + Command.Connection.Open(); + } + + protected internal void CloseConnection() + { + OnClosingConnection(); + + Command.Parameters.Clear(); + Command.CommandText = string.Empty; + + if (Command.Transaction == null) + Command.Connection.Close(); // Only close if no transaction is present + + UnbindEvents(); + } + + private void WriteToTraceLog() + { + if (MapRepository.Instance.EnableTraceLogging) + { + var sb = new StringBuilder(); + sb.AppendLine(); + sb.AppendLine("==== Begin Query Trace ===="); + sb.AppendLine(); + sb.AppendLine("QUERY TYPE:"); + sb.AppendLine(Command.CommandType.ToString()); + sb.AppendLine(); + sb.AppendLine("QUERY TEXT:"); + sb.AppendLine(Command.CommandText); + sb.AppendLine(); + sb.AppendLine("PARAMETERS:"); + foreach (IDbDataParameter p in Parameters) + { + object val = (p.Value != null && p.Value is string) ? string.Format("\"{0}\"", p.Value) : p.Value; + sb.AppendFormat("{0} = [{1}]", p.ParameterName, val ?? "NULL").AppendLine(); + } + sb.AppendLine(); + sb.AppendLine("==== End Query Trace ===="); + sb.AppendLine(); + + Trace.Write(sb.ToString()); + } + } + + private void UnbindEvents() + { + OpeningConnection = null; + ClosingConnection = null; + } + + public void BeginTransaction(IsolationLevel isolationLevel) + { + OpenConnection(); + DbTransaction trans = Command.Connection.BeginTransaction(isolationLevel); + Command.Transaction = trans; + } + + public void RollBack() + { + try + { + if (Command.Transaction != null) + Command.Transaction.Rollback(); + } + finally + { + Command.Connection.Close(); + } + } + + public void Commit() + { + try + { + if (Command.Transaction != null) + Command.Transaction.Commit(); + } + finally + { + Command.Connection.Close(); + } + } + + #endregion + + #region - IDisposable Members - + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); // In case a derived class implements a finalizer + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + if (_command != null) + { + if (_command.Transaction != null) + { + _command.Transaction.Dispose(); + _command.Transaction = null; + } + + if (_command.Connection != null) + { + _command.Connection.Dispose(); + _command.Connection = null; + } + + _command.Dispose(); + _command = null; + } + } + } + + #endregion + + } +} diff --git a/src/Marr.Data/DataMappingException.cs b/src/Marr.Data/DataMappingException.cs new file mode 100644 index 000000000..f1e888b5d --- /dev/null +++ b/src/Marr.Data/DataMappingException.cs @@ -0,0 +1,22 @@ +using System; + +namespace Marr.Data +{ + public class DataMappingException : Exception + { + public DataMappingException() + : base() + { + } + + public DataMappingException(string message) + : base(message) + { + } + + public DataMappingException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/Marr.Data/EntityGraph.cs b/src/Marr.Data/EntityGraph.cs new file mode 100644 index 000000000..72d28dcdf --- /dev/null +++ b/src/Marr.Data/EntityGraph.cs @@ -0,0 +1,419 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using Marr.Data.Mapping; +using System.Reflection; + +namespace Marr.Data +{ + /// + /// Holds metadata about an object graph that is being queried and eagerly loaded. + /// Contains all metadata needed to instantiate the object and fill it with data from a DataReader. + /// Does not iterate through lazy loaded child relationships. + /// + internal class EntityGraph : IEnumerable + { + private MapRepository _repos; + private EntityGraph _parent; + private Type _entityType; + private Relationship _relationship; + private ColumnMapCollection _columns; + private RelationshipCollection _relationships; + private List _children; + private object _entity; + private GroupingKeyCollection _groupingKeyColumns; + private Dictionary _entityReferences; + + internal IList RootList { get; private set; } + internal bool IsParentReference { get; private set; } + + /// + /// Recursively builds an entity graph of the given parent type. + /// + /// + public EntityGraph(Type entityType, IList rootList) + : this(entityType, null, null) // Recursively constructs hierarchy + { + RootList = rootList; + } + + /// + /// Recursively builds entity graph hierarchy. + /// + /// + /// + /// + private EntityGraph(Type entityType, EntityGraph parent, Relationship relationship) + { + _repos = MapRepository.Instance; + + _entityType = entityType; + _parent = parent; + _relationship = relationship; + IsParentReference = !IsRoot && AnyParentsAreOfType(entityType); + if (!IsParentReference) + { + _columns = _repos.GetColumns(entityType); + } + + _relationships = _repos.GetRelationships(entityType); + _children = new List(); + Member = relationship != null ? relationship.Member : null; + _entityReferences = new Dictionary(); + + if (IsParentReference) + { + return; + } + + // Create a new EntityGraph for each child relationship that is not lazy loaded + foreach (Relationship childRelationship in Relationships) + { + if (!childRelationship.IsLazyLoaded) + { + _children.Add(new EntityGraph(childRelationship.RelationshipInfo.EntityType, this, childRelationship)); + } + } + } + + public MemberInfo Member { get; private set; } + + /// + /// Gets the parent of this EntityGraph. + /// + public EntityGraph Parent + { + get + { + return _parent; + } + } + + /// + /// Gets the Type of this EntityGraph. + /// + public Type EntityType + { + get { return _entityType; } + } + + /// + /// Gets a boolean than indicates whether this entity is the root node in the graph. + /// + public bool IsRoot + { + get + { + return _parent == null; + } + } + + /// + /// Gets a boolean that indicates whether this entity is a child. + /// + public bool IsChild + { + get + { + return _parent != null; + } + } + + /// + /// Gets the columns mapped to this entity. + /// + public ColumnMapCollection Columns + { + get { return _columns; } + } + + /// + /// Gets the relationships mapped to this entity. + /// + public RelationshipCollection Relationships + { + get { return _relationships; } + } + + /// + /// A list of EntityGraph objects that hold metadata about the child entities that will be loaded. + /// + public List Children + { + get { return _children; } + } + + /// + /// Adds an Child in the graph for LazyLoaded property. + /// + public void AddLazyRelationship(Relationship childRelationship) + { + _children.Add(new EntityGraph(childRelationship.RelationshipInfo.EntityType.GetGenericArguments()[0], this, childRelationship)); + } + + /// + /// Adds an entity to the appropriate place in the object graph. + /// + /// + public void AddEntity(object entityInstance) + { + _entity = entityInstance; + + // Add newly created entityInstance to list (Many) or set it to field (One) + if (IsRoot) + { + RootList.Add(entityInstance); + } + else if (_relationship.RelationshipInfo.RelationType == RelationshipTypes.Many) + { + var list = _parent._entityReferences[_parent.GroupingKeyColumns.GroupingKey] + .ChildLists[_relationship.Member.Name]; + + list.Add(entityInstance); + } + else // RelationTypes.One + { + if (_relationship.IsLazyLoaded) + _relationship.Setter(_parent._entity, Activator.CreateInstance(_relationship.MemberType, entityInstance)); + else + _relationship.Setter(_parent._entity, entityInstance); + } + + EntityReference entityRef = new EntityReference(entityInstance); + _entityReferences.Add(GroupingKeyColumns.GroupingKey, entityRef); + + InitOneToManyChildLists(entityRef); + } + + /// + /// Searches for a previously loaded parent entity and then sets that reference to the mapped Relationship property. + /// + public void AddParentReference() + { + var parentReference = FindParentReference(); + _relationship.Setter(_parent._entity, parentReference); + } + + /// + /// Concatenates the values of the GroupingKeys property and compares them + /// against the LastKeyGroup property. Returns true if the values are different, + /// or false if the values are the same. + /// The currently concatenated keys are saved in the LastKeyGroup property. + /// + /// + /// + public bool IsNewGroup(DbDataReader reader) + { + bool isNewGroup = false; + + // Get primary keys from parent entity and any one-to-one child entites + GroupingKeyCollection groupingKeyColumns = GroupingKeyColumns; + + // Concatenate column values + KeyGroupInfo keyGroupInfo = groupingKeyColumns.CreateGroupingKey(reader); + + if (!keyGroupInfo.HasNullKey && !_entityReferences.ContainsKey(keyGroupInfo.GroupingKey)) + { + isNewGroup = true; + } + + return isNewGroup; + } + + /// + /// Gets the GroupingKeys for this entity. + /// GroupingKeys determine when to create and add a new entity to the graph. + /// + /// + /// A simple entity with no relationships will return only its PrimaryKey columns. + /// A parent entity with one-to-one child relationships will include its own PrimaryKeys, + /// and it will recursively traverse all Children with one-to-one relationships and add their PrimaryKeys. + /// A child entity that has a one-to-one relationship with its parent will use the same + /// GroupingKeys already defined by its parent. + /// + public GroupingKeyCollection GroupingKeyColumns + { + get + { + if (_groupingKeyColumns == null) + _groupingKeyColumns = GetGroupingKeyColumns(); + + return _groupingKeyColumns; + } + } + + private bool AnyParentsAreOfType(Type type) + { + EntityGraph parent = _parent; + while (parent != null) + { + if (parent._entityType == type) + { + return true; + } + parent = parent._parent; + } + + return false; + } + + private object FindParentReference() + { + var parent = Parent.Parent; + while (parent != null) + { + if (parent._entityType == _relationship.MemberType) + { + return parent._entity; + } + + parent = parent.Parent; + } + + return null; + } + + /// + /// Initializes the owning lists on many-to-many Children. + /// + /// + private void InitOneToManyChildLists(EntityReference entityRef) + { + // Get a reference to the parent's the childrens' OwningLists to the parent entity + for (int i = 0; i < Relationships.Count; i++) + { + Relationship relationship = Relationships[i]; + if (relationship.RelationshipInfo.RelationType == RelationshipTypes.Many) + { + try + { + IList list = (IList)_repos.ReflectionStrategy.CreateInstance(relationship.MemberType); + relationship.Setter(entityRef.Entity, list); + + // Save a reference to each 1-M list + entityRef.AddChildList(relationship.Member.Name, list); + } + catch (Exception ex) + { + throw new DataMappingException( + string.Format("{0}.{1} is a \"Many\" relationship type so it must derive from IList.", + entityRef.Entity.GetType().Name, relationship.Member.Name), + ex); + } + } + } + } + + /// + /// Gets a list of keys to group by. + /// + /// + /// When converting an unnormalized set of data from a database view, + /// a new entity is only created when the grouping keys have changed. + /// NOTE: This behavior works on the assumption that the view result set + /// has been sorted by the root entity primary key(s), followed by the + /// child entity primary keys. + /// + /// + private GroupingKeyCollection GetGroupingKeyColumns() + { + // Get primary keys for this parent entity + GroupingKeyCollection groupingKeyColumns = new GroupingKeyCollection(); + groupingKeyColumns.PrimaryKeys.AddRange(Columns.PrimaryKeys); + + // The following conditions should fail with an exception: + // 1) Any parent entity (entity with children) must have at least one PK specified or an exception will be thrown + // 2) All 1-M relationship entities must have at least one PK specified + // * Only 1-1 entities with no children are allowed to have 0 PKs specified. + if ((groupingKeyColumns.PrimaryKeys.Count == 0 && _children.Count > 0) || + (groupingKeyColumns.PrimaryKeys.Count == 0 && !IsRoot && _relationship.RelationshipInfo.RelationType == RelationshipTypes.Many)) + throw new MissingPrimaryKeyException(string.Format("There are no primary key mappings defined for the following entity: '{0}'.", EntityType.Name)); + + // Add parent's keys + if (IsChild) + groupingKeyColumns.ParentPrimaryKeys.AddRange(Parent.GroupingKeyColumns); + + return groupingKeyColumns; + } + + #region IEnumerable Members + + public IEnumerator GetEnumerator() + { + return TraverseGraph(this); + } + + /// + /// Recursively traverses through every entity in the EntityGraph. + /// + /// + /// + private static IEnumerator TraverseGraph(EntityGraph entityGraph) + { + Stack stack = new Stack(); + stack.Push(entityGraph); + + while (stack.Count > 0) + { + EntityGraph node = stack.Pop(); + yield return node; + + foreach (EntityGraph childGraph in node.Children) + { + stack.Push(childGraph); + } + } + } + + + #endregion + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + } +} + +public struct KeyGroupInfo +{ + private string _groupingKey; + private bool _hasNullKey; + + public KeyGroupInfo(string groupingKey, bool hasNullKey) + { + _groupingKey = groupingKey; + _hasNullKey = hasNullKey; + } + + public string GroupingKey + { + get { return _groupingKey; } + } + + public bool HasNullKey + { + get { return _hasNullKey; } + } +} diff --git a/src/Marr.Data/EntityMerger.cs b/src/Marr.Data/EntityMerger.cs new file mode 100644 index 000000000..246421567 --- /dev/null +++ b/src/Marr.Data/EntityMerger.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; + +namespace Marr.Data +{ + /// + /// This utility class allows you to join two existing entity collections. + /// + public class EntityMerger + { + /// + /// Joines to existing entity collections. + /// + /// The parent entity type. + /// The child entity type. + /// The parent entities. + /// The child entities + /// A predicate that defines the relationship between the parent and child entities. Returns true if they are related. + /// An action that adds a related child to the parent. + public static void Merge(IEnumerable parentList, IEnumerable childList, Func relationship, Action mergeAction) + { + foreach (TParent parent in parentList) + { + foreach (TChild child in childList) + { + if (relationship(parent, child)) + { + mergeAction(parent, child); + } + } + } + } + } +} diff --git a/src/Marr.Data/EntityReference.cs b/src/Marr.Data/EntityReference.cs new file mode 100644 index 000000000..2062fad3d --- /dev/null +++ b/src/Marr.Data/EntityReference.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Collections; + +namespace Marr.Data +{ + /// + /// Stores an entity along with all of its 1-M IList references. + /// + public class EntityReference + { + public EntityReference(object entity) + { + Entity = entity; + ChildLists = new Dictionary(); + } + + public object Entity { get; private set; } + public Dictionary ChildLists { get; private set; } + + public void AddChildList(string memberName, IList list) + { + if (ChildLists.ContainsKey(memberName)) + ChildLists[memberName] = list; + else + ChildLists.Add(memberName, list); + } + } +} diff --git a/src/Marr.Data/ExtensionMethods.cs b/src/Marr.Data/ExtensionMethods.cs new file mode 100644 index 000000000..85292083e --- /dev/null +++ b/src/Marr.Data/ExtensionMethods.cs @@ -0,0 +1,19 @@ +using System.Data.Common; + +namespace Marr.Data +{ + /// + /// This class contains public extension methods. + /// + public static class ExtensionMethods + { + /// + /// Gets a value from a DbDataReader by using the column name; + /// + public static T GetValue(this DbDataReader reader, string columnName) + { + int ordinal = reader.GetOrdinal(columnName); + return (T)reader.GetValue(ordinal); + } + } +} diff --git a/src/Marr.Data/GroupingKeyCollection.cs b/src/Marr.Data/GroupingKeyCollection.cs new file mode 100644 index 000000000..d38907d9f --- /dev/null +++ b/src/Marr.Data/GroupingKeyCollection.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using Marr.Data.Mapping; +using System.Data.Common; + +namespace Marr.Data +{ + public class GroupingKeyCollection : IEnumerable + { + public GroupingKeyCollection() + { + PrimaryKeys = new ColumnMapCollection(); + ParentPrimaryKeys = new ColumnMapCollection(); + } + + public ColumnMapCollection PrimaryKeys { get; private set; } + public ColumnMapCollection ParentPrimaryKeys { get; private set; } + + public int Count + { + get + { + return PrimaryKeys.Count + ParentPrimaryKeys.Count; + } + } + + /// + /// Gets the PK values that define this entity in the graph. + /// + internal string GroupingKey { get; private set; } + + /// + /// Returns a concatented string containing the primary key values of the current record. + /// + /// The open data reader. + /// Returns the primary key value(s) as a string. + internal KeyGroupInfo CreateGroupingKey(DbDataReader reader) + { + StringBuilder pkValues = new StringBuilder(); + bool hasNullValue = false; + + foreach (ColumnMap pkColumn in this) + { + object pkValue = reader[pkColumn.ColumnInfo.GetColumName(true)]; + + if (pkValue == DBNull.Value) + hasNullValue = true; + + pkValues.Append(pkValue.ToString()); + } + + GroupingKey = pkValues.ToString(); + + return new KeyGroupInfo(GroupingKey, hasNullValue); + } + + #region IEnumerable Members + + public IEnumerator GetEnumerator() + { + foreach (ColumnMap map in ParentPrimaryKeys) + { + yield return map; + } + + foreach (ColumnMap map in PrimaryKeys) + { + yield return map; + } + } + + #endregion + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + } +} diff --git a/src/Marr.Data/IDataMapper.cs b/src/Marr.Data/IDataMapper.cs new file mode 100644 index 000000000..6d1eca49e --- /dev/null +++ b/src/Marr.Data/IDataMapper.cs @@ -0,0 +1,219 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using System.Data; +using System.Data.Common; +using System.Collections.Generic; +using Marr.Data.Parameters; +using System.Linq.Expressions; +using Marr.Data.QGen; + +namespace Marr.Data +{ + public interface IDataMapper : IDisposable + { + #region - Contructor, Members - + + string ConnectionString { get; } + DbProviderFactory ProviderFactory { get; } + DbCommand Command { get; } + + /// + /// Gets or sets a value that determines whether the DataMapper will + /// use a stored procedure or a sql text command to access + /// the database. The default is stored procedure. + /// + SqlModes SqlMode { get; set; } + + #endregion + + #region - Update - + + UpdateQueryBuilder Update(); + int Update(T entity, Expression> filter); + int Update(string tableName, T entity, Expression> filter); + int Update(T entity, string sql); + + #endregion + + #region - Insert - + + /// + /// Creates an InsertQueryBuilder that allows you to build an insert statement. + /// This method gives you the flexibility to manually configure all options of your insert statement. + /// Note: You must manually call the Execute() chaining method to run the query. + /// + InsertQueryBuilder Insert(); + + /// + /// Generates and executes an insert query for the given entity. + /// This overload will automatically run an identity query if you have mapped an auto-incrementing column, + /// and if an identity query has been implemented for your current database dialect. + /// + object Insert(T entity); + + /// + /// Generates and executes an insert query for the given entity. + /// This overload will automatically run an identity query if you have mapped an auto-incrementing column, + /// and if an identity query has been implemented for your current database dialect. + /// + object Insert(string tableName, T entity); + + /// + /// Executes an insert query for the given entity using the given sql insert statement. + /// This overload will automatically run an identity query if you have mapped an auto-incrementing column, + /// and if an identity query has been implemented for your current database dialect. + /// + object Insert(T entity, string sql); + + #endregion + + #region - Delete - + + int Delete(Expression> filter); + int Delete(string tableName, Expression> filter); + + #endregion + + #region - Connections / Transactions - + + void BeginTransaction(IsolationLevel isolationLevel); + void RollBack(); + void Commit(); + event EventHandler OpeningConnection; + + #endregion + + #region - ExecuteScalar, ExecuteNonQuery, ExecuteReader - + + /// + /// Executes a non query that returns an integer. + /// + /// The SQL command to execute. + /// An integer value + int ExecuteNonQuery(string sql); + + /// + /// Executes a stored procedure that returns a scalar value. + /// + /// The SQL command to execute. + /// A scalar value + object ExecuteScalar(string sql); + + /// + /// Executes a DataReader that can be controlled using a Func delegate. + /// (Note that reader.Read() will be called automatically). + /// + /// The type that will be return in the result set. + /// The sql statement that will be executed. + /// The function that will build the the TResult set. + /// An IEnumerable of TResult. + IEnumerable ExecuteReader(string sql, Func func); + + /// + /// Executes a DataReader that can be controlled using an Action delegate. + /// + /// The sql statement that will be executed. + /// The delegate that will work with the result set. + void ExecuteReader(string sql, Action action); + + #endregion + + #region - DataSets - + + DataSet GetDataSet(string sql); + DataSet GetDataSet(string sql, DataSet ds, string tableName); + DataTable GetDataTable(string sql, DataTable dt, string tableName); + DataTable GetDataTable(string sql); + int InsertDataTable(DataTable table, string insertSP); + int InsertDataTable(DataTable table, string insertSP, UpdateRowSource updateRowSource); + int UpdateDataSet(DataSet ds, string updateSP); + int DeleteDataTable(DataTable dt, string deleteSP); + + #endregion + + #region - Parameters - + + DbParameterCollection Parameters { get; } + ParameterChainMethods AddParameter(string name, object value); + IDbDataParameter AddParameter(IDbDataParameter parameter); + + #endregion + + #region - Find - + + /// + /// Returns an entity of type T. + /// + /// The type of entity that is to be instantiated and loaded with values. + /// The SQL command to execute. + /// An instantiated and loaded entity of type T. + T Find(string sql); + + /// + /// Returns an entity of type T. + /// + /// The type of entity that is to be instantiated and loaded with values. + /// The SQL command to execute. + /// A previously instantiated entity that will be loaded with values. + /// An instantiated and loaded entity of type T. + T Find(string sql, T ent); + + #endregion + + #region - Query - + + /// + /// Creates a QueryBuilder that allows you to build a query. + /// + /// The type of object that will be queried. + /// Returns a QueryBuilder of T. + QueryBuilder Query(); + + /// + /// Returns the results of a query. + /// Uses a List of type T to return the data. + /// + /// The type of object that will be queried. + /// Returns a list of the specified type. + List Query(string sql); + + /// + /// Returns the results of a query or a stored procedure. + /// + /// The type of object that will be queried. + /// The sql query or stored procedure name to run. + /// A previously instantiated list to populate. + /// Returns a list of the specified type. + ICollection Query(string sql, ICollection entityList); + + #endregion + + #region - Query to Graph - + + /// + /// Runs a query and then tries to instantiate the entire object graph with entites. + /// + List QueryToGraph(string sql); + + /// + /// Runs a query and then tries to instantiate the entire object graph with entites. + /// + ICollection QueryToGraph(string sql, ICollection entityList); + + #endregion + } +} diff --git a/src/Marr.Data/LazyLoaded.cs b/src/Marr.Data/LazyLoaded.cs new file mode 100644 index 000000000..10d9c13d1 --- /dev/null +++ b/src/Marr.Data/LazyLoaded.cs @@ -0,0 +1,131 @@ +using System; + +namespace Marr.Data +{ + public interface ILazyLoaded : ICloneable + { + bool IsLoaded { get; } + void Prepare(Func dataMapperFactory, object parent); + void LazyLoad(); + } + + /// + /// Allows a field to be lazy loaded. + /// + /// + public class LazyLoaded : ILazyLoaded + { + protected TChild _value; + + public LazyLoaded() + { + } + + public LazyLoaded(TChild val) + { + _value = val; + IsLoaded = true; + } + + public TChild Value + { + get + { + LazyLoad(); + return _value; + } + } + + public bool IsLoaded { get; protected set; } + + public virtual void Prepare(Func dataMapperFactory, object parent) + { } + + public virtual void LazyLoad() + { } + + public static implicit operator LazyLoaded(TChild val) + { + return new LazyLoaded(val); + } + + public static implicit operator TChild(LazyLoaded lazy) + { + return lazy.Value; + } + + public object Clone() + { + return MemberwiseClone(); + } + } + + /// + /// This is the lazy loading proxy. + /// + /// The parent entity that contains the lazy loaded entity. + /// The child entity that is being lazy loaded. + internal class LazyLoaded : LazyLoaded + { + private TParent _parent; + private Func _dbMapperFactory; + + private readonly Func _query; + private readonly Func _condition; + + internal LazyLoaded(Func query, Func condition = null) + { + _query = query; + _condition = condition; + } + + public LazyLoaded(TChild val) + : base(val) + { + _value = val; + IsLoaded = true; + } + + /// + /// The second part of the initialization happens when the entity is being built. + /// + /// Knows how to instantiate a new IDataMapper. + /// The parent entity. + public override void Prepare(Func dataMapperFactory, object parent) + { + _dbMapperFactory = dataMapperFactory; + _parent = (TParent)parent; + } + + public override void LazyLoad() + { + if (!IsLoaded) + { + if (_condition != null && _condition(_parent)) + { + using (IDataMapper db = _dbMapperFactory()) + { + _value = _query(db, _parent); + } + } + else + { + _value = default(TChild); + } + + IsLoaded = true; + } + } + + public static implicit operator LazyLoaded(TChild val) + { + return new LazyLoaded(val); + } + + public static implicit operator TChild(LazyLoaded lazy) + { + return lazy.Value; + } + } + +} \ No newline at end of file diff --git a/src/Marr.Data/MapRepository.cs b/src/Marr.Data/MapRepository.cs new file mode 100644 index 000000000..50747a8ff --- /dev/null +++ b/src/Marr.Data/MapRepository.cs @@ -0,0 +1,250 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using System.Collections.Generic; +using Marr.Data.Converters; +using Marr.Data.Parameters; +using Marr.Data.Mapping; +using Marr.Data.Mapping.Strategies; +using Marr.Data.Reflection; + +namespace Marr.Data +{ + public class MapRepository + { + private static readonly object _tablesLock = new object(); + private static readonly object _columnsLock = new object(); + private static readonly object _relationshipsLock = new object(); + + private IDbTypeBuilder _dbTypeBuilder; + private Dictionary _columnMapStrategies; + + internal Dictionary Tables { get; set; } + internal Dictionary Columns { get; set; } + internal Dictionary Relationships { get; set; } + public Dictionary TypeConverters { get; private set; } + + // Explicit static constructor to tell C# compiler + // not to mark type as beforefieldinit + static MapRepository() + { } + + private MapRepository() + { + Tables = new Dictionary(); + Columns = new Dictionary(); + Relationships = new Dictionary(); + TypeConverters = new Dictionary(); + + // Register a default IReflectionStrategy + ReflectionStrategy = new SimpleReflectionStrategy(); + + // Register a default type converter for Enums + TypeConverters.Add(typeof(Enum), new EnumStringConverter()); + + // Register a default IDbTypeBuilder + _dbTypeBuilder = new DbTypeBuilder(); + + _columnMapStrategies = new Dictionary(); + RegisterDefaultMapStrategy(new AttributeMapStrategy()); + + EnableTraceLogging = false; + } + + private readonly static MapRepository _instance = new MapRepository(); + + /// + /// Gets a reference to the singleton MapRepository. + /// + public static MapRepository Instance + { + get + { + return _instance; + } + } + + /// + /// Gets or sets a boolean that determines whether debug information should be written to the trace log. + /// The default is false. + /// + public bool EnableTraceLogging { get; set; } + + #region - Column Map Strategies - + + public void RegisterDefaultMapStrategy(IMapStrategy strategy) + { + RegisterMapStrategy(typeof(object), strategy); + } + + public void RegisterMapStrategy(Type entityType, IMapStrategy strategy) + { + if (_columnMapStrategies.ContainsKey(entityType)) + _columnMapStrategies[entityType] = strategy; + else + _columnMapStrategies.Add(entityType, strategy); + } + + private IMapStrategy GetMapStrategy(Type entityType) + { + if (_columnMapStrategies.ContainsKey(entityType)) + { + // Return entity specific column map strategy + return _columnMapStrategies[entityType]; + } + // Return the default column map strategy + return _columnMapStrategies[typeof(object)]; + } + + #endregion + + #region - Table repository - + + internal string GetTableName(Type entityType) + { + if (!Tables.ContainsKey(entityType)) + { + lock (_tablesLock) + { + if (!Tables.ContainsKey(entityType)) + { + string tableName = GetMapStrategy(entityType).MapTable(entityType); + Tables.Add(entityType, tableName); + return tableName; + } + } + } + + return Tables[entityType]; + } + + #endregion + + #region - Columns repository - + + public ColumnMapCollection GetColumns(Type entityType) + { + if (!Columns.ContainsKey(entityType)) + { + lock (_columnsLock) + { + if (!Columns.ContainsKey(entityType)) + { + ColumnMapCollection columnMaps = GetMapStrategy(entityType).MapColumns(entityType); + Columns.Add(entityType, columnMaps); + return columnMaps; + } + } + } + + return Columns[entityType]; + } + + #endregion + + #region - Relationships repository - + + public RelationshipCollection GetRelationships(Type type) + { + if (!Relationships.ContainsKey(type)) + { + lock (_relationshipsLock) + { + if (!Relationships.ContainsKey(type)) + { + RelationshipCollection relationships = GetMapStrategy(type).MapRelationships(type); + Relationships.Add(type, relationships); + return relationships; + } + } + } + + return Relationships[type]; + } + + #endregion + + #region - Reflection Strategy - + + /// + /// Gets or sets the reflection strategy that the DataMapper will use to load entities. + /// By default the CachedReflector will be used, which provides a performance increase over the SimpleReflector. + /// However, the SimpleReflector can be used in Medium Trust enviroments. + /// + /// + public IReflectionStrategy ReflectionStrategy { get; set; } + + #endregion + + #region - Type Converters - + + /// + /// Registers a converter for a given type. + /// + /// The CLR data type that will be converted. + /// An IConverter object that will handle the data conversion. + public void RegisterTypeConverter(Type type, IConverter converter) + { + TypeConverters[type] = converter; + } + + /// + /// Checks for a type converter (if one exists). + /// 1) Checks for a converter registered for the current columns data type. + /// 2) Checks to see if a converter is registered for all enums (type of Enum) if the current column is an enum. + /// 3) Checks to see if a converter is registered for all objects (type of Object). + /// + /// The current data map. + /// Returns an IConverter object or null if one does not exist. + internal IConverter GetConverter(Type dataType) + { + if (TypeConverters.ContainsKey(dataType)) + { + // User registered type converter + return TypeConverters[dataType]; + } + if (TypeConverters.ContainsKey(typeof(Enum)) && dataType.IsEnum) + { + // A converter is registered to handled enums + return TypeConverters[typeof(Enum)]; + } + if (TypeConverters.ContainsKey(typeof(object))) + { + // User registered default converter + return TypeConverters[typeof(object)]; + } + // No conversion + return null; + } + + #endregion + + #region - DbTypeBuilder - + + /// + /// Gets or sets the IDBTypeBuilder that is responsible for converting parameter DbTypes based on the parameter value. + /// Defaults to use the DbTypeBuilder. + /// You can replace this with a more specific builder if you want more control over the way the parameter types are set. + /// + public IDbTypeBuilder DbTypeBuilder + { + get { return _dbTypeBuilder; } + set { _dbTypeBuilder = value; } + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Marr.Data/Mapping/ColumnAttribute.cs b/src/Marr.Data/Mapping/ColumnAttribute.cs new file mode 100644 index 000000000..e75ea6476 --- /dev/null +++ b/src/Marr.Data/Mapping/ColumnAttribute.cs @@ -0,0 +1,115 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using System.Data; + +namespace Marr.Data.Mapping +{ + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)] + public class ColumnAttribute : Attribute, IColumnInfo + { + private string _name; + private string _altName; + private int _size = 0; + private bool _isPrimaryKey; + private bool _isAutoIncrement; + private bool _returnValue; + private ParameterDirection _paramDirection = ParameterDirection.Input; + + public ColumnAttribute() + { + } + + public ColumnAttribute(string name) + { + _name = name; + } + + /// + /// Gets or sets the column name. + /// + public string Name + { + get { return _name; } + set { _name = value; } + } + + /// + /// Gets or sets an alternate name that is used to define this column in views. + /// If an AltName is present, it is used in the QueryViewToObjectGraph method. + /// If an AltName is not present, it will return the Name property value. + /// + public string AltName + { + get { return _altName; } + set { _altName = value; } + } + + /// + /// Gets or sets the column size. + /// + public int Size + { + get { return _size; } + set { _size = value; } + } + + /// + /// Gets or sets a value that determines whether the column is the Primary Key. + /// + public bool IsPrimaryKey + { + get { return _isPrimaryKey; } + set { _isPrimaryKey = value; } + } + + /// + /// Gets or sets a value that determines whether the column is an auto-incrementing seed column. + /// + public bool IsAutoIncrement + { + get { return _isAutoIncrement; } + set { _isAutoIncrement = value; } + } + + /// + /// Gets or sets a value that determines whether the column has a return value. + /// + public bool ReturnValue + { + get { return _returnValue; } + set { _returnValue = value; } + } + + /// + /// Gets or sets the ParameterDirection. + /// + public ParameterDirection ParamDirection + { + get { return _paramDirection; } + set { _paramDirection = value; } + } + + public string TryGetAltName() + { + if (!string.IsNullOrEmpty(AltName) && AltName != Name) + { + return AltName; + } + return Name; + } + } +} diff --git a/src/Marr.Data/Mapping/ColumnInfo.cs b/src/Marr.Data/Mapping/ColumnInfo.cs new file mode 100644 index 000000000..e8a7c8cd7 --- /dev/null +++ b/src/Marr.Data/Mapping/ColumnInfo.cs @@ -0,0 +1,32 @@ +using System.Data; + +namespace Marr.Data.Mapping +{ + public class ColumnInfo : IColumnInfo + { + public ColumnInfo() + { + IsPrimaryKey = false; + IsAutoIncrement = false; + ReturnValue = false; + ParamDirection = ParameterDirection.Input; + } + + public string Name { get; set; } + public string AltName { get; set; } + public int Size { get; set; } + public bool IsPrimaryKey { get; set; } + public bool IsAutoIncrement { get; set; } + public bool ReturnValue { get; set; } + public ParameterDirection ParamDirection { get; set; } + + public string TryGetAltName() + { + if (!string.IsNullOrEmpty(AltName) && AltName != Name) + { + return AltName; + } + return Name; + } + } +} diff --git a/src/Marr.Data/Mapping/ColumnMap.cs b/src/Marr.Data/Mapping/ColumnMap.cs new file mode 100644 index 000000000..e971daca0 --- /dev/null +++ b/src/Marr.Data/Mapping/ColumnMap.cs @@ -0,0 +1,70 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using System.Reflection; +using Marr.Data.Converters; +using Marr.Data.Reflection; + +namespace Marr.Data.Mapping +{ + /// + /// Contains information about the class fields and their associated stored proc parameters + /// + public class ColumnMap + { + + /// + /// Creates a column map with an empty ColumnInfo object. + /// + /// The .net member that is being mapped. + public ColumnMap(MemberInfo member) + : this(member, new ColumnInfo()) + { } + + public ColumnMap(MemberInfo member, IColumnInfo columnInfo) + { + FieldName = member.Name; + ColumnInfo = columnInfo; + + // If the column name is not specified, the field name will be used. + if (string.IsNullOrEmpty(columnInfo.Name)) + columnInfo.Name = member.Name; + + FieldType = ReflectionHelper.GetMemberType(member); + Type paramNetType = FieldType; + + Converter = MapRepository.Instance.GetConverter(FieldType); + if (Converter != null) + { + paramNetType = Converter.DbType; + } + + DBType = MapRepository.Instance.DbTypeBuilder.GetDbType(paramNetType); + + Getter = MapRepository.Instance.ReflectionStrategy.BuildGetter(member.DeclaringType, FieldName); + Setter = MapRepository.Instance.ReflectionStrategy.BuildSetter(member.DeclaringType, FieldName); + } + + public string FieldName { get; set; } + public Type FieldType { get; set; } + public Enum DBType { get; set; } + public IColumnInfo ColumnInfo { get; set; } + + public GetterDelegate Getter { get; private set; } + public SetterDelegate Setter { get; private set; } + public IConverter Converter { get; private set; } + } +} diff --git a/src/Marr.Data/Mapping/ColumnMapBuilder.cs b/src/Marr.Data/Mapping/ColumnMapBuilder.cs new file mode 100644 index 000000000..9a7b5531d --- /dev/null +++ b/src/Marr.Data/Mapping/ColumnMapBuilder.cs @@ -0,0 +1,235 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Data; +using Marr.Data.Mapping.Strategies; + +namespace Marr.Data.Mapping +{ + /// + /// This class has fluent methods that are used to easily configure column mappings. + /// + /// + public class ColumnMapBuilder + { + private FluentMappings.MappingsFluentEntity _fluentEntity; + private string _currentPropertyName; + + public ColumnMapBuilder(FluentMappings.MappingsFluentEntity fluentEntity, ColumnMapCollection mappedColumns) + { + _fluentEntity = fluentEntity; + MappedColumns = mappedColumns; + } + + /// + /// Gets the list of column mappings that are being configured. + /// + public ColumnMapCollection MappedColumns { get; private set; } + + #region - Fluent Methods - + + /// + /// Initializes the configurator to configure the given property. + /// + /// + /// + public ColumnMapBuilder For(Expression> property) + { + For(property.GetMemberName()); + return this; + } + + /// + /// Initializes the configurator to configure the given property or field. + /// + /// + /// + public ColumnMapBuilder For(string propertyName) + { + _currentPropertyName = propertyName; + + // Try to add the column map if it doesn't exist + if (MappedColumns.GetByFieldName(_currentPropertyName) == null) + { + TryAddColumnMapForField(_currentPropertyName); + } + + return this; + } + + public ColumnMapBuilder SetPrimaryKey() + { + AssertCurrentPropertyIsSet(); + return SetPrimaryKey(_currentPropertyName); + } + + public ColumnMapBuilder SetPrimaryKey(string propertyName) + { + MappedColumns.GetByFieldName(propertyName).ColumnInfo.IsPrimaryKey = true; + return this; + } + + public ColumnMapBuilder SetAutoIncrement() + { + AssertCurrentPropertyIsSet(); + return SetAutoIncrement(_currentPropertyName); + } + + public ColumnMapBuilder SetAutoIncrement(string propertyName) + { + MappedColumns.GetByFieldName(propertyName).ColumnInfo.IsAutoIncrement = true; + return this; + } + + public ColumnMapBuilder SetColumnName(string columnName) + { + AssertCurrentPropertyIsSet(); + return SetColumnName(_currentPropertyName, columnName); + } + + public ColumnMapBuilder SetColumnName(string propertyName, string columnName) + { + MappedColumns.GetByFieldName(propertyName).ColumnInfo.Name = columnName; + return this; + } + + public ColumnMapBuilder SetReturnValue() + { + AssertCurrentPropertyIsSet(); + return SetReturnValue(_currentPropertyName); + } + + public ColumnMapBuilder SetReturnValue(string propertyName) + { + MappedColumns.GetByFieldName(propertyName).ColumnInfo.ReturnValue = true; + return this; + } + + public ColumnMapBuilder SetSize(int size) + { + AssertCurrentPropertyIsSet(); + return SetSize(_currentPropertyName, size); + } + + public ColumnMapBuilder SetSize(string propertyName, int size) + { + MappedColumns.GetByFieldName(propertyName).ColumnInfo.Size = size; + return this; + } + + public ColumnMapBuilder SetAltName(string altName) + { + AssertCurrentPropertyIsSet(); + return SetAltName(_currentPropertyName, altName); + } + + public ColumnMapBuilder SetAltName(string propertyName, string altName) + { + MappedColumns.GetByFieldName(propertyName).ColumnInfo.AltName = altName; + return this; + } + + public ColumnMapBuilder SetParamDirection(ParameterDirection direction) + { + AssertCurrentPropertyIsSet(); + return SetParamDirection(_currentPropertyName, direction); + } + + public ColumnMapBuilder SetParamDirection(string propertyName, ParameterDirection direction) + { + MappedColumns.GetByFieldName(propertyName).ColumnInfo.ParamDirection = direction; + return this; + } + + public ColumnMapBuilder Ignore(Expression> property) + { + string propertyName = property.GetMemberName(); + return Ignore(propertyName); + } + + public ColumnMapBuilder Ignore(string propertyName) + { + var columnMap = MappedColumns.GetByFieldName(propertyName); + MappedColumns.Remove(columnMap); + return this; + } + + public ColumnMapBuilder PrefixAltNames(string prefix) + { + MappedColumns.PrefixAltNames(prefix); + return this; + } + + public ColumnMapBuilder SuffixAltNames(string suffix) + { + MappedColumns.SuffixAltNames(suffix); + return this; + } + + public FluentMappings.MappingsFluentTables Tables + { + get + { + if (_fluentEntity == null) + { + throw new Exception("This property is not compatible with the obsolete 'MapBuilder' class."); + } + + return _fluentEntity.Table; + } + } + + public FluentMappings.MappingsFluentRelationships Relationships + { + get + { + if (_fluentEntity == null) + { + throw new Exception("This property is not compatible with the obsolete 'MapBuilder' class."); + } + + return _fluentEntity.Relationships; + } + } + + public FluentMappings.MappingsFluentEntity Entity() + { + return new FluentMappings.MappingsFluentEntity(true); + } + + /// + /// Tries to add a ColumnMap for the given field name. + /// Throws and exception if field cannot be found. + /// + private void TryAddColumnMapForField(string fieldName) + { + // Set strategy to filter for public or private fields + ConventionMapStrategy strategy = new ConventionMapStrategy(false); + + // Find the field that matches the given field name + strategy.ColumnPredicate = mi => mi.Name == fieldName; + ColumnMap columnMap = strategy.MapColumns(typeof(TEntity)).FirstOrDefault(); + + if (columnMap == null) + { + throw new DataMappingException(string.Format("Could not find the field '{0}' in '{1}'.", + fieldName, + typeof(TEntity).Name)); + } + MappedColumns.Add(columnMap); + } + + /// + /// Throws an exception if the "current" property has not been set. + /// + private void AssertCurrentPropertyIsSet() + { + if (string.IsNullOrEmpty(_currentPropertyName)) + { + throw new DataMappingException("A property must first be specified using the 'For' method."); + } + } + + #endregion + } +} diff --git a/src/Marr.Data/Mapping/ColumnMapCollection.cs b/src/Marr.Data/Mapping/ColumnMapCollection.cs new file mode 100644 index 000000000..4c1b57595 --- /dev/null +++ b/src/Marr.Data/Mapping/ColumnMapCollection.cs @@ -0,0 +1,172 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System.Collections.Generic; +using System.Data; +using System.Text.RegularExpressions; +using System.Data.Common; + +namespace Marr.Data.Mapping +{ + /// + /// This class contains a list of column mappings. + /// It also provides various methods to filter the collection. + /// + public class ColumnMapCollection : List + { + #region - Filters - + + public ColumnMap GetByColumnName(string columnName) + { + return Find(m => m.ColumnInfo.Name == columnName); + } + + public ColumnMap GetByFieldName(string fieldName) + { + return Find(m => m.FieldName == fieldName); + } + + /// + /// Iterates through all fields marked as return values. + /// + public IEnumerable ReturnValues + { + get + { + foreach (ColumnMap map in this) + if (map.ColumnInfo.ReturnValue) + yield return map; + } + } + + /// + /// Iterates through all fields that are not return values. + /// + public ColumnMapCollection NonReturnValues + { + get + { + ColumnMapCollection collection = new ColumnMapCollection(); + + foreach (ColumnMap map in this) + if (!map.ColumnInfo.ReturnValue) + collection.Add(map); + + return collection; + } + } + + /// + /// Iterates through all fields marked as Output parameters or InputOutput. + /// + public IEnumerable OutputFields + { + get + { + foreach (ColumnMap map in this) + if (map.ColumnInfo.ParamDirection == ParameterDirection.InputOutput || + map.ColumnInfo.ParamDirection == ParameterDirection.Output) + yield return map; + } + } + + /// + /// Iterates through all fields marked as primary keys. + /// + public ColumnMapCollection PrimaryKeys + { + get + { + ColumnMapCollection keys = new ColumnMapCollection(); + foreach (ColumnMap map in this) + if (map.ColumnInfo.IsPrimaryKey) + keys.Add(map); + + return keys; + } + } + + /// + /// Parses and orders the parameters from the query text. + /// Filters the list of mapped columns to match the parameters found in the sql query. + /// All parameters starting with the '@' or ':' symbol are matched and returned. + /// + /// The command and parameters that are being parsed. + /// A list of mapped columns that are present in the sql statement as parameters. + public ColumnMapCollection OrderParameters(DbCommand command) + { + if (command.CommandType == CommandType.Text && Count > 0) + { + string commandTypeString = command.GetType().ToString(); + if (commandTypeString.Contains("Oracle") || commandTypeString.Contains("OleDb")) + { + ColumnMapCollection columns = new ColumnMapCollection(); + + // Find all @Parameters contained in the sql statement + string paramPrefix = commandTypeString.Contains("Oracle") ? ":" : "@"; + string regexString = string.Format(@"{0}[\w-]+", paramPrefix); + Regex regex = new Regex(regexString); + foreach (Match m in regex.Matches(command.CommandText)) + { + ColumnMap matchingColumn = Find(c => string.Concat(paramPrefix, c.ColumnInfo.Name.ToLower()) == m.Value.ToLower()); + if (matchingColumn != null) + columns.Add(matchingColumn); + } + + return columns; + } + } + + return this; + } + + + #endregion + + #region - Actions - + + /// + /// Set's each column's altname as the given prefix + the column name. + /// Ex: + /// Original column name: "ID" + /// Passed in prefix: "PRODUCT_" + /// Generated AltName: "PRODUCT_ID" + /// + /// The given prefix. + /// + public ColumnMapCollection PrefixAltNames(string prefix) + { + ForEach(c => c.ColumnInfo.AltName = c.ColumnInfo.Name.Insert(0, prefix)); + return this; + } + + /// + /// Set's each column's altname as the column name + the given prefix. + /// Ex: + /// Original column name: "ID" + /// Passed in suffix: "_PRODUCT" + /// Generated AltName: "ID_PRODUCT" + /// + /// + /// + public ColumnMapCollection SuffixAltNames(string suffix) + { + ForEach(c => c.ColumnInfo.AltName = c.ColumnInfo.Name + suffix); + return this; + } + + #endregion + } +} diff --git a/src/Marr.Data/Mapping/EnumConversionType.cs b/src/Marr.Data/Mapping/EnumConversionType.cs new file mode 100644 index 000000000..5cb0c0ea1 --- /dev/null +++ b/src/Marr.Data/Mapping/EnumConversionType.cs @@ -0,0 +1,24 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +namespace Marr.Data.Mapping +{ + public enum EnumConversionType + { + NA, + Int, + String + } +} diff --git a/src/Marr.Data/Mapping/FluentMappings.cs b/src/Marr.Data/Mapping/FluentMappings.cs new file mode 100644 index 000000000..b05680197 --- /dev/null +++ b/src/Marr.Data/Mapping/FluentMappings.cs @@ -0,0 +1,234 @@ +using System; +using System.Reflection; +using Marr.Data.Mapping.Strategies; +using System.Collections; + +namespace Marr.Data.Mapping +{ + /// + /// Provides a fluent interface for mapping domain entities and properties to database tables and columns. + /// + public class FluentMappings + { + private bool _publicOnly; + + public FluentMappings() + : this(true) + { } + + public FluentMappings(bool publicOnly) + { + _publicOnly = publicOnly; + + } + + public MappingsFluentEntity Entity() + { + return new MappingsFluentEntity(_publicOnly); + } + + public class MappingsFluentEntity + { + public MappingsFluentEntity(bool publicOnly) + { + Columns = new MappingsFluentColumns(this, publicOnly); + Table = new MappingsFluentTables(this); + Relationships = new MappingsFluentRelationships(this, publicOnly); + } + + /// + /// Contains methods that map entity properties to database table and view column names; + /// + public MappingsFluentColumns Columns { get; private set; } + + /// + /// Contains methods that map entity classes to database table names. + /// + public MappingsFluentTables Table { get; private set; } + + /// + /// Contains methods that map sub-entities with database table and view column names. + /// + public MappingsFluentRelationships Relationships { get; private set; } + } + + public class MappingsFluentColumns + { + private bool _publicOnly; + private MappingsFluentEntity _fluentEntity; + + public MappingsFluentColumns(MappingsFluentEntity fluentEntity, bool publicOnly) + { + _fluentEntity = fluentEntity; + _publicOnly = publicOnly; + } + + /// + /// Creates column mappings for the given type. + /// Maps all properties except ICollection properties. + /// + /// The type that is being built. + /// + public ColumnMapBuilder AutoMapAllProperties() + { + return AutoMapPropertiesWhere(m => m.MemberType == MemberTypes.Property && + !typeof(ICollection).IsAssignableFrom((m as PropertyInfo).PropertyType)); + } + + /// + /// Creates column mappings for the given type. + /// Maps all properties that are simple types (int, string, DateTime, etc). + /// ICollection properties are not included. + /// + /// The type that is being built. + /// + public ColumnMapBuilder AutoMapSimpleTypeProperties() + { + return AutoMapPropertiesWhere(m => m.MemberType == MemberTypes.Property && + DataHelper.IsSimpleType((m as PropertyInfo).PropertyType) && + !typeof(ICollection).IsAssignableFrom((m as PropertyInfo).PropertyType)); + } + + /// + /// Creates column mappings for the given type if they match the predicate. + /// + /// The type that is being built. + /// Determines whether a mapping should be created based on the member info. + /// + public ColumnMapBuilder AutoMapPropertiesWhere(Func predicate) + { + Type entityType = typeof(TEntity); + ConventionMapStrategy strategy = new ConventionMapStrategy(_publicOnly); + strategy.ColumnPredicate = predicate; + ColumnMapCollection columns = strategy.MapColumns(entityType); + MapRepository.Instance.Columns[entityType] = columns; + return new ColumnMapBuilder(_fluentEntity, columns); + } + + /// + /// Creates a ColumnMapBuilder that starts out with no pre-populated columns. + /// All columns must be added manually using the builder. + /// + /// + /// + public ColumnMapBuilder MapProperties() + { + Type entityType = typeof(TEntity); + ColumnMapCollection columns = new ColumnMapCollection(); + MapRepository.Instance.Columns[entityType] = columns; + return new ColumnMapBuilder(_fluentEntity, columns); + } + } + + public class MappingsFluentTables + { + private MappingsFluentEntity _fluentEntity; + + public MappingsFluentTables(MappingsFluentEntity fluentEntity) + { + _fluentEntity = fluentEntity; + } + + /// + /// Provides a fluent table mapping interface. + /// + /// + /// + public TableBuilder AutoMapTable() + { + return new TableBuilder(_fluentEntity); + } + + /// + /// Sets the table name for a given type. + /// + /// + /// + public TableBuilder MapTable(string tableName) + { + return new TableBuilder(_fluentEntity).SetTableName(tableName); + } + } + + public class MappingsFluentRelationships + { + private MappingsFluentEntity _fluentEntity; + private bool _publicOnly; + + public MappingsFluentRelationships(MappingsFluentEntity fluentEntity, bool publicOnly) + { + _fluentEntity = fluentEntity; + _publicOnly = publicOnly; + } + + /// + /// Creates relationship mappings for the given type. + /// Maps all properties that implement ICollection or are not "simple types". + /// + /// + public RelationshipBuilder AutoMapICollectionOrComplexProperties() + { + return AutoMapPropertiesWhere(m => + m.MemberType == MemberTypes.Property && + ( + typeof(ICollection).IsAssignableFrom((m as PropertyInfo).PropertyType) || !DataHelper.IsSimpleType((m as PropertyInfo).PropertyType) + ) + ); + + } + + /// + /// Creates relationship mappings for the given type. + /// Maps all properties that implement ICollection. + /// + /// + public RelationshipBuilder AutoMapICollectionProperties() + { + return AutoMapPropertiesWhere(m => + m.MemberType == MemberTypes.Property && + typeof(ICollection).IsAssignableFrom((m as PropertyInfo).PropertyType)); + } + + /// + /// Creates relationship mappings for the given type. + /// Maps all properties that are not "simple types". + /// + /// + public RelationshipBuilder AutoMapComplexTypeProperties() + { + return AutoMapPropertiesWhere(m => + m.MemberType == MemberTypes.Property && + !DataHelper.IsSimpleType((m as PropertyInfo).PropertyType) && + !MapRepository.Instance.TypeConverters.ContainsKey((m as PropertyInfo).PropertyType)); + } + + /// + /// Creates relationship mappings for the given type if they match the predicate. + /// + /// Determines whether a mapping should be created based on the member info. + /// + public RelationshipBuilder AutoMapPropertiesWhere(Func predicate) + { + Type entityType = typeof(TEntity); + ConventionMapStrategy strategy = new ConventionMapStrategy(_publicOnly); + strategy.RelationshipPredicate = predicate; + RelationshipCollection relationships = strategy.MapRelationships(entityType); + MapRepository.Instance.Relationships[entityType] = relationships; + return new RelationshipBuilder(_fluentEntity, relationships); + } + + /// + /// Creates a RelationshipBuilder that starts out with no pre-populated relationships. + /// All relationships must be added manually using the builder. + /// + /// + public RelationshipBuilder MapProperties() + { + Type entityType = typeof(T); + RelationshipCollection relationships = new RelationshipCollection(); + MapRepository.Instance.Relationships[entityType] = relationships; + return new RelationshipBuilder(_fluentEntity, relationships); + } + } + } +} diff --git a/src/Marr.Data/Mapping/IColumnInfo.cs b/src/Marr.Data/Mapping/IColumnInfo.cs new file mode 100644 index 000000000..6cf28833b --- /dev/null +++ b/src/Marr.Data/Mapping/IColumnInfo.cs @@ -0,0 +1,32 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System.Data; + +namespace Marr.Data.Mapping +{ + public interface IColumnInfo + { + string Name { get; set; } + string AltName { get; set; } + int Size { get; set; } + bool IsPrimaryKey { get; set; } + bool IsAutoIncrement { get; set; } + bool ReturnValue { get; set; } + ParameterDirection ParamDirection { get; set; } + string TryGetAltName(); + } + +} diff --git a/src/Marr.Data/Mapping/IRelationshipInfo.cs b/src/Marr.Data/Mapping/IRelationshipInfo.cs new file mode 100644 index 000000000..004ed7837 --- /dev/null +++ b/src/Marr.Data/Mapping/IRelationshipInfo.cs @@ -0,0 +1,32 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; + +namespace Marr.Data.Mapping +{ + public interface IRelationshipInfo + { + RelationshipTypes RelationType { get; set; } + Type EntityType { get; set; } + } + + public enum RelationshipTypes + { + AutoDetect, + One, + Many + } +} diff --git a/src/Marr.Data/Mapping/MapBuilder.cs b/src/Marr.Data/Mapping/MapBuilder.cs new file mode 100644 index 000000000..bb3a0023f --- /dev/null +++ b/src/Marr.Data/Mapping/MapBuilder.cs @@ -0,0 +1,206 @@ +using System; +using System.Linq; +using Marr.Data.Mapping.Strategies; +using System.Reflection; +using System.Collections; + +namespace Marr.Data.Mapping +{ + [Obsolete("This class is obsolete. Please use the 'Mappings' class.")] + public class MapBuilder + { + private bool _publicOnly; + + public MapBuilder() + : this(true) + { } + + public MapBuilder(bool publicOnly) + { + _publicOnly = publicOnly; + } + + #region - Columns - + + /// + /// Creates column mappings for the given type. + /// Maps all properties except ICollection properties. + /// + /// The type that is being built. + /// + public ColumnMapBuilder BuildColumns() + { + return BuildColumns(m => m.MemberType == MemberTypes.Property && + !typeof(ICollection).IsAssignableFrom((m as PropertyInfo).PropertyType)); + } + + /// + /// Creates column mappings for the given type. + /// Maps all properties that are simple types (int, string, DateTime, etc). + /// ICollection properties are not included. + /// + /// The type that is being built. + /// + public ColumnMapBuilder BuildColumnsFromSimpleTypes() + { + return BuildColumns(m => m.MemberType == MemberTypes.Property && + DataHelper.IsSimpleType((m as PropertyInfo).PropertyType) && + !typeof(ICollection).IsAssignableFrom((m as PropertyInfo).PropertyType)); + } + + /// + /// Creates column mappings for the given type. + /// Maps properties that are included in the include list. + /// + /// The type that is being built. + /// + /// + public ColumnMapBuilder BuildColumns(params string[] propertiesToInclude) + { + return BuildColumns(m => + m.MemberType == MemberTypes.Property && + propertiesToInclude.Contains(m.Name)); + } + + /// + /// Creates column mappings for the given type. + /// Maps all properties except the ones in the exclusion list. + /// + /// The type that is being built. + /// + /// + public ColumnMapBuilder BuildColumnsExcept(params string[] propertiesToExclude) + { + return BuildColumns(m => + m.MemberType == MemberTypes.Property && + !propertiesToExclude.Contains(m.Name)); + } + + /// + /// Creates column mappings for the given type if they match the predicate. + /// + /// The type that is being built. + /// Determines whether a mapping should be created based on the member info. + /// + public ColumnMapBuilder BuildColumns(Func predicate) + { + Type entityType = typeof(T); + ConventionMapStrategy strategy = new ConventionMapStrategy(_publicOnly); + strategy.ColumnPredicate = predicate; + ColumnMapCollection columns = strategy.MapColumns(entityType); + MapRepository.Instance.Columns[entityType] = columns; + return new ColumnMapBuilder(null, columns); + } + + /// + /// Creates a ColumnMapBuilder that starts out with no pre-populated columns. + /// All columns must be added manually using the builder. + /// + /// + /// + public ColumnMapBuilder Columns() + { + Type entityType = typeof(T); + ColumnMapCollection columns = new ColumnMapCollection(); + MapRepository.Instance.Columns[entityType] = columns; + return new ColumnMapBuilder(null, columns); + } + + #endregion + + #region - Relationships - + + /// + /// Creates relationship mappings for the given type. + /// Maps all properties that implement ICollection. + /// + /// The type that is being built. + /// + public RelationshipBuilder BuildRelationships() + { + return BuildRelationships(m => + m.MemberType == MemberTypes.Property && + typeof(ICollection).IsAssignableFrom((m as PropertyInfo).PropertyType)); + } + + /// + /// Creates relationship mappings for the given type. + /// Maps all properties that are listed in the include list. + /// + /// The type that is being built. + /// + /// + public RelationshipBuilder BuildRelationships(params string[] propertiesToInclude) + { + Func predicate = m => + ( + // ICollection properties + m.MemberType == MemberTypes.Property && + typeof(ICollection).IsAssignableFrom((m as PropertyInfo).PropertyType) && + propertiesToInclude.Contains(m.Name) + ) || ( // Single entity properties + m.MemberType == MemberTypes.Property && + !typeof(ICollection).IsAssignableFrom((m as PropertyInfo).PropertyType) && + propertiesToInclude.Contains(m.Name) + ); + + return BuildRelationships(predicate); + } + + /// + /// Creates relationship mappings for the given type if they match the predicate. + /// + /// The type that is being built. + /// Determines whether a mapping should be created based on the member info. + /// + public RelationshipBuilder BuildRelationships(Func predicate) + { + Type entityType = typeof(T); + ConventionMapStrategy strategy = new ConventionMapStrategy(_publicOnly); + strategy.RelationshipPredicate = predicate; + RelationshipCollection relationships = strategy.MapRelationships(entityType); + MapRepository.Instance.Relationships[entityType] = relationships; + return new RelationshipBuilder(null, relationships); + } + + /// + /// Creates a RelationshipBuilder that starts out with no pre-populated relationships. + /// All relationships must be added manually using the builder. + /// + /// + /// + public RelationshipBuilder Relationships() + { + Type entityType = typeof(T); + RelationshipCollection relationships = new RelationshipCollection(); + MapRepository.Instance.Relationships[entityType] = relationships; + return new RelationshipBuilder(null, relationships); + } + + #endregion + + #region - Tables - + + /// + /// Provides a fluent table mapping interface. + /// + /// + /// + public TableBuilder BuildTable() + { + return new TableBuilder(null); + } + + /// + /// Sets the table name for a given type. + /// + /// + /// + public TableBuilder BuildTable(string tableName) + { + return new TableBuilder(null).SetTableName(tableName); + } + + #endregion + } +} diff --git a/src/Marr.Data/Mapping/MappingHelper.cs b/src/Marr.Data/Mapping/MappingHelper.cs new file mode 100644 index 000000000..80e2acb47 --- /dev/null +++ b/src/Marr.Data/Mapping/MappingHelper.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Data.Common; +using Marr.Data.Converters; + +namespace Marr.Data.Mapping +{ + internal class MappingHelper + { + private MapRepository _repos; + private IDataMapper _db; + + public MappingHelper(IDataMapper db) + { + _repos = MapRepository.Instance; + _db = db; + } + + /// + /// Instantiates an entity and loads its mapped fields with the data from the reader. + /// + public object CreateAndLoadEntity(ColumnMapCollection mappings, DbDataReader reader, bool useAltName) + { + return CreateAndLoadEntity(typeof(T), mappings, reader, useAltName); + } + + /// + /// Instantiates an entity and loads its mapped fields with the data from the reader. + /// + /// The entity being created and loaded. + /// The field mappings for the passed in entity. + /// The open data reader. + /// Determines if the column AltName should be used. + /// Returns an entity loaded with data. + public object CreateAndLoadEntity(Type entityType, ColumnMapCollection mappings, DbDataReader reader, bool useAltName) + { + // Create new entity + object ent = _repos.ReflectionStrategy.CreateInstance(entityType); + return LoadExistingEntity(mappings, reader, ent, useAltName); + } + + public object LoadExistingEntity(ColumnMapCollection mappings, DbDataReader reader, object ent, bool useAltName) + { + // Populate entity fields from data reader + foreach (ColumnMap dataMap in mappings) + { + object dbValue = null; + try + { + string colName = dataMap.ColumnInfo.GetColumName(useAltName); + int ordinal = reader.GetOrdinal(colName); + dbValue = reader.GetValue(ordinal); + + // Handle conversions + if (dataMap.Converter != null) + { + var convertContext = new ConverterContext + { + DbValue = dbValue, + ColumnMap = dataMap, + MapCollection = mappings, + DataRecord = reader + }; + + dbValue = dataMap.Converter.FromDB(convertContext); + } + + if (dbValue != DBNull.Value && dbValue != null) + { + dataMap.Setter(ent, dbValue); + } + } + catch (Exception ex) + { + string msg = string.Format("The DataMapper was unable to load the following field: '{0}' value: '{1}'. {2}", + dataMap.ColumnInfo.Name, dbValue, ex.Message); + + throw new DataMappingException(msg, ex); + } + } + + PrepareLazyLoadedProperties(ent); + + return ent; + } + + private void PrepareLazyLoadedProperties(object ent) + { + // Handle lazy loaded properties + Type entType = ent.GetType(); + if (_repos.Relationships.ContainsKey(entType)) + { + var provider = _db.ProviderFactory; + var connectionString = _db.ConnectionString; + Func dbCreate = () => + { + var db = new DataMapper(provider, connectionString); + db.SqlMode = SqlModes.Text; + return db; + }; + + var relationships = _repos.Relationships[entType]; + foreach (var rel in relationships.Where(r => r.IsLazyLoaded)) + { + var lazyLoaded = (ILazyLoaded)rel.LazyLoaded.Clone(); + lazyLoaded.Prepare(dbCreate, ent); + rel.Setter(ent, lazyLoaded); + } + } + } + + public T LoadSimpleValueFromFirstColumn(DbDataReader reader) + { + try + { + return (T)reader.GetValue(0); + } + catch (Exception ex) + { + string firstColumnName = reader.GetName(0); + string msg = string.Format("The DataMapper was unable to create a value of type '{0}' from the first column '{1}'.", + typeof(T).Name, firstColumnName); + + throw new DataMappingException(msg, ex); + } + } + + /// + /// Creates all parameters for a SP based on the mappings of the entity, + /// and assigns them values based on the field values of the entity. + /// + public void CreateParameters(T entity, ColumnMapCollection columnMapCollection, bool isAutoQuery) + { + ColumnMapCollection mappings = columnMapCollection; + + if (!isAutoQuery) + { + // Order columns (applies to Oracle and OleDb only) + mappings = columnMapCollection.OrderParameters(_db.Command); + } + + foreach (ColumnMap columnMap in mappings) + { + if (columnMap.ColumnInfo.IsAutoIncrement) + continue; + + var param = _db.Command.CreateParameter(); + param.ParameterName = columnMap.ColumnInfo.Name; + param.Size = columnMap.ColumnInfo.Size; + param.Direction = columnMap.ColumnInfo.ParamDirection; + + object val = columnMap.Getter(entity); + + param.Value = val ?? DBNull.Value; // Convert nulls to DBNulls + + if (columnMap.Converter != null) + { + param.Value = columnMap.Converter.ToDB(param.Value); + } + + // Set the appropriate DbType property depending on the parameter type + // Note: the columnMap.DBType property was set when the ColumnMap was created + MapRepository.Instance.DbTypeBuilder.SetDbType(param, columnMap.DBType); + + _db.Command.Parameters.Add(param); + } + } + + /// + /// Assigns the SP result columns to the passed in 'mappings' fields. + /// + public void SetOutputValues(T entity, IEnumerable mappings) + { + foreach (ColumnMap dataMap in mappings) + { + object output = _db.Command.Parameters[dataMap.ColumnInfo.Name].Value; + dataMap.Setter(entity, output); + } + } + + /// + /// Assigns the passed in 'value' to the passed in 'mappings' fields. + /// + public void SetOutputValues(T entity, IEnumerable mappings, object value) + { + foreach (ColumnMap dataMap in mappings) + { + dataMap.Setter(entity, Convert.ChangeType(value, dataMap.FieldType)); + } + } + + } +} diff --git a/src/Marr.Data/Mapping/Relationship.cs b/src/Marr.Data/Mapping/Relationship.cs new file mode 100644 index 000000000..e7794c633 --- /dev/null +++ b/src/Marr.Data/Mapping/Relationship.cs @@ -0,0 +1,98 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using System.Collections; +using System.Reflection; +using Marr.Data.Reflection; + +namespace Marr.Data.Mapping +{ + public class Relationship + { + + public Relationship(MemberInfo member) + : this(member, new RelationshipInfo()) + { } + + public Relationship(MemberInfo member, IRelationshipInfo relationshipInfo) + { + Member = member; + + MemberType = ReflectionHelper.GetMemberType(member); + + // Try to determine the RelationshipType + if (relationshipInfo.RelationType == RelationshipTypes.AutoDetect) + { + if (typeof(ICollection).IsAssignableFrom(MemberType)) + { + relationshipInfo.RelationType = RelationshipTypes.Many; + } + else + { + relationshipInfo.RelationType = RelationshipTypes.One; + } + } + + // Try to determine the EntityType + if (relationshipInfo.EntityType == null) + { + if (relationshipInfo.RelationType == RelationshipTypes.Many) + { + if (MemberType.IsGenericType) + { + // Assume a Collection or List and return T + relationshipInfo.EntityType = MemberType.GetGenericArguments()[0]; + } + else + { + throw new ArgumentException(string.Format( + "The DataMapper could not determine the RelationshipAttribute EntityType for {0}.", + MemberType.Name)); + } + } + else + { + relationshipInfo.EntityType = MemberType; + } + } + + RelationshipInfo = relationshipInfo; + + + + Setter = MapRepository.Instance.ReflectionStrategy.BuildSetter(member.DeclaringType, member.Name); + } + + public IRelationshipInfo RelationshipInfo { get; private set; } + + public MemberInfo Member { get; private set; } + + public Type MemberType { get; private set; } + + public bool IsLazyLoaded + { + get + { + return LazyLoaded != null; + } + } + + public ILazyLoaded LazyLoaded { get; set; } + + public GetterDelegate Getter { get; set; } + public SetterDelegate Setter { get; set; } + } +} diff --git a/src/Marr.Data/Mapping/RelationshipAttribute.cs b/src/Marr.Data/Mapping/RelationshipAttribute.cs new file mode 100644 index 000000000..aed53e92e --- /dev/null +++ b/src/Marr.Data/Mapping/RelationshipAttribute.cs @@ -0,0 +1,75 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; + +namespace Marr.Data.Mapping +{ + /// + /// Defines a field as a related entity that needs to be created at filled with data. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)] + public class RelationshipAttribute : Attribute, IRelationshipInfo + { + /// + /// Defines a data relationship. + /// + public RelationshipAttribute() + : this(RelationshipTypes.AutoDetect) + { } + + /// + /// Defines a data relationship. + /// + /// + public RelationshipAttribute(RelationshipTypes relationType) + { + RelationType = relationType; + } + + /// + /// Defines a One-ToMany data relationship for a given type. + /// + /// The type of the child entity. + public RelationshipAttribute(Type entityType) + : this(entityType, RelationshipTypes.AutoDetect) + { } + + /// + /// Defines a data relationship. + /// + /// The type of the child entity. + /// The relationship type can be "One" or "Many". + public RelationshipAttribute(Type entityType, RelationshipTypes relationType) + { + EntityType = entityType; + RelationType = relationType; + } + + #region IRelationshipInfo Members + + /// + /// Gets or sets the relationship type can be "One" or "Many". + /// + public RelationshipTypes RelationType { get; set; } + + /// + /// Gets or sets the type of the child entity. + /// + public Type EntityType { get; set; } + + #endregion + } +} diff --git a/src/Marr.Data/Mapping/RelationshipBuilder.cs b/src/Marr.Data/Mapping/RelationshipBuilder.cs new file mode 100644 index 000000000..b4926633f --- /dev/null +++ b/src/Marr.Data/Mapping/RelationshipBuilder.cs @@ -0,0 +1,172 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using Marr.Data.Mapping.Strategies; + +namespace Marr.Data.Mapping +{ + /// + /// This class has fluent methods that are used to easily configure relationship mappings. + /// + /// + public class RelationshipBuilder + { + private FluentMappings.MappingsFluentEntity _fluentEntity; + private string _currentPropertyName; + + public RelationshipBuilder(FluentMappings.MappingsFluentEntity fluentEntity, RelationshipCollection relationships) + { + _fluentEntity = fluentEntity; + Relationships = relationships; + } + + /// + /// Gets the list of relationship mappings that are being configured. + /// + public RelationshipCollection Relationships { get; private set; } + + #region - Fluent Methods - + + /// + /// Initializes the configurator to configure the given property. + /// + /// + /// + public RelationshipBuilder For(Expression> property) + { + return For(property.GetMemberName()); + } + + /// + /// Initializes the configurator to configure the given property or field. + /// + /// + /// + public RelationshipBuilder For(string propertyName) + { + _currentPropertyName = propertyName; + + // Try to add the relationship if it doesn't exist + if (Relationships[_currentPropertyName] == null) + { + TryAddRelationshipForField(_currentPropertyName); + } + + return this; + } + + /// + /// Sets a property to be lazy loaded, with a given query. + /// + /// + /// + /// condition in which a child could exist. eg. avoid call to db if foreign key is 0 or null + /// + public RelationshipBuilder LazyLoad(Func query, Func condition = null) + { + AssertCurrentPropertyIsSet(); + + Relationships[_currentPropertyName].LazyLoaded = new LazyLoaded(query, condition); + return this; + } + + public RelationshipBuilder SetOneToOne() + { + AssertCurrentPropertyIsSet(); + SetOneToOne(_currentPropertyName); + return this; + } + + public RelationshipBuilder SetOneToOne(string propertyName) + { + Relationships[propertyName].RelationshipInfo.RelationType = RelationshipTypes.One; + return this; + } + + public RelationshipBuilder SetOneToMany() + { + AssertCurrentPropertyIsSet(); + SetOneToMany(_currentPropertyName); + return this; + } + + public RelationshipBuilder SetOneToMany(string propertyName) + { + Relationships[propertyName].RelationshipInfo.RelationType = RelationshipTypes.Many; + return this; + } + + public RelationshipBuilder Ignore(Expression> property) + { + string propertyName = property.GetMemberName(); + Relationships.RemoveAll(r => r.Member.Name == propertyName); + return this; + } + + public FluentMappings.MappingsFluentTables Tables + { + get + { + if (_fluentEntity == null) + { + throw new Exception("This property is not compatible with the obsolete 'MapBuilder' class."); + } + + return _fluentEntity.Table; + } + } + + public FluentMappings.MappingsFluentColumns Columns + { + get + { + if (_fluentEntity == null) + { + throw new Exception("This property is not compatible with the obsolete 'MapBuilder' class."); + } + + return _fluentEntity.Columns; + } + } + + public FluentMappings.MappingsFluentEntity Entity() + { + return new FluentMappings.MappingsFluentEntity(true); + } + + /// + /// Tries to add a Relationship for the given field name. + /// Throws and exception if field cannot be found. + /// + private void TryAddRelationshipForField(string fieldName) + { + // Set strategy to filter for public or private fields + ConventionMapStrategy strategy = new ConventionMapStrategy(false); + + // Find the field that matches the given field name + strategy.RelationshipPredicate = mi => mi.Name == fieldName; + Relationship relationship = strategy.MapRelationships(typeof(TEntity)).FirstOrDefault(); + + if (relationship == null) + { + throw new DataMappingException(string.Format("Could not find the field '{0}' in '{1}'.", + fieldName, + typeof(TEntity).Name)); + } + Relationships.Add(relationship); + } + + /// + /// Throws an exception if the "current" property has not been set. + /// + private void AssertCurrentPropertyIsSet() + { + if (string.IsNullOrEmpty(_currentPropertyName)) + { + throw new DataMappingException("A property must first be specified using the 'For' method."); + } + } + + #endregion + } +} diff --git a/src/Marr.Data/Mapping/RelationshipCollection.cs b/src/Marr.Data/Mapping/RelationshipCollection.cs new file mode 100644 index 000000000..8d91c0252 --- /dev/null +++ b/src/Marr.Data/Mapping/RelationshipCollection.cs @@ -0,0 +1,35 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System.Collections.Generic; + +namespace Marr.Data.Mapping +{ + public class RelationshipCollection : List + { + /// + /// Gets a ColumnMap by its field name. + /// + /// + /// + public Relationship this[string fieldName] + { + get + { + return Find(m => m.Member.Name == fieldName); + } + } + } +} diff --git a/src/Marr.Data/Mapping/RelationshipInfo.cs b/src/Marr.Data/Mapping/RelationshipInfo.cs new file mode 100644 index 000000000..401ad7183 --- /dev/null +++ b/src/Marr.Data/Mapping/RelationshipInfo.cs @@ -0,0 +1,11 @@ +using System; + +namespace Marr.Data.Mapping +{ + public class RelationshipInfo : IRelationshipInfo + { + public RelationshipTypes RelationType { get; set; } + + public Type EntityType { get; set; } + } +} diff --git a/src/Marr.Data/Mapping/Strategies/AttributeMapStrategy.cs b/src/Marr.Data/Mapping/Strategies/AttributeMapStrategy.cs new file mode 100644 index 000000000..e0308c6b2 --- /dev/null +++ b/src/Marr.Data/Mapping/Strategies/AttributeMapStrategy.cs @@ -0,0 +1,70 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using System.Reflection; + +namespace Marr.Data.Mapping.Strategies +{ + /// + /// Maps fields or properties that are marked with the ColumnAttribute. + /// + public class AttributeMapStrategy : ReflectionMapStrategyBase + { + public AttributeMapStrategy() + : base() + { } + + public AttributeMapStrategy(bool publicOnly) + : base(publicOnly) + { } + + public AttributeMapStrategy(BindingFlags flags) + : base(flags) + { } + + /// + /// Registers any member with a ColumnAttribute as a ColumnMap. + /// The entity that is being mapped. + /// The current member that is being inspected. + /// A ColumnAttribute (is null of one does not exist). + /// A list of ColumnMaps. + /// + protected override void CreateColumnMap(Type entityType, MemberInfo member, ColumnAttribute columnAtt, ColumnMapCollection columnMaps) + { + if (columnAtt != null) + { + ColumnMap columnMap = new ColumnMap(member, columnAtt); + columnMaps.Add(columnMap); + } + } + + /// + /// Registers any member with a RelationshipAttribute as a relationship. + /// + /// The entity that is being mapped. + /// The current member that is being inspected. + /// A RelationshipAttribute (is null if one does not exist). + /// A list of Relationships. + protected override void CreateRelationship(Type entityType, MemberInfo member, RelationshipAttribute relationshipAtt, RelationshipCollection relationships) + { + if (relationshipAtt != null) + { + Relationship relationship = new Relationship(member, relationshipAtt); + relationships.Add(relationship); + } + } + } +} diff --git a/src/Marr.Data/Mapping/Strategies/ConventionMapStrategy.cs b/src/Marr.Data/Mapping/Strategies/ConventionMapStrategy.cs new file mode 100644 index 000000000..6230d7fc8 --- /dev/null +++ b/src/Marr.Data/Mapping/Strategies/ConventionMapStrategy.cs @@ -0,0 +1,48 @@ +using System; +using System.Reflection; +using System.Collections; + +namespace Marr.Data.Mapping.Strategies +{ + /// + /// Allows you to specify a member based filter by defining predicates that filter the members that are mapped. + /// + public class ConventionMapStrategy : ReflectionMapStrategyBase + { + public ConventionMapStrategy(bool publicOnly) + : base(publicOnly) + { + // Default: Only map members that are properties + ColumnPredicate = m => m.MemberType == MemberTypes.Property; + + // Default: Only map members that are properties and that are ICollection types + RelationshipPredicate = m => + { + return m.MemberType == MemberTypes.Property && typeof(ICollection).IsAssignableFrom((m as PropertyInfo).PropertyType); + }; + } + + public Func ColumnPredicate; + public Func RelationshipPredicate; + + + + protected override void CreateColumnMap(Type entityType, MemberInfo member, ColumnAttribute columnAtt, ColumnMapCollection columnMaps) + { + if (ColumnPredicate(member)) + { + // Map public property to DB column + columnMaps.Add(new ColumnMap(member)); + } + } + + protected override void CreateRelationship(Type entityType, MemberInfo member, RelationshipAttribute relationshipAtt, RelationshipCollection relationships) + { + if (RelationshipPredicate(member)) + { + relationships.Add(new Relationship(member)); + } + } + + } +} diff --git a/src/Marr.Data/Mapping/Strategies/IMapStrategy.cs b/src/Marr.Data/Mapping/Strategies/IMapStrategy.cs new file mode 100644 index 000000000..460b19055 --- /dev/null +++ b/src/Marr.Data/Mapping/Strategies/IMapStrategy.cs @@ -0,0 +1,45 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; + +namespace Marr.Data.Mapping.Strategies +{ + /// + /// A strategy for creating mappings for a given entity. + /// + public interface IMapStrategy + { + /// + /// Creates a table map for a given entity type. + /// + /// + /// + string MapTable(Type entityType); + + /// + /// Creates a ColumnMapCollection for a given entity type. + /// + /// The entity that is being mapped. + ColumnMapCollection MapColumns(Type entityType); + + /// + /// Creates a RelationshpCollection for a given entity type. + /// + /// The entity that is being mapped. + /// + RelationshipCollection MapRelationships(Type entityType); + } +} diff --git a/src/Marr.Data/Mapping/Strategies/PropertyMapStrategy.cs b/src/Marr.Data/Mapping/Strategies/PropertyMapStrategy.cs new file mode 100644 index 000000000..f9c41afd2 --- /dev/null +++ b/src/Marr.Data/Mapping/Strategies/PropertyMapStrategy.cs @@ -0,0 +1,83 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using System.Collections; +using System.Reflection; + +namespace Marr.Data.Mapping.Strategies +{ + /// + /// Maps all public properties to DB columns. + /// + public class PropertyMapStrategy : AttributeMapStrategy + { + public PropertyMapStrategy(bool publicOnly) + : base(publicOnly) + { } + + /// + /// Maps properties to DB columns if a ColumnAttribute is not present. + /// The entity that is being mapped. + /// The current member that is being inspected. + /// A ColumnAttribute (is null of one does not exist). + /// A list of ColumnMaps. + /// + protected override void CreateColumnMap(Type entityType, MemberInfo member, ColumnAttribute columnAtt, ColumnMapCollection columnMaps) + { + if (columnAtt != null) + { + // Add columns with ColumnAttribute + base.CreateColumnMap(entityType, member, columnAtt, columnMaps); + } + else + { + if (member.MemberType == MemberTypes.Property) + { + // Map public property to DB column + columnMaps.Add(new ColumnMap(member)); + } + } + } + + /// + /// Maps a relationship if a RelationshipAttribute is present. + /// + /// The entity that is being mapped. + /// The current member that is being inspected. + /// A RelationshipAttribute (is null if one does not exist). + /// A list of Relationships. + protected override void CreateRelationship(Type entityType, MemberInfo member, RelationshipAttribute relationshipAtt, RelationshipCollection relationships) + { + if (relationshipAtt != null) + { + // Add relationships by RelationshipAttribute + base.CreateRelationship(entityType, member, relationshipAtt, relationships); + } + else + { + if (member.MemberType == MemberTypes.Property) + { + PropertyInfo propertyInfo = member as PropertyInfo; + if (typeof(ICollection).IsAssignableFrom(propertyInfo.PropertyType)) + { + Relationship relationship = new Relationship(member); + relationships.Add(relationship); + } + } + } + } + } +} diff --git a/src/Marr.Data/Mapping/Strategies/ReflectionMapStrategyBase.cs b/src/Marr.Data/Mapping/Strategies/ReflectionMapStrategyBase.cs new file mode 100644 index 000000000..429cfec87 --- /dev/null +++ b/src/Marr.Data/Mapping/Strategies/ReflectionMapStrategyBase.cs @@ -0,0 +1,145 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using System.Reflection; + +namespace Marr.Data.Mapping.Strategies +{ + /// + /// Iterates through the members of an entity based on the BindingFlags, and provides an abstract method for adding ColumnMaps for each member. + /// + public abstract class ReflectionMapStrategyBase : IMapStrategy + { + private BindingFlags _bindingFlags; + + /// + /// Loops through members with the following BindingFlags: + /// Instance | NonPublic | Public | FlattenHierarchy + /// + public ReflectionMapStrategyBase() + { + _bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy; + } + + /// + /// Loops through members with the following BindingFlags: + /// Instance | Public | FlattenHierarchy | NonPublic (optional) + /// + /// + public ReflectionMapStrategyBase(bool publicOnly) + { + if (publicOnly) + { + _bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy; + } + else + { + _bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy; + } + } + + /// + /// Loops through members based on the passed in BindingFlags. + /// + /// + public ReflectionMapStrategyBase(BindingFlags bindingFlags) + { + _bindingFlags = bindingFlags; + } + + public string MapTable(Type entityType) + { + object[] atts = entityType.GetCustomAttributes(typeof(TableAttribute), true); + if (atts.Length > 0) + { + return (atts[0] as TableAttribute).Name; + } + return entityType.Name; + } + + /// + /// Implements IMapStrategy. + /// Loops through filtered members and calls the virtual "CreateColumnMap" void for each member. + /// Subclasses can override CreateColumnMap to customize adding ColumnMaps. + /// + /// + /// + public ColumnMapCollection MapColumns(Type entityType) + { + ColumnMapCollection columnMaps = new ColumnMapCollection(); + + MemberInfo[] members = entityType.GetMembers(_bindingFlags); + foreach (var member in members) + { + ColumnAttribute columnAtt = GetColumnAttribute(member); + CreateColumnMap(entityType, member, columnAtt, columnMaps); + } + + return columnMaps; + } + + public RelationshipCollection MapRelationships(Type entityType) + { + RelationshipCollection relationships = new RelationshipCollection(); + + MemberInfo[] members = entityType.GetMembers(_bindingFlags); + foreach (MemberInfo member in members) + { + RelationshipAttribute relationshipAtt = GetRelationshipAttribute(member); + CreateRelationship(entityType, member, relationshipAtt, relationships); + } + + return relationships; + } + + protected ColumnAttribute GetColumnAttribute(MemberInfo member) + { + if (member.IsDefined(typeof(ColumnAttribute), false)) + { + return (ColumnAttribute)member.GetCustomAttributes(typeof(ColumnAttribute), false)[0]; + } + + return null; + } + + protected RelationshipAttribute GetRelationshipAttribute(MemberInfo member) + { + if (member.IsDefined(typeof(RelationshipAttribute), false)) + { + return (RelationshipAttribute)member.GetCustomAttributes(typeof(RelationshipAttribute), false)[0]; + } + + return null; + } + + /// + /// Inspect a member and optionally add a ColumnMap. + /// + /// The entity type that is being mapped. + /// The member that is being mapped. + /// The ColumnMapCollection that is being created. + protected abstract void CreateColumnMap(Type entityType, MemberInfo member, ColumnAttribute columnAtt, ColumnMapCollection columnMaps); + + /// + /// Inspect a member and optionally add a Relationship. + /// + /// The entity that is being mapped. + /// The current member that is being inspected. + /// A RelationshipAttribute (is null if one does not exist). + /// A list of Relationships. + protected abstract void CreateRelationship(Type entityType, MemberInfo member, RelationshipAttribute relationshipAtt, RelationshipCollection relationships); + } +} diff --git a/src/Marr.Data/Mapping/TableAttribute.cs b/src/Marr.Data/Mapping/TableAttribute.cs new file mode 100644 index 000000000..595894f82 --- /dev/null +++ b/src/Marr.Data/Mapping/TableAttribute.cs @@ -0,0 +1,40 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; + +namespace Marr.Data.Mapping +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] + public class TableAttribute : Attribute + { + private string _name; + + public TableAttribute() + { + } + + public TableAttribute(string name) + { + _name = name; + } + + public string Name + { + get { return _name; } + set { _name = value; } + } + } +} diff --git a/src/Marr.Data/Mapping/TableBuilder.cs b/src/Marr.Data/Mapping/TableBuilder.cs new file mode 100644 index 000000000..d4313e76b --- /dev/null +++ b/src/Marr.Data/Mapping/TableBuilder.cs @@ -0,0 +1,58 @@ +using System; + +namespace Marr.Data.Mapping +{ + /// + /// This class has fluent methods that are used to easily configure the table mapping. + /// + public class TableBuilder + { + private FluentMappings.MappingsFluentEntity _fluentEntity; + + public TableBuilder(FluentMappings.MappingsFluentEntity fluentEntity) + { + _fluentEntity = fluentEntity; + } + + #region - Fluent Methods - + + public TableBuilder SetTableName(string tableName) + { + MapRepository.Instance.Tables[typeof(TEntity)] = tableName; + return this; + } + + public FluentMappings.MappingsFluentColumns Columns + { + get + { + if (_fluentEntity == null) + { + throw new Exception("This property is not compatible with the obsolete 'MapBuilder' class."); + } + + return _fluentEntity.Columns; + } + } + + public FluentMappings.MappingsFluentRelationships Relationships + { + get + { + if (_fluentEntity == null) + { + throw new Exception("This property is not compatible with the obsolete 'MapBuilder' class."); + } + + return _fluentEntity.Relationships; + } + } + + public FluentMappings.MappingsFluentEntity Entity() + { + return new FluentMappings.MappingsFluentEntity(true); + } + + #endregion + } +} diff --git a/src/Marr.Data/Marr.Data.csproj b/src/Marr.Data/Marr.Data.csproj new file mode 100644 index 000000000..0a009748b --- /dev/null +++ b/src/Marr.Data/Marr.Data.csproj @@ -0,0 +1,9 @@ + + + netstandard2.0 + + 3.17.0.0 + false + false + + \ No newline at end of file diff --git a/src/Marr.Data/Parameters/DbTypeBuilder.cs b/src/Marr.Data/Parameters/DbTypeBuilder.cs new file mode 100644 index 000000000..94b6d2f5c --- /dev/null +++ b/src/Marr.Data/Parameters/DbTypeBuilder.cs @@ -0,0 +1,69 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using System.Data; + +namespace Marr.Data.Parameters +{ + public class DbTypeBuilder : IDbTypeBuilder + { + public Enum GetDbType(Type type) + { + if (type == typeof(String)) + return DbType.String; + + if (type == typeof(Int32)) + return DbType.Int32; + + if (type == typeof(Decimal)) + return DbType.Decimal; + + if (type == typeof(DateTime)) + return DbType.DateTime; + + if (type == typeof(Boolean)) + return DbType.Boolean; + + if (type == typeof(Int16)) + return DbType.Int16; + + if (type == typeof(Single)) + return DbType.Single; + + if (type == typeof(Int64)) + return DbType.Int64; + + if (type == typeof(Double)) + return DbType.Double; + + if (type == typeof(Byte)) + return DbType.Byte; + + if (type == typeof(Byte[])) + return DbType.Binary; + + if (type == typeof(Guid)) + return DbType.Guid; + + return DbType.Object; + } + + public void SetDbType(IDbDataParameter param, Enum dbType) + { + param.DbType = (DbType)dbType; + } + } +} diff --git a/src/Marr.Data/Parameters/IDbTypeBuilder.cs b/src/Marr.Data/Parameters/IDbTypeBuilder.cs new file mode 100644 index 000000000..1b04f2470 --- /dev/null +++ b/src/Marr.Data/Parameters/IDbTypeBuilder.cs @@ -0,0 +1,30 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using System.Data; + +namespace Marr.Data.Parameters +{ + /// + /// Converts from a .NET datatype to the appropriate DB type enum, + /// and then adds the value to the appropriate property on the parameter. + /// + public interface IDbTypeBuilder + { + Enum GetDbType(Type type); + void SetDbType(IDbDataParameter param, Enum dbType); + } +} diff --git a/src/Marr.Data/Parameters/ParameterChainMethods.cs b/src/Marr.Data/Parameters/ParameterChainMethods.cs new file mode 100644 index 000000000..e00535ff7 --- /dev/null +++ b/src/Marr.Data/Parameters/ParameterChainMethods.cs @@ -0,0 +1,133 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Runtime.InteropServices.ComTypes; +using Marr.Data.Converters; + +namespace Marr.Data.Parameters +{ + /// + /// This class allows chaining methods to be called for convenience when adding a parameter. + /// + public class ParameterChainMethods + { + /// + /// Creates a new parameter and adds it to the command's Parameters collection. + /// + /// The command that the parameter will be added to. + /// The parameter name. + public ParameterChainMethods(DbCommand command, string parameterName, object value) + { + Parameter = command.CreateParameter(); + Parameter.ParameterName = parameterName; + + // Convert null to DBNull.Value + if (value == null) + value = DBNull.Value; + + Type valueType = value.GetType(); + + // Check for a registered IConverter + //If we have a list of ints, we ignore the converter since we want to do an in statement! + var list = value as List; + if (list != null) + { + Parameter.Value = $"{string.Join(",", list)}"; + } + else + { + IConverter converter = MapRepository.Instance.GetConverter(valueType); + if (converter != null) + { + Parameter.Value = converter.ToDB(value); + } + else + { + Parameter.Value = value; + } + } + + + //// Determine the correct DbType based on the passed in value type + //IDbTypeBuilder typeBuilder = MapRepository.Instance.DbTypeBuilder; + //Enum dbType = typeBuilder.GetDbType(valueType); + + //// Set the appropriate DbType property depending on the parameter type + //typeBuilder.SetDbType(Parameter, dbType); + + command.Parameters.Add(Parameter); + } + + /// + /// Gets a reference to the parameter. + /// + public IDbDataParameter Parameter { get; private set; } + + /// + /// Sets the direction of a parameter. + /// + /// Determines the direction of a parameter. + /// Return a ParameterChainMethods object. + public ParameterChainMethods Direction(ParameterDirection direction) + { + Parameter.Direction = direction; + return this; + } + + /// + /// Sets the direction of a parameter to 'Output'. + /// + /// + public ParameterChainMethods Output() + { + Parameter.Direction = ParameterDirection.Output; + return this; + } + + public ParameterChainMethods DBType(DbType dbType) + { + Parameter.DbType = dbType; + return this; + } + + public ParameterChainMethods Size(int size) + { + Parameter.Size = size; + return this; + } + + public ParameterChainMethods Precision(byte precision) + { + Parameter.Precision = precision; + return this; + } + + public ParameterChainMethods Scale(byte scale) + { + Parameter.Scale = scale; + return this; + } + + public ParameterChainMethods Name(string name) + { + Parameter.ParameterName = name; + return this; + } + } +} diff --git a/src/Marr.Data/QGen/DeleteQuery.cs b/src/Marr.Data/QGen/DeleteQuery.cs new file mode 100644 index 000000000..7ea5a72fc --- /dev/null +++ b/src/Marr.Data/QGen/DeleteQuery.cs @@ -0,0 +1,28 @@ +using Marr.Data.QGen.Dialects; + +namespace Marr.Data.QGen +{ + /// + /// This class creates a SQL delete query. + /// + public class DeleteQuery : IQuery + { + protected Table TargetTable { get; set; } + protected string WhereClause { get; set; } + protected Dialect Dialect { get; set; } + + public DeleteQuery(Dialect dialect, Table targetTable, string whereClause) + { + Dialect = dialect; + TargetTable = targetTable; + WhereClause = whereClause; + } + + public string Generate() + { + return string.Format("DELETE FROM {0} {1} ", + Dialect.CreateToken(TargetTable.Name), + WhereClause); + } + } +} diff --git a/src/Marr.Data/QGen/Dialects/Dialect.cs b/src/Marr.Data/QGen/Dialects/Dialect.cs new file mode 100644 index 000000000..915866dfe --- /dev/null +++ b/src/Marr.Data/QGen/Dialects/Dialect.cs @@ -0,0 +1,80 @@ +using System; +using System.Text; + +namespace Marr.Data.QGen.Dialects +{ + public class Dialect + { + /// + /// The default token is surrounded by brackets. + /// + /// + public virtual string CreateToken(string token) + { + if (string.IsNullOrEmpty(token)) + { + return string.Empty; + } + + string[] parts = token.Replace('[', new Char()).Replace(']', new Char()).Split('.'); + + StringBuilder sb = new StringBuilder(); + foreach (string part in parts) + { + if (sb.Length > 0) + sb.Append("."); + + sb.Append("[").Append(part).Append("]"); + } + + return sb.ToString(); + } + + public virtual string IdentityQuery + { + get + { + return null; + } + } + + public bool HasIdentityQuery + { + get + { + return !string.IsNullOrEmpty(IdentityQuery); + } + } + + public virtual bool SupportsBatchQueries + { + get + { + return true; + } + } + + public virtual string StartsWithFormat + { + get { return "({0} LIKE {1} + '%')"; } + } + + public virtual string EndsWithFormat + { + get { return "({0} LIKE '%' + {1})"; } + } + + public virtual string ContainsFormat + { + get { return "({0} LIKE '%' + {1} + '%')"; } + } + + public virtual string InFormat + { + get + { + return "({0} in ({1}))"; + } + } + } +} diff --git a/src/Marr.Data/QGen/Dialects/FirebirdDialect.cs b/src/Marr.Data/QGen/Dialects/FirebirdDialect.cs new file mode 100644 index 000000000..49f42ab21 --- /dev/null +++ b/src/Marr.Data/QGen/Dialects/FirebirdDialect.cs @@ -0,0 +1,17 @@ +using System; + +namespace Marr.Data.QGen.Dialects +{ + public class FirebirdDialect : Dialect + { + public override string CreateToken(string token) + { + if (string.IsNullOrEmpty(token)) + { + return string.Empty; + } + + return token.Replace('[', new Char()).Replace(']', new Char()); + } + } +} diff --git a/src/Marr.Data/QGen/Dialects/OracleDialect.cs b/src/Marr.Data/QGen/Dialects/OracleDialect.cs new file mode 100644 index 000000000..23bc76751 --- /dev/null +++ b/src/Marr.Data/QGen/Dialects/OracleDialect.cs @@ -0,0 +1,35 @@ +using System; +using System.Linq; +using System.Text; + +namespace Marr.Data.QGen.Dialects +{ + public class OracleDialect : Dialect + { + public override string CreateToken(string token) + { + if (string.IsNullOrEmpty(token)) + { + return string.Empty; + } + + string[] parts = token.Replace('[', new Char()).Replace(']', new Char()).Split('.'); + + StringBuilder sb = new StringBuilder(); + foreach (string part in parts) + { + if (sb.Length > 0) + sb.Append("."); + + bool hasSpaces = part.Contains(' '); + + if (hasSpaces) + sb.Append("[").Append(part).Append("]"); + else + sb.Append(part); + } + + return sb.ToString(); + } + } +} diff --git a/src/Marr.Data/QGen/Dialects/SqlServerCeDialect.cs b/src/Marr.Data/QGen/Dialects/SqlServerCeDialect.cs new file mode 100644 index 000000000..355037f19 --- /dev/null +++ b/src/Marr.Data/QGen/Dialects/SqlServerCeDialect.cs @@ -0,0 +1,21 @@ +namespace Marr.Data.QGen.Dialects +{ + public class SqlServerCeDialect : Dialect + { + public override string IdentityQuery + { + get + { + return "SELECT @@IDENTITY;"; + } + } + + public override bool SupportsBatchQueries + { + get + { + return false; + } + } + } +} diff --git a/src/Marr.Data/QGen/Dialects/SqlServerDialect.cs b/src/Marr.Data/QGen/Dialects/SqlServerDialect.cs new file mode 100644 index 000000000..54bf61de4 --- /dev/null +++ b/src/Marr.Data/QGen/Dialects/SqlServerDialect.cs @@ -0,0 +1,13 @@ +namespace Marr.Data.QGen.Dialects +{ + public class SqlServerDialect : Dialect + { + public override string IdentityQuery + { + get + { + return "SELECT SCOPE_IDENTITY();"; + } + } + } +} diff --git a/src/Marr.Data/QGen/Dialects/SqliteDialect.cs b/src/Marr.Data/QGen/Dialects/SqliteDialect.cs new file mode 100644 index 000000000..f7eddfa23 --- /dev/null +++ b/src/Marr.Data/QGen/Dialects/SqliteDialect.cs @@ -0,0 +1,28 @@ +namespace Marr.Data.QGen.Dialects +{ + public class SqliteDialect : Dialect + { + public override string IdentityQuery + { + get + { + return "SELECT last_insert_rowid();"; + } + } + + public override string StartsWithFormat + { + get { return "({0} LIKE {1} || '%')"; } + } + + public override string EndsWithFormat + { + get { return "({0} LIKE '%' || {1})"; } + } + + public override string ContainsFormat + { + get { return "({0} LIKE '%' || {1} || '%')"; } + } + } +} diff --git a/src/Marr.Data/QGen/ExpressionVisitor.cs b/src/Marr.Data/QGen/ExpressionVisitor.cs new file mode 100644 index 000000000..381688e0e --- /dev/null +++ b/src/Marr.Data/QGen/ExpressionVisitor.cs @@ -0,0 +1,146 @@ +/* This class was copied from Mehfuz's LinqExtender project, which is available from github. + * http://mehfuzh.github.com/LinqExtender/ + */ + +using System; +using System.Linq.Expressions; + +namespace Marr.Data.QGen +{ + /// + /// Expression visitor + /// + public class ExpressionVisitor + { + /// + /// Visits expression and delegates call to different to branch. + /// + /// + /// + protected virtual Expression Visit(Expression expression) + { + if (expression == null) + return null; + + switch (expression.NodeType) + { + case ExpressionType.Lambda: + return VisitLamda((LambdaExpression)expression); + case ExpressionType.ArrayLength: + case ExpressionType.Convert: + case ExpressionType.ConvertChecked: + case ExpressionType.Negate: + case ExpressionType.UnaryPlus: + case ExpressionType.NegateChecked: + case ExpressionType.Not: + case ExpressionType.Quote: + case ExpressionType.TypeAs: + return VisitUnary((UnaryExpression)expression); + case ExpressionType.Add: + case ExpressionType.AddChecked: + case ExpressionType.And: + case ExpressionType.AndAlso: + case ExpressionType.ArrayIndex: + case ExpressionType.Coalesce: + case ExpressionType.Divide: + case ExpressionType.Equal: + case ExpressionType.ExclusiveOr: + case ExpressionType.GreaterThan: + case ExpressionType.GreaterThanOrEqual: + case ExpressionType.LeftShift: + case ExpressionType.LessThan: + case ExpressionType.LessThanOrEqual: + case ExpressionType.Modulo: + case ExpressionType.Multiply: + case ExpressionType.MultiplyChecked: + case ExpressionType.NotEqual: + case ExpressionType.Or: + case ExpressionType.OrElse: + case ExpressionType.Power: + case ExpressionType.RightShift: + case ExpressionType.Subtract: + case ExpressionType.SubtractChecked: + return VisitBinary((BinaryExpression)expression); + case ExpressionType.Call: + return VisitMethodCall((MethodCallExpression)expression); + case ExpressionType.Constant: + return VisitConstant((ConstantExpression)expression); + case ExpressionType.MemberAccess: + return VisitMemberAccess((MemberExpression)expression); + case ExpressionType.Parameter: + return VisitParameter((ParameterExpression)expression); + + } + throw new ArgumentOutOfRangeException("expression", expression.NodeType.ToString()); + } + + /// + /// Visits the constance expression. To be implemented by user. + /// + /// + /// + protected virtual Expression VisitConstant(ConstantExpression expression) + { + return expression; + } + + /// + /// Visits the memeber access expression. To be implemented by user. + /// + /// + /// + protected virtual Expression VisitMemberAccess(MemberExpression expression) + { + return expression; + } + + /// + /// Visits the method call expression. To be implemented by user. + /// + /// + /// + protected virtual Expression VisitMethodCall(MethodCallExpression expression) + { + throw new NotImplementedException(); + } + + /// + /// Visits the binary expression. + /// + /// + /// + protected virtual Expression VisitBinary(BinaryExpression expression) + { + Visit(expression.Left); + Visit(expression.Right); + return expression; + } + + /// + /// Visits the unary expression. + /// + /// + /// + protected virtual Expression VisitUnary(UnaryExpression expression) + { + Visit(expression.Operand); + return expression; + } + + /// + /// Visits the lamda expression. + /// + /// + /// + protected virtual Expression VisitLamda(LambdaExpression lambdaExpression) + { + Visit(lambdaExpression.Body); + return lambdaExpression; + } + + private Expression VisitParameter(ParameterExpression expression) + { + return expression; + } + } +} diff --git a/src/Marr.Data/QGen/IQuery.cs b/src/Marr.Data/QGen/IQuery.cs new file mode 100644 index 000000000..29df89567 --- /dev/null +++ b/src/Marr.Data/QGen/IQuery.cs @@ -0,0 +1,11 @@ +namespace Marr.Data.QGen +{ + internal interface IQuery + { + /// + /// Generates a SQL query for a given entity. + /// + /// + string Generate(); + } +} diff --git a/src/Marr.Data/QGen/IQueryBuilder.cs b/src/Marr.Data/QGen/IQueryBuilder.cs new file mode 100644 index 000000000..4a896f336 --- /dev/null +++ b/src/Marr.Data/QGen/IQueryBuilder.cs @@ -0,0 +1,12 @@ +namespace Marr.Data.QGen +{ + public interface IQueryBuilder + { + string BuildQuery(); + } + + public interface ISortQueryBuilder : IQueryBuilder + { + string BuildQuery(bool useAltNames); + } +} diff --git a/src/Marr.Data/QGen/InsertQuery.cs b/src/Marr.Data/QGen/InsertQuery.cs new file mode 100644 index 000000000..28460bf97 --- /dev/null +++ b/src/Marr.Data/QGen/InsertQuery.cs @@ -0,0 +1,67 @@ +using System.Text; +using Marr.Data.Mapping; +using System.Data.Common; +using Marr.Data.QGen.Dialects; + +namespace Marr.Data.QGen +{ + /// + /// This class creates an insert query. + /// + public class InsertQuery : IQuery + { + protected Dialect Dialect { get; set; } + protected string Target { get; set; } + protected ColumnMapCollection Columns { get; set; } + protected DbCommand Command { get; set; } + + public InsertQuery(Dialect dialect, ColumnMapCollection columns, DbCommand command, string target) + { + if (string.IsNullOrEmpty(target)) + { + throw new DataMappingException("A target table must be passed in or set in a TableAttribute."); + } + Dialect = dialect; + Target = target; + Columns = columns; + Command = command; + } + + public virtual string Generate() + { + StringBuilder sql = new StringBuilder(); + StringBuilder values = new StringBuilder(") VALUES ("); + + sql.AppendFormat("INSERT INTO {0} (", Dialect.CreateToken(Target)); + + int sqlStartIndex = sql.Length; + int valuesStartIndex = values.Length; + + foreach (DbParameter p in Command.Parameters) + { + var c = Columns.GetByColumnName(p.ParameterName); + + if (c == null) + break; // All insert columns have been added + + if (sql.Length > sqlStartIndex) + sql.Append(","); + + if (values.Length > valuesStartIndex) + values.Append(","); + + if (!c.ColumnInfo.IsAutoIncrement) + { + sql.AppendFormat(Dialect.CreateToken(c.ColumnInfo.Name)); + values.AppendFormat("{0}{1}", Command.ParameterPrefix(), p.ParameterName); + } + } + + values.Append(")"); + + sql.Append(values); + + return sql.ToString(); + } + } +} diff --git a/src/Marr.Data/QGen/InsertQueryBuilder.cs b/src/Marr.Data/QGen/InsertQueryBuilder.cs new file mode 100644 index 000000000..85cd0098f --- /dev/null +++ b/src/Marr.Data/QGen/InsertQueryBuilder.cs @@ -0,0 +1,203 @@ +using System; +using System.Collections.Generic; +using Marr.Data.Mapping; +using System.Linq.Expressions; +using Marr.Data.QGen.Dialects; + +namespace Marr.Data.QGen +{ + public class InsertQueryBuilder : IQueryBuilder + { + private DataMapper _db; + private string _tableName; + private T _entity; + private MappingHelper _mappingHelper; + private ColumnMapCollection _mappings; + private SqlModes _previousSqlMode; + private bool _generateQuery = true; + private bool _getIdentityValue; + private Dialect _dialect; + private ColumnMapCollection _columnsToInsert; + + public InsertQueryBuilder() + { + // Used only for unit testing with mock frameworks + } + + public InsertQueryBuilder(DataMapper db) + { + _db = db; + _tableName = MapRepository.Instance.GetTableName(typeof(T)); + _previousSqlMode = _db.SqlMode; + _mappingHelper = new MappingHelper(_db); + _mappings = MapRepository.Instance.GetColumns(typeof(T)); + _dialect = QueryFactory.CreateDialect(_db); + } + + public virtual InsertQueryBuilder TableName(string tableName) + { + _tableName = tableName; + return this; + } + + public virtual InsertQueryBuilder QueryText(string queryText) + { + _generateQuery = false; + _db.Command.CommandText = queryText; + return this; + } + + public virtual InsertQueryBuilder Entity(T entity) + { + _entity = entity; + return this; + } + + /// + /// Runs an identity query to get the value of an autoincrement field. + /// + /// + public virtual InsertQueryBuilder GetIdentity() + { + if (!_dialect.HasIdentityQuery) + { + string err = string.Format("The current dialect '{0}' does not have an identity query implemented.", _dialect.ToString()); + throw new DataMappingException(err); + } + + _getIdentityValue = true; + return this; + } + + public virtual InsertQueryBuilder ColumnsIncluding(params Expression>[] properties) + { + List columnList = new List(); + + foreach (var column in properties) + { + columnList.Add(column.GetMemberName()); + } + + return ColumnsIncluding(columnList.ToArray()); + } + + public virtual InsertQueryBuilder ColumnsIncluding(params string[] properties) + { + _columnsToInsert = new ColumnMapCollection(); + + foreach (string propertyName in properties) + { + _columnsToInsert.Add(_mappings.GetByFieldName(propertyName)); + } + + return this; + } + + public virtual InsertQueryBuilder ColumnsExcluding(params Expression>[] properties) + { + List columnList = new List(); + + foreach (var column in properties) + { + columnList.Add(column.GetMemberName()); + } + + return ColumnsExcluding(columnList.ToArray()); + } + + public virtual InsertQueryBuilder ColumnsExcluding(params string[] properties) + { + _columnsToInsert = new ColumnMapCollection(); + + _columnsToInsert.AddRange(_mappings); + + foreach (string propertyName in properties) + { + _columnsToInsert.RemoveAll(c => c.FieldName == propertyName); + } + + return this; + } + + public virtual object Execute() + { + if (_generateQuery) + { + BuildQuery(); + } + else + { + TryAppendIdentityQuery(); + _mappingHelper.CreateParameters(_entity, _mappings.NonReturnValues, _generateQuery); + } + + object scalar = null; + + try + { + _db.OpenConnection(); + + scalar = _db.Command.ExecuteScalar(); + + if (_getIdentityValue && !_dialect.SupportsBatchQueries) + { + // Run identity query as a separate query + _db.Command.CommandText = _dialect.IdentityQuery; + scalar = _db.Command.ExecuteScalar(); + } + + _mappingHelper.SetOutputValues(_entity, _mappings.OutputFields); + if (scalar != null) + { + _mappingHelper.SetOutputValues(_entity, _mappings.ReturnValues, scalar); + } + } + finally + { + _db.CloseConnection(); + } + + + if (_generateQuery) + { + // Return to previous sql mode + _db.SqlMode = _previousSqlMode; + } + + return scalar; + } + + public virtual string BuildQuery() + { + if (_entity == null) + throw new ArgumentNullException("You must specify an entity to insert."); + + // Override SqlMode since we know this will be a text query + _db.SqlMode = SqlModes.Text; + + var columns = _columnsToInsert ?? _mappings; + + _mappingHelper.CreateParameters(_entity, columns, _generateQuery); + IQuery query = QueryFactory.CreateInsertQuery(columns, _db, _tableName); + + _db.Command.CommandText = query.Generate(); + + TryAppendIdentityQuery(); + + return _db.Command.CommandText; + } + + private void TryAppendIdentityQuery() + { + if (_getIdentityValue && _dialect.SupportsBatchQueries) + { + // Append a batched identity query + if (!_db.Command.CommandText.EndsWith(";")) + { + _db.Command.CommandText += ";"; + } + _db.Command.CommandText += _dialect.IdentityQuery; + } + } + } +} diff --git a/src/Marr.Data/QGen/JoinBuilder.cs b/src/Marr.Data/QGen/JoinBuilder.cs new file mode 100644 index 000000000..45ea28312 --- /dev/null +++ b/src/Marr.Data/QGen/JoinBuilder.cs @@ -0,0 +1,36 @@ +using System; +using System.Data.Common; +using Marr.Data.QGen.Dialects; +using System.Linq.Expressions; + +namespace Marr.Data.QGen +{ + /// + /// This class overrides the WhereBuilder which utilizes the ExpressionVisitor base class, + /// and it is responsible for translating the lambda expression into a "JOIN ON" clause. + /// It populates the protected string builder, which outputs the "JOIN ON" clause when the ToString method is called. + /// + /// The entity that is on the left side of the join. + /// The entity that is on the right side of the join. + public class JoinBuilder : WhereBuilder + { + public JoinBuilder(DbCommand command, Dialect dialect, Expression> filter, TableCollection tables) + : base(command, dialect, filter.Body, tables, false, true) + { } + + protected override string PrefixText + { + get + { + return "ON"; + } + } + + protected override Expression VisitMemberAccess(MemberExpression expression) + { + string fqColumn = GetFullyQualifiedColumnName(expression.Member, expression.Expression.Type); + _sb.Append(fqColumn); + return expression; + } + } +} diff --git a/src/Marr.Data/QGen/PagingQueryDecorator.cs b/src/Marr.Data/QGen/PagingQueryDecorator.cs new file mode 100644 index 000000000..60a2ece05 --- /dev/null +++ b/src/Marr.Data/QGen/PagingQueryDecorator.cs @@ -0,0 +1,232 @@ +using System.Collections.Generic; +using System.Text; + +namespace Marr.Data.QGen +{ + /// + /// Decorates the SelectQuery by wrapping it in a paging query. + /// + public class PagingQueryDecorator : IQuery + { + private SelectQuery _innerQuery; + private int _firstRow; + private int _lastRow; + + public PagingQueryDecorator(SelectQuery innerQuery, int skip, int take) + { + if (string.IsNullOrEmpty(innerQuery.OrderBy.ToString())) + { + throw new DataMappingException("A paged query must specify an order by clause."); + } + + _innerQuery = innerQuery; + _firstRow = skip + 1; + _lastRow = skip + take; + } + + public string Generate() + { + // Decide which type of paging query to create + + if (_innerQuery.IsView || _innerQuery.IsJoin) + { + return ComplexPaging(); + } + return SimplePaging(); + } + + /// + /// Generates a query that pages a simple inner query. + /// + /// + private string SimplePaging() + { + // Create paged query + StringBuilder sql = new StringBuilder(); + + sql.AppendLine("WITH RowNumCTE AS"); + sql.AppendLine("("); + _innerQuery.BuildSelectClause(sql); + BuildRowNumberColumn(sql); + _innerQuery.BuildFromClause(sql); + _innerQuery.BuildJoinClauses(sql); + _innerQuery.BuildWhereClause(sql); + sql.AppendLine(")"); + BuildSimpleOuterSelect(sql); + + return sql.ToString(); + } + + /// + /// Generates a query that pages a view or joined inner query. + /// + /// + private string ComplexPaging() + { + // Create paged query + StringBuilder sql = new StringBuilder(); + + sql.AppendLine("WITH GroupCTE AS ("); + BuildSelectClause(sql); + BuildGroupColumn(sql); + _innerQuery.BuildFromClause(sql); + _innerQuery.BuildJoinClauses(sql); + _innerQuery.BuildWhereClause(sql); + sql.AppendLine("),"); + sql.AppendLine("RowNumCTE AS ("); + sql.AppendLine("SELECT *"); + BuildRowNumberColumn(sql); + sql.AppendLine("FROM GroupCTE"); + sql.AppendLine("WHERE GroupRow = 1"); + sql.AppendLine(")"); + _innerQuery.BuildSelectClause(sql); + _innerQuery.BuildFromClause(sql); + _innerQuery.BuildJoinClauses(sql); + BuildJoinBackToCTE(sql); + sql.AppendFormat("WHERE RowNumber BETWEEN {0} AND {1}", _firstRow, _lastRow); + + return sql.ToString(); + } + + private void BuildJoinBackToCTE(StringBuilder sql) + { + Table baseTable = GetBaseTable(); + sql.AppendLine("INNER JOIN RowNumCTE cte"); + int pksAdded = 0; + foreach (var pk in baseTable.Columns.PrimaryKeys) + { + if (pksAdded > 0) + sql.Append(" AND "); + + string cteQueryPkName = _innerQuery.NameOrAltName(pk.ColumnInfo); + string outerQueryPkName = _innerQuery.IsJoin ? pk.ColumnInfo.Name : _innerQuery.NameOrAltName(pk.ColumnInfo); + sql.AppendFormat("ON cte.{0} = {1} ", cteQueryPkName, _innerQuery.Dialect.CreateToken(string.Concat("t0", ".", outerQueryPkName))); + pksAdded++; + } + sql.AppendLine(); + } + + private void BuildSimpleOuterSelect(StringBuilder sql) + { + sql.Append("SELECT "); + int startIndex = sql.Length; + + // COLUMNS + foreach (Table join in _innerQuery.Tables) + { + for (int i = 0; i < join.Columns.Count; i++) + { + var c = join.Columns[i]; + + if (sql.Length > startIndex) + sql.Append(","); + + string token = _innerQuery.NameOrAltName(c.ColumnInfo); + sql.Append(_innerQuery.Dialect.CreateToken(token)); + } + } + + sql.AppendLine("FROM RowNumCTE"); + sql.AppendFormat("WHERE RowNumber BETWEEN {0} AND {1}", _firstRow, _lastRow).AppendLine(); + sql.AppendLine("ORDER BY RowNumber ASC;"); + } + + private void BuildGroupColumn(StringBuilder sql) + { + bool isView = _innerQuery.IsView; + sql.AppendFormat(", ROW_NUMBER() OVER (PARTITION BY {0} {1}) As GroupRow ", BuildBaseTablePKColumns(isView), _innerQuery.OrderBy.BuildQuery(isView)); + } + + private string BuildBaseTablePKColumns(bool useAltName = true) + { + Table baseTable = GetBaseTable(); + + StringBuilder sb = new StringBuilder(); + foreach (var col in baseTable.Columns.PrimaryKeys) + { + if (sb.Length > 0) + sb.AppendLine(", "); + + string columnName = useAltName ? + _innerQuery.NameOrAltName(col.ColumnInfo) : + col.ColumnInfo.Name; + + sb.AppendFormat(_innerQuery.Dialect.CreateToken(string.Concat(baseTable.Alias, ".", columnName))); + } + + return sb.ToString(); + } + + private void BuildRowNumberColumn(StringBuilder sql) + { + string orderBy = _innerQuery.OrderBy.ToString(); + // Remove table prefixes from order columns + foreach (Table t in _innerQuery.Tables) + { + orderBy = orderBy.Replace(string.Format("[{0}].", t.Alias), ""); + } + + sql.AppendFormat(", ROW_NUMBER() OVER ({0}) As RowNumber ", orderBy); + } + + private Table GetBaseTable() + { + Table baseTable = null; + if (_innerQuery.Tables[0] is View) + { + baseTable = (_innerQuery.Tables[0] as View).Tables[0]; + } + else + { + baseTable = _innerQuery.Tables[0]; + } + return baseTable; + } + + public void BuildSelectClause(StringBuilder sql) + { + List appended = new List(); + + sql.Append("SELECT "); + + int startIndex = sql.Length; + + // COLUMNS + foreach (Table join in _innerQuery.Tables) + { + for (int i = 0; i < join.Columns.Count; i++) + { + var c = join.Columns[i]; + + if (sql.Length > startIndex && sql[sql.Length - 1] != ',') + sql.Append(","); + + if (join is View) + { + string token = _innerQuery.Dialect.CreateToken(string.Concat(join.Alias, ".", _innerQuery.NameOrAltName(c.ColumnInfo))); + if (appended.Contains(token)) + continue; + + sql.Append(token); + appended.Add(token); + } + else + { + string token = string.Concat(join.Alias, ".", c.ColumnInfo.Name); + if (appended.Contains(token)) + continue; + + sql.Append(_innerQuery.Dialect.CreateToken(token)); + + if (_innerQuery.UseAltName && c.ColumnInfo.AltName != null && c.ColumnInfo.AltName != c.ColumnInfo.Name) + { + string altName = c.ColumnInfo.AltName; + sql.AppendFormat(" AS {0}", altName); + } + } + } + } + } + + } +} diff --git a/src/Marr.Data/QGen/QueryBuilder.cs b/src/Marr.Data/QGen/QueryBuilder.cs new file mode 100644 index 000000000..ba135ac07 --- /dev/null +++ b/src/Marr.Data/QGen/QueryBuilder.cs @@ -0,0 +1,635 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Marr.Data.Mapping; +using System.Data.Common; +using System.Collections; +using Marr.Data.QGen.Dialects; + +namespace Marr.Data.QGen +{ + /// + /// This class is responsible for building a select query. + /// It uses chaining methods to provide a fluent interface for creating select queries. + /// + /// + public class QueryBuilder : ExpressionVisitor, IEnumerable, IQueryBuilder + { + #region - Private Members - + + private DataMapper _db; + private Dialect _dialect; + private TableCollection _tables; + private WhereBuilder _whereBuilder; + private SortBuilder _sortBuilder; + private bool _isGraph = false; + private bool _isFromView = false; + private bool _isFromTable = false; + private bool _isJoin = false; + private bool _isManualQuery = false; + private bool _enablePaging = false; + private int _skip; + private int _take; + private string _queryText; + private List _childrenToLoad; + private SortBuilder SortBuilder + { + get + { + // Lazy load + if (_sortBuilder == null) + { + bool useAltNames = _isFromView || _isGraph || _isJoin; + _sortBuilder = new SortBuilder(this, _db, _whereBuilder, _dialect, _tables, useAltNames); + } + + return _sortBuilder; + } + } + private List _results = new List(); + private EntityGraph _entityGraph; + private EntityGraph EntGraph + { + get + { + if (_entityGraph == null) + { + _entityGraph = new EntityGraph(typeof(T), _results); + } + + return _entityGraph; + } + } + + #endregion + + #region - Constructor - + + public QueryBuilder() + { + // Used only for unit testing with mock frameworks + } + + public QueryBuilder(DataMapper db, Dialect dialect) + { + _db = db; + _dialect = dialect; + _tables = new TableCollection(); + _tables.Add(new Table(typeof(T))); + _childrenToLoad = new List(); + } + + #endregion + + #region - Fluent Methods - + + /// + /// Overrides the base table name that will be used in the query. + /// + [Obsolete("This method is obsolete. Use either the FromTable or FromView method instead.", true)] + public virtual QueryBuilder From(string tableName) + { + return FromView(tableName); + } + + /// + /// Overrides the base view name that will be used in the query. + /// Will try to use the mapped "AltName" values when loading the columns. + /// + public virtual QueryBuilder FromView(string viewName) + { + if (string.IsNullOrEmpty(viewName)) + throw new ArgumentNullException("view"); + + _isFromView = true; + + // Replace the base table with a view with tables + if (_tables[0] is View) + { + (_tables[0] as View).Name = viewName; + } + else + { + View view = new View(viewName, _tables.ToArray()); + _tables.ReplaceBaseTable(view); + } + + return this; + } + + /// + /// Overrides the base table name that will be used in the query. + /// Will not try to use the mapped "AltName" values when loading the columns. + /// + public virtual QueryBuilder FromTable(string table) + { + if (string.IsNullOrEmpty(table)) + throw new ArgumentNullException("view"); + + _isFromTable = true; + + // Override the base table name + _tables[0].Name = table; + return this; + } + + /// + /// Allows you to manually specify the query text. + /// + public virtual QueryBuilder QueryText(string queryText) + { + _isManualQuery = true; + _queryText = queryText; + return this; + } + + /// + /// If no parameters are passed in, this method instructs the DataMapper to load all related entities in the graph. + /// If specific entities are passed in, only these relationships will be loaded. + /// + /// A list of related child entites to load (passed in as properties / lambda expressions). + public virtual QueryBuilder Graph(params Expression>[] childrenToLoad) + { + TableCollection tablesInView = new TableCollection(); + if (childrenToLoad.Length > 0) + { + // Add base table + tablesInView.Add(_tables[0]); + + foreach (var exp in childrenToLoad) + { + MemberInfo child = (exp.Body as MemberExpression).Member; + + var node = EntGraph.Where(g => g.Member != null && g.Member.EqualsMember(child)).FirstOrDefault(); + if (node != null) + { + tablesInView.Add(new Table(node.EntityType, JoinType.None)); + } + + if (!_childrenToLoad.ContainsMember(child)) + { + _childrenToLoad.Add(child); + } + } + } + else + { + // Add all tables in the graph + foreach (var node in EntGraph) + { + tablesInView.Add(new Table(node.EntityType, JoinType.None)); + } + } + + // Replace the base table with a view with tables + View view = new View(_tables[0].Name, tablesInView.ToArray()); + _tables.ReplaceBaseTable(view); + + _isGraph = true; + return this; + } + + public virtual QueryBuilder Page(int pageNumber, int pageSize) + { + _enablePaging = true; + _skip = (pageNumber - 1) * pageSize; + _take = pageSize; + return this; + } + + private string[] ParseChildrenToLoad(Expression>[] childrenToLoad) + { + List entitiesToLoad = new List(); + + // Parse relationship member names from expression array + foreach (var exp in childrenToLoad) + { + MemberInfo member = (exp.Body as MemberExpression).Member; + entitiesToLoad.Add(member.Name); + + } + + return entitiesToLoad.ToArray(); + } + + /// + /// Allows you to interact with the DbDataReader to manually load entities. + /// + /// An action that takes a DbDataReader. + public virtual void DataReader(Action readerAction) + { + if (string.IsNullOrEmpty(_queryText)) + throw new ArgumentNullException("The query text cannot be blank."); + + var mappingHelper = new MappingHelper(_db); + _db.Command.CommandText = _queryText; + + try + { + _db.OpenConnection(); + using (DbDataReader reader = _db.Command.ExecuteReader()) + { + readerAction.Invoke(reader); + } + } + finally + { + _db.CloseConnection(); + } + } + + public virtual int GetRowCount() + { + SqlModes previousSqlMode = _db.SqlMode; + + // Generate a row count query + string where = _whereBuilder != null ? _whereBuilder.ToString() : string.Empty; + + bool useAltNames = _isFromView || _isGraph || _isJoin; + IQuery query = QueryFactory.CreateRowCountSelectQuery(_tables, _db, where, SortBuilder, useAltNames); + string queryText = query.Generate(); + + _db.SqlMode = SqlModes.Text; + int count = Convert.ToInt32(_db.ExecuteScalar(queryText)); + + _db.SqlMode = previousSqlMode; + return count; + } + + /// + /// Executes the query and returns a list of results. + /// + /// A list of query results of type T. + public virtual List ToList() + { + SqlModes previousSqlMode = _db.SqlMode; + + ValidateQuery(); + + BuildQueryOrAppendClauses(); + + if (_isGraph || _isJoin) + { + _results = (List)_db.QueryToGraph(_queryText, EntGraph, _childrenToLoad); + } + else + { + _results = (List)_db.Query(_queryText, _results, _isFromView); + } + + // Return to previous sql mode + _db.SqlMode = previousSqlMode; + + return _results; + } + + private void ValidateQuery() + { + if (_isManualQuery && _isFromView) + throw new InvalidOperationException("Cannot use FromView in conjunction with QueryText"); + + if (_isManualQuery && _isFromTable) + throw new InvalidOperationException("Cannot use FromTable in conjunction with QueryText"); + + if (_isManualQuery && _isJoin) + throw new InvalidOperationException("Cannot use Join in conjuntion with QueryText"); + + if (_isManualQuery && _enablePaging) + throw new InvalidOperationException("Cannot use Page, Skip or Take in conjunction with QueryText"); + + if (_isJoin && _isFromView) + throw new InvalidOperationException("Cannot use FromView in conjunction with Join"); + + if (_isJoin && _isFromTable) + throw new InvalidOperationException("Cannot use FromView in conjunction with Join"); + + if (_isJoin && _isGraph) + throw new InvalidOperationException("Cannot use Graph in conjunction with Join"); + + if (_isFromView && _isFromTable) + throw new InvalidOperationException("Cannot use FromView in conjunction with FromTable"); + } + + private void BuildQueryOrAppendClauses() + { + if (_queryText == null) + { + // Build entire query + _db.SqlMode = SqlModes.Text; + BuildQuery(); + } + else if (_whereBuilder != null || _sortBuilder != null) + { + _db.SqlMode = SqlModes.Text; + if (_whereBuilder != null) + { + // Append a where clause to an existing query + _queryText = string.Concat(_queryText, " ", _whereBuilder.ToString()); + } + + if (_sortBuilder != null) + { + // Append an order clause to an existing query + _queryText = string.Concat(_queryText, " ", _sortBuilder.ToString()); + } + } + } + + public virtual string BuildQuery() + { + // Generate a query + string where = _whereBuilder != null ? _whereBuilder.ToString() : string.Empty; + + bool useAltNames = _isFromView || _isGraph || _isJoin; + + IQuery query = null; + if (_enablePaging) + { + query = QueryFactory.CreatePagingSelectQuery(_tables, _db, where, SortBuilder, useAltNames, _skip, _take); + } + else + { + query = QueryFactory.CreateSelectQuery(_tables, _db, where, SortBuilder, useAltNames); + } + + _queryText = query.Generate(); + + return _queryText; + } + + #endregion + + #region - Helper Methods - + + private ColumnMapCollection GetColumns(IEnumerable entitiesToLoad) + { + // If QueryToGraph and no child load entities are specified, load all children + bool useAltNames = _isFromView || _isGraph || _isJoin; + bool loadAllChildren = useAltNames && entitiesToLoad == null; + + // If Query + if (!useAltNames) + { + return MapRepository.Instance.GetColumns(typeof(T)); + } + + ColumnMapCollection columns = new ColumnMapCollection(); + + Type baseEntityType = typeof(T); + EntityGraph graph = new EntityGraph(baseEntityType, null); + + foreach (var lvl in graph) + { + if (loadAllChildren || lvl.IsRoot || entitiesToLoad.Contains(lvl.Member.Name)) + { + columns.AddRange(lvl.Columns); + } + } + + return columns; + } + + public static implicit operator List(QueryBuilder builder) + { + return builder.ToList(); + } + + #endregion + + #region - Linq Support - + + public virtual SortBuilder Where(Expression> filterExpression) + { + bool useAltNames = _isFromView || _isGraph; + bool addTablePrefixToColumns = true; + _whereBuilder = new WhereBuilder(_db.Command, _dialect, filterExpression, _tables, useAltNames, addTablePrefixToColumns); + return SortBuilder; + } + + public virtual SortBuilder Where(Expression> filterExpression) + { + bool useAltNames = _isFromView || _isGraph; + bool addTablePrefixToColumns = true; + _whereBuilder = new WhereBuilder(_db.Command, _dialect, filterExpression, _tables, useAltNames, addTablePrefixToColumns); + return SortBuilder; + } + + public virtual SortBuilder Where(string whereClause) + { + if (string.IsNullOrEmpty(whereClause)) + throw new ArgumentNullException("whereClause"); + + if (!whereClause.ToUpper().Contains("WHERE ")) + { + whereClause = whereClause.Insert(0, " WHERE "); + } + + bool useAltNames = _isFromView || _isGraph || _isJoin; + _whereBuilder = new WhereBuilder(whereClause, useAltNames); + return SortBuilder; + } + + public virtual SortBuilder OrderBy(Expression> sortExpression) + { + SortBuilder.OrderBy(sortExpression); + return SortBuilder; + } + + public virtual SortBuilder OrderBy(Expression> sortExpression, SortDirection sortDirection) + { + SortBuilder.OrderBy(sortExpression, sortDirection); + return SortBuilder; + } + + public virtual SortBuilder ThenBy(Expression> sortExpression) + { + SortBuilder.OrderBy(sortExpression); + return SortBuilder; + } + + public virtual SortBuilder ThenBy(Expression> sortExpression, SortDirection sortDirection) + { + SortBuilder.OrderBy(sortExpression, sortDirection); + return SortBuilder; + } + + public virtual SortBuilder OrderByDescending(Expression> sortExpression) + { + SortBuilder.OrderByDescending(sortExpression); + return SortBuilder; + } + + public virtual SortBuilder ThenByDescending(Expression> sortExpression) + { + SortBuilder.OrderByDescending(sortExpression); + return SortBuilder; + } + + public virtual SortBuilder OrderBy(string orderByClause) + { + if (string.IsNullOrEmpty(orderByClause)) + throw new ArgumentNullException("orderByClause"); + + if (!orderByClause.ToUpper().Contains("ORDER BY ")) + { + orderByClause = orderByClause.Insert(0, " ORDER BY "); + } + + SortBuilder.OrderBy(orderByClause); + return SortBuilder; + } + + public virtual QueryBuilder Take(int count) + { + _enablePaging = true; + _take = count; + return this; + } + + public virtual QueryBuilder Skip(int count) + { + _enablePaging = true; + _skip = count; + return this; + } + + /// + /// Handles all. + /// + /// + /// + protected override Expression Visit(Expression expression) + { + return base.Visit(expression); + } + + /// + /// Handles Where. + /// + /// + /// + protected override Expression VisitLamda(LambdaExpression lambdaExpression) + { + _sortBuilder = Where(lambdaExpression as Expression>); + return base.VisitLamda(lambdaExpression); + } + + /// + /// Handles OrderBy. + /// + /// + /// + protected override Expression VisitMethodCall(MethodCallExpression expression) + { + if (expression.Method.Name == "OrderBy" || expression.Method.Name == "ThenBy") + { + var memberExp = ((expression.Arguments[1] as UnaryExpression).Operand as LambdaExpression).Body as MemberExpression; + _sortBuilder.Order(memberExp.Expression.Type, memberExp.Member.Name); + } + if (expression.Method.Name == "OrderByDescending" || expression.Method.Name == "ThenByDescending") + { + var memberExp = ((expression.Arguments[1] as UnaryExpression).Operand as LambdaExpression).Body as MemberExpression; + _sortBuilder.OrderByDescending(memberExp.Expression.Type, memberExp.Member.Name); + } + + return base.VisitMethodCall(expression); + } + + public virtual QueryBuilder Join(JoinType joinType, Expression>> rightEntity, Expression> filterExpression) + { + _isJoin = true; + MemberInfo rightMember = (rightEntity.Body as MemberExpression).Member; + return Join(joinType, rightMember, filterExpression); + } + + public virtual QueryBuilder Join(JoinType joinType, Expression> rightEntity, Expression> filterExpression) + { + _isJoin = true; + MemberInfo rightMember = (rightEntity.Body as MemberExpression).Member; + return Join(joinType, rightMember, filterExpression); + } + + public virtual QueryBuilder Join(JoinType joinType, Expression>> rightEntity, Expression> filterExpression) + { + _isJoin = true; + MemberInfo rightMember = (rightEntity.Body as MemberExpression).Member; + + foreach (var item in EntGraph) + { + if (item.EntityType == typeof(TLeft)) + { + var relationship = item.Relationships.Single(v => v.Member == rightMember); + item.AddLazyRelationship(relationship); + } + } + + return Join(joinType, rightMember, filterExpression); + } + + public virtual QueryBuilder Join(JoinType joinType, MemberInfo rightMember, Expression> filterExpression) + { + _isJoin = true; + + if (!_childrenToLoad.ContainsMember(rightMember)) + _childrenToLoad.Add(rightMember); + + Table table = new Table(typeof(TRight), joinType); + _tables.Add(table); + + var builder = new JoinBuilder(_db.Command, _dialect, filterExpression, _tables); + + table.JoinClause = builder.ToString(); + return this; + } + + public virtual bool Any(Expression> filterExpression) + { + bool useAltNames = _isFromView || _isGraph; + bool addTablePrefixToColumns = true; + _whereBuilder = new WhereBuilder(_db.Command, _dialect, filterExpression, _tables, useAltNames, addTablePrefixToColumns); + return Any(); + } + + public virtual bool Any() + { + SqlModes previousSqlMode = _db.SqlMode; + + // Generate a row count query + string where = _whereBuilder != null ? _whereBuilder.ToString() : string.Empty; + + bool useAltNames = _isFromView || _isGraph || _isJoin; + IQuery query = QueryFactory.CreateRowCountSelectQuery(_tables, _db, where, SortBuilder, useAltNames); + string queryText = query.Generate(); + + _db.SqlMode = SqlModes.Text; + int count = Convert.ToInt32(_db.ExecuteScalar(queryText)); + + _db.SqlMode = previousSqlMode; + return count > 0; + } + + #endregion + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() + { + var list = ToList(); + return list.GetEnumerator(); + } + + #endregion + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() + { + var list = ToList(); + return list.GetEnumerator(); + } + + #endregion + } +} diff --git a/src/Marr.Data/QGen/QueryFactory.cs b/src/Marr.Data/QGen/QueryFactory.cs new file mode 100644 index 000000000..5e1d90b9a --- /dev/null +++ b/src/Marr.Data/QGen/QueryFactory.cs @@ -0,0 +1,113 @@ +using System; +using Marr.Data.Mapping; +using Marr.Data.QGen.Dialects; + +namespace Marr.Data.QGen +{ + /// + /// This class contains the factory logic that determines which type of IQuery object should be created. + /// + internal class QueryFactory + { + private const string DB_SqlClient = "System.Data.SqlClient.SqlClientFactory"; + private const string DB_OleDb = "System.Data.OleDb.OleDbFactory"; + private const string DB_SqlCeClient = "System.Data.SqlServerCe.SqlCeProviderFactory"; + private const string DB_SystemDataOracleClient = "System.Data.OracleClientFactory"; + private const string DB_OracleDataAccessClient = "Oracle.DataAccess.Client.OracleClientFactory"; + private const string DB_FireBirdClient = "FirebirdSql.Data.FirebirdClient.FirebirdClientFactory"; + private const string DB_SQLiteClient = "System.Data.SQLite.SQLiteFactory"; + + public static IQuery CreateUpdateQuery(ColumnMapCollection columns, IDataMapper dataMapper, string target, string whereClause) + { + Dialect dialect = CreateDialect(dataMapper); + return new UpdateQuery(dialect, columns, dataMapper.Command, target, whereClause); + } + + public static IQuery CreateInsertQuery(ColumnMapCollection columns, IDataMapper dataMapper, string target) + { + Dialect dialect = CreateDialect(dataMapper); + return new InsertQuery(dialect, columns, dataMapper.Command, target); + } + + public static IQuery CreateDeleteQuery(Dialect dialect, Table targetTable, string whereClause) + { + return new DeleteQuery(dialect, targetTable, whereClause); + } + + public static IQuery CreateSelectQuery(TableCollection tables, IDataMapper dataMapper, string where, ISortQueryBuilder orderBy, bool useAltName) + { + Dialect dialect = CreateDialect(dataMapper); + return new SelectQuery(dialect, tables, where, orderBy, useAltName); + } + + public static IQuery CreateRowCountSelectQuery(TableCollection tables, IDataMapper dataMapper, string where, ISortQueryBuilder orderBy, bool useAltName) + { + SelectQuery innerQuery = (SelectQuery)CreateSelectQuery(tables, dataMapper, where, orderBy, useAltName); + + string providerString = dataMapper.ProviderFactory.ToString(); + switch (providerString) + { + case DB_SqlClient: + return new RowCountQueryDecorator(innerQuery); + + case DB_SqlCeClient: + return new RowCountQueryDecorator(innerQuery); + + case DB_SQLiteClient: + return new SqliteRowCountQueryDecorator(innerQuery); + + default: + throw new NotImplementedException("Row count has not yet been implemented for this provider."); + } + } + + public static IQuery CreatePagingSelectQuery(TableCollection tables, IDataMapper dataMapper, string where, ISortQueryBuilder orderBy, bool useAltName, int skip, int take) + { + SelectQuery innerQuery = (SelectQuery)CreateSelectQuery(tables, dataMapper, where, orderBy, useAltName); + + string providerString = dataMapper.ProviderFactory.ToString(); + switch (providerString) + { + case DB_SqlClient: + return new PagingQueryDecorator(innerQuery, skip, take); + + case DB_SqlCeClient: + return new PagingQueryDecorator(innerQuery, skip, take); + + case DB_SQLiteClient: + return new SqlitePagingQueryDecorator(innerQuery, skip, take); + + default: + throw new NotImplementedException("Paging has not yet been implemented for this provider."); + } + } + + public static Dialect CreateDialect(IDataMapper dataMapper) + { + string providerString = dataMapper.ProviderFactory.ToString(); + switch (providerString) + { + case DB_SqlClient: + return new SqlServerDialect(); + + case DB_OracleDataAccessClient: + return new OracleDialect(); + + case DB_SystemDataOracleClient: + return new OracleDialect(); + + case DB_SqlCeClient: + return new SqlServerCeDialect(); + + case DB_FireBirdClient: + return new FirebirdDialect(); + + case DB_SQLiteClient: + return new SqliteDialect(); + + default: + return new Dialect(); + } + } + } +} diff --git a/src/Marr.Data/QGen/QueryQueueItem.cs b/src/Marr.Data/QGen/QueryQueueItem.cs new file mode 100644 index 000000000..98edb6c79 --- /dev/null +++ b/src/Marr.Data/QGen/QueryQueueItem.cs @@ -0,0 +1,19 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; + +//namespace Marr.Data.QGen +//{ +// public class QueryQueueItem +// { +// public QueryQueueItem(string queryText, IEnumerable entitiesToLoad) +// { +// QueryText = queryText; +// EntitiesToLoad = entitiesToLoad; +// } + +// public string QueryText { get; set; } +// public IEnumerable EntitiesToLoad { get; private set; } +// } +//} diff --git a/src/Marr.Data/QGen/RowCountQueryDecorator.cs b/src/Marr.Data/QGen/RowCountQueryDecorator.cs new file mode 100644 index 000000000..f7c225389 --- /dev/null +++ b/src/Marr.Data/QGen/RowCountQueryDecorator.cs @@ -0,0 +1,121 @@ +using System.Text; + +namespace Marr.Data.QGen +{ + public class RowCountQueryDecorator : IQuery + { + private SelectQuery _innerQuery; + + public RowCountQueryDecorator(SelectQuery innerQuery) + { + _innerQuery = innerQuery; + } + + public string Generate() + { + // Decide which type of paging query to create + if (_innerQuery.IsView || _innerQuery.IsJoin) + { + return ComplexRowCount(); + } + return SimpleRowCount(); + } + + /// + /// Generates a row count query for a multiple table joined query (groups by the parent entity). + /// + /// + private string ComplexRowCount() + { + // Create paged query + StringBuilder sql = new StringBuilder(); + + sql.AppendLine("WITH GroupCTE AS ("); + sql.Append("SELECT ").AppendLine(BuildBaseTablePKColumns()); + BuildGroupColumn(sql); + _innerQuery.BuildFromClause(sql); + _innerQuery.BuildJoinClauses(sql); + _innerQuery.BuildWhereClause(sql); + sql.AppendLine(")"); + BuildSelectCountClause(sql); + sql.AppendLine("FROM GroupCTE"); + sql.AppendLine("WHERE GroupRow = 1"); + + return sql.ToString(); + } + + /// + /// Generates a row count query for a single table query (no joins). + /// + /// + private string SimpleRowCount() + { + StringBuilder sql = new StringBuilder(); + + BuildSelectCountClause(sql); + _innerQuery.BuildFromClause(sql); + _innerQuery.BuildJoinClauses(sql); + _innerQuery.BuildWhereClause(sql); + + return sql.ToString(); + } + + private void BuildGroupColumn(StringBuilder sql) + { + string baseTablePKColumns = BuildBaseTablePKColumns(); + sql.AppendFormat(", ROW_NUMBER() OVER (PARTITION BY {0} ORDER BY {1}) As GroupRow ", baseTablePKColumns, baseTablePKColumns); + } + + private string BuildBaseTablePKColumns() + { + Table baseTable = GetBaseTable(); + + StringBuilder sb = new StringBuilder(); + foreach (var col in baseTable.Columns.PrimaryKeys) + { + if (sb.Length > 0) + sb.AppendLine(", "); + + string colName = _innerQuery.IsView ? + _innerQuery.NameOrAltName(col.ColumnInfo) : + col.ColumnInfo.Name; + + sb.AppendFormat(_innerQuery.Dialect.CreateToken(string.Concat(baseTable.Alias, ".", colName))); + } + + return sb.ToString(); + } + + private void BuildSelectCountClause(StringBuilder sql) + { + sql.AppendLine("SELECT COUNT(*)"); + } + + private Table GetBaseTable() + { + Table baseTable = null; + if (_innerQuery.Tables[0] is View) + { + baseTable = (_innerQuery.Tables[0] as View).Tables[0]; + } + else + { + baseTable = _innerQuery.Tables[0]; + } + return baseTable; + } + } +} + +/* +WITH GroupCTE AS +( + SELECT [t0].[ID],[t0].[OrderName],[t1].[ID] AS OrderItemID,[t1].[OrderID],[t1].[ItemDescription],[t1].[Price], + ROW_NUMBER() OVER (PARTITION BY [t0].[ID] ORDER BY [t0].[OrderName]) As GroupRow + FROM [Order] [t0] + LEFT JOIN [OrderItem] [t1] ON (([t0].[ID] = [t1].[OrderID])) + --WHERE (([t0].[OrderName] = @P0)) +) +SELECT * FROM GroupCTE +WHERE GroupRow = 1 +*/ \ No newline at end of file diff --git a/src/Marr.Data/QGen/SelectQuery.cs b/src/Marr.Data/QGen/SelectQuery.cs new file mode 100644 index 000000000..97aa15f10 --- /dev/null +++ b/src/Marr.Data/QGen/SelectQuery.cs @@ -0,0 +1,159 @@ +using System.Linq; +using System.Text; +using Marr.Data.Mapping; +using Marr.Data.QGen.Dialects; + +namespace Marr.Data.QGen +{ + /// + /// This class is responsible for creating a select query. + /// + public class SelectQuery : IQuery + { + public Dialect Dialect { get; set; } + public string WhereClause { get; set; } + public ISortQueryBuilder OrderBy { get; set; } + public TableCollection Tables { get; set; } + public bool UseAltName; + + public SelectQuery(Dialect dialect, TableCollection tables, string whereClause, ISortQueryBuilder orderBy, bool useAltName) + { + Dialect = dialect; + Tables = tables; + WhereClause = whereClause; + OrderBy = orderBy; + UseAltName = useAltName; + } + + public bool IsView + { + get + { + return Tables[0] is View; + } + } + + public bool IsJoin + { + get + { + return Tables.Count > 1; + } + } + + public virtual string Generate() + { + StringBuilder sql = new StringBuilder(); + + BuildSelectClause(sql); + BuildFromClause(sql); + BuildJoinClauses(sql); + BuildWhereClause(sql); + BuildOrderClause(sql); + + return sql.ToString(); + } + + public void BuildSelectClause(StringBuilder sql) + { + sql.Append("SELECT "); + + int startIndex = sql.Length; + + // COLUMNS + foreach (Table join in Tables) + { + for (int i = 0; i < join.Columns.Count; i++) + { + var c = join.Columns[i]; + + if (sql.Length > startIndex) + sql.Append(","); + + if (join is View) + { + string token = string.Concat(join.Alias, ".", NameOrAltName(c.ColumnInfo)); + sql.Append(Dialect.CreateToken(token)); + } + else + { + string token = string.Concat(join.Alias, ".", c.ColumnInfo.Name); + sql.Append(Dialect.CreateToken(token)); + + if (UseAltName && c.ColumnInfo.AltName != null && c.ColumnInfo.AltName != c.ColumnInfo.Name) + { + string altName = c.ColumnInfo.AltName; + sql.AppendFormat(" AS {0}", altName); + } + } + } + } + } + + public string NameOrAltName(IColumnInfo columnInfo) + { + if (UseAltName && columnInfo.AltName != null && columnInfo.AltName != columnInfo.Name) + { + return columnInfo.AltName; + } + return columnInfo.Name; + } + + public void BuildFromClause(StringBuilder sql) + { + // BASE TABLE + Table baseTable = Tables[0]; + sql.AppendFormat(" FROM {0} {1} ", Dialect.CreateToken(baseTable.Name), Dialect.CreateToken(baseTable.Alias)); + } + + public void BuildJoinClauses(StringBuilder sql) + { + // JOINS + for (int i = 1; i < Tables.Count; i++) + { + if (Tables[i].JoinType != JoinType.None) + { + sql.AppendFormat("{0} {1} {2} {3} ", + TranslateJoin(Tables[i].JoinType), + Dialect.CreateToken(Tables[i].Name), + Dialect.CreateToken(Tables[i].Alias), + Tables[i].JoinClause); + } + } + } + + public void BuildWhereClause(StringBuilder sql) + { + sql.Append(WhereClause); + } + + public void BuildOrderClause(StringBuilder sql) + { + sql.Append(OrderBy.ToString()); + } + + public void BuildGroupBy(StringBuilder sql) + { + var baseTable = this.Tables.First(); + var primaryKeyColumn = baseTable.Columns.Single(c => c.ColumnInfo.IsPrimaryKey); + + string token = this.Dialect.CreateToken(string.Concat(baseTable.Alias, ".", primaryKeyColumn.ColumnInfo.Name)); + sql.AppendFormat(" GROUP BY {0}", token); + } + + private string TranslateJoin(JoinType join) + { + switch (join) + { + case JoinType.Inner: + return "INNER JOIN"; + case JoinType.Left: + return "LEFT JOIN"; + case JoinType.Right: + return "RIGHT JOIN"; + default: + return string.Empty; + } + } + } +} diff --git a/src/Marr.Data/QGen/SortBuilder.cs b/src/Marr.Data/QGen/SortBuilder.cs new file mode 100644 index 000000000..32c85eccd --- /dev/null +++ b/src/Marr.Data/QGen/SortBuilder.cs @@ -0,0 +1,262 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using System.Linq.Expressions; +using Marr.Data.QGen.Dialects; + +namespace Marr.Data.QGen +{ + /// + /// This class is responsible for creating an "ORDER BY" clause. + /// It uses chaining methods to provide a fluent interface. + /// It also has some methods that coincide with Linq methods, to provide Linq compatibility. + /// + /// + public class SortBuilder : IEnumerable, ISortQueryBuilder + { + private string _constantOrderByClause; + private QueryBuilder _baseBuilder; + private Dialect _dialect; + private List> _sortExpressions; + private bool _useAltName; + private TableCollection _tables; + private IDataMapper _db; + private WhereBuilder _whereBuilder; + + public SortBuilder() + { + // Used only for unit testing with mock frameworks + } + + public SortBuilder(QueryBuilder baseBuilder, IDataMapper db, WhereBuilder whereBuilder, Dialect dialect, TableCollection tables, bool useAltName) + { + _baseBuilder = baseBuilder; + _db = db; + _whereBuilder = whereBuilder; + _dialect = dialect; + _sortExpressions = new List>(); + _useAltName = useAltName; + _tables = tables; + } + + #region - AndWhere / OrWhere - + + public virtual SortBuilder OrWhere(Expression> filterExpression) + { + var orWhere = new WhereBuilder(_db.Command, _dialect, filterExpression, _tables, false, true); + _whereBuilder.Append(orWhere, WhereAppendType.OR); + return this; + } + + public virtual SortBuilder OrWhere(string whereClause) + { + var orWhere = new WhereBuilder(whereClause, false); + _whereBuilder.Append(orWhere, WhereAppendType.OR); + return this; + } + + public virtual SortBuilder AndWhere(Expression> filterExpression) + { + var andWhere = new WhereBuilder(_db.Command, _dialect, filterExpression, _tables, false, true); + _whereBuilder.Append(andWhere, WhereAppendType.AND); + return this; + } + + public virtual SortBuilder AndWhere(string whereClause) + { + var andWhere = new WhereBuilder(whereClause, false); + _whereBuilder.Append(andWhere, WhereAppendType.AND); + return this; + } + + #endregion + + #region - Order - + + internal SortBuilder Order(Type declaringType, string propertyName) + { + _sortExpressions.Add(new SortColumn(declaringType, propertyName, SortDirection.Asc)); + return this; + } + + internal SortBuilder OrderByDescending(Type declaringType, string propertyName) + { + _sortExpressions.Add(new SortColumn(declaringType, propertyName, SortDirection.Desc)); + return this; + } + + public virtual SortBuilder OrderBy(string orderByClause) + { + if (string.IsNullOrEmpty(orderByClause)) + throw new ArgumentNullException("orderByClause"); + + if (!orderByClause.ToUpper().Contains("ORDER BY ")) + { + orderByClause = orderByClause.Insert(0, " ORDER BY "); + } + + _constantOrderByClause = orderByClause; + return this; + } + + public virtual SortBuilder OrderBy(Expression> sortExpression) + { + _sortExpressions.Add(new SortColumn(sortExpression, SortDirection.Asc)); + return this; + } + + public virtual SortBuilder OrderBy(Expression> sortExpression, SortDirection sortDirection) + { + _sortExpressions.Add(new SortColumn(sortExpression, sortDirection)); + return this; + } + + public virtual SortBuilder OrderByDescending(Expression> sortExpression) + { + _sortExpressions.Add(new SortColumn(sortExpression, SortDirection.Desc)); + return this; + } + + public virtual SortBuilder ThenBy(Expression> sortExpression) + { + _sortExpressions.Add(new SortColumn(sortExpression, SortDirection.Asc)); + return this; + } + + public virtual SortBuilder ThenBy(Expression> sortExpression, SortDirection sortDirection) + { + _sortExpressions.Add(new SortColumn(sortExpression, sortDirection)); + return this; + } + + public virtual SortBuilder ThenByDescending(Expression> sortExpression) + { + _sortExpressions.Add(new SortColumn(sortExpression, SortDirection.Desc)); + return this; + } + + #endregion + + #region - Paging - + + public virtual SortBuilder Take(int count) + { + _baseBuilder.Take(count); + return this; + } + + public virtual SortBuilder Skip(int count) + { + _baseBuilder.Skip(count); + return this; + } + + public virtual SortBuilder Page(int pageNumber, int pageSize) + { + _baseBuilder.Page(pageNumber, pageSize); + return this; + } + + #endregion + + #region - GetRowCount - + + public virtual int GetRowCount() + { + return _baseBuilder.GetRowCount(); + } + + #endregion + + #region - ToList / ToString / BuildQuery - + + public virtual List ToList() + { + return _baseBuilder.ToList(); + } + + public virtual string BuildQuery() + { + return _baseBuilder.BuildQuery(); + } + + public virtual string BuildQuery(bool useAltName) + { + if (!string.IsNullOrEmpty(_constantOrderByClause)) + { + return _constantOrderByClause; + } + + StringBuilder sb = new StringBuilder(); + + foreach (var sort in _sortExpressions) + { + if (sb.Length > 0) + sb.Append(","); + + Table table = _tables.FindTable(sort.DeclaringType); + + if (table == null) + { + string msg = string.Format("The property '{0} -> {1}' you are trying to reference in the 'ORDER BY' statement belongs to an entity that has not been joined in your query. To reference this property, you must join the '{0}' entity using the Join method.", + sort.DeclaringType.Name, + sort.PropertyName); + + throw new DataMappingException(msg); + } + + string columnName = DataHelper.GetColumnName(sort.DeclaringType, sort.PropertyName, useAltName); + + if (!useAltName) + sb.Append(_dialect.CreateToken(string.Format("{0}.{1}", table.Alias, columnName))); + + else + sb.Append(_dialect.CreateToken(string.Format("{0}", columnName))); + + if (sort.Direction == SortDirection.Desc) + sb.Append(" DESC"); + } + + if (sb.Length > 0) + sb.Insert(0, " ORDER BY "); + + return sb.ToString(); + } + + public override string ToString() + { + return BuildQuery(_useAltName); + } + + #endregion + + #region - Implicit List Operator - + + public static implicit operator List(SortBuilder builder) + { + return builder.ToList(); + } + + #endregion + + #region IEnumerable Members + + public virtual IEnumerator GetEnumerator() + { + var list = ToList(); + return list.GetEnumerator(); + } + + #endregion + + #region IEnumerable Members + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + + #endregion + } +} diff --git a/src/Marr.Data/QGen/SortColumn.cs b/src/Marr.Data/QGen/SortColumn.cs new file mode 100644 index 000000000..6d9236219 --- /dev/null +++ b/src/Marr.Data/QGen/SortColumn.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq.Expressions; + +namespace Marr.Data.QGen +{ + public class SortColumn + { + public SortColumn(Expression> sortExpression, SortDirection direction) + { + MemberExpression me = GetMemberExpression(sortExpression.Body); + DeclaringType = me.Expression.Type; + PropertyName = me.Member.Name; + Direction = direction; + } + + public SortColumn(Type declaringType, string propertyName, SortDirection direction) + { + DeclaringType = declaringType; + PropertyName = propertyName; + Direction = direction; + } + + public SortDirection Direction { get; private set; } + public Type DeclaringType { get; private set; } + public string PropertyName { get; private set; } + + private MemberExpression GetMemberExpression(Expression exp) + { + MemberExpression me = exp as MemberExpression; + + if (me == null) + { + var ue = exp as UnaryExpression; + me = ue.Operand as MemberExpression; + } + + return me; + } + } + + public enum SortDirection + { + Asc, + Desc + } +} diff --git a/src/Marr.Data/QGen/SqlitePagingQueryDecorator.cs b/src/Marr.Data/QGen/SqlitePagingQueryDecorator.cs new file mode 100644 index 000000000..f77614523 --- /dev/null +++ b/src/Marr.Data/QGen/SqlitePagingQueryDecorator.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Marr.Data.QGen +{ + /// + /// Decorates the SelectQuery by wrapping it in a paging query. + /// + public class SqlitePagingQueryDecorator : IQuery + { + private SelectQuery _innerQuery; + private int _skip; + private int _take; + + public SqlitePagingQueryDecorator(SelectQuery innerQuery, int skip, int take) + { + if (string.IsNullOrEmpty(innerQuery.OrderBy.ToString())) + { + throw new DataMappingException("A paged query must specify an order by clause."); + } + + _innerQuery = innerQuery; + _skip = skip; + _take = take; + } + + public string Generate() + { + if (_innerQuery.IsView || _innerQuery.IsJoin) + { + return ComplexPaging(); + } + return SimplePaging(); + } + + private string SimplePaging() + { + // Create paged query + StringBuilder sql = new StringBuilder(); + + _innerQuery.BuildSelectClause(sql); + _innerQuery.BuildFromClause(sql); + _innerQuery.BuildJoinClauses(sql); + _innerQuery.BuildWhereClause(sql); + _innerQuery.BuildOrderClause(sql); + sql.AppendLine(String.Format(" LIMIT {0},{1}", _skip, _take)); + + return sql.ToString(); + } + + private string ComplexPaging() + { + var baseTable = _innerQuery.Tables.First(); + + + StringBuilder sql = new StringBuilder(); + + _innerQuery.BuildSelectClause(sql); + sql.Append(" FROM ("); + BuildSimpleInnerSelect(sql); + _innerQuery.BuildFromClause(sql); + _innerQuery.BuildJoinClauses(sql); + _innerQuery.BuildWhereClause(sql); + BuildGroupBy(sql); + BuildOrderBy(sql); + sql.AppendFormat(" LIMIT {0},{1}", _skip, _take); + sql.AppendFormat(") AS {0} ", _innerQuery.Dialect.CreateToken(baseTable.Alias)); + + _innerQuery.BuildJoinClauses(sql); + + return sql.ToString(); + } + + public void BuildSelectClause(StringBuilder sql) + { + List appended = new List(); + + sql.Append("SELECT "); + + int startIndex = sql.Length; + + // COLUMNS + foreach (Table join in _innerQuery.Tables) + { + for (int i = 0; i < join.Columns.Count; i++) + { + var c = join.Columns[i]; + + if (sql.Length > startIndex && sql[sql.Length - 1] != ',') + sql.Append(","); + + if (join is View) + { + string token = _innerQuery.Dialect.CreateToken(string.Concat(join.Alias, ".", _innerQuery.NameOrAltName(c.ColumnInfo))); + if (appended.Contains(token)) + continue; + + sql.Append(token); + appended.Add(token); + } + else + { + string token = string.Concat(join.Alias, ".", c.ColumnInfo.Name); + if (appended.Contains(token)) + continue; + + sql.Append(_innerQuery.Dialect.CreateToken(token)); + + if (_innerQuery.UseAltName && c.ColumnInfo.AltName != null && c.ColumnInfo.AltName != c.ColumnInfo.Name) + { + string altName = c.ColumnInfo.AltName; + sql.AppendFormat(" AS {0}", altName); + } + } + } + } + } + + private void BuildSimpleInnerSelect(StringBuilder sql) + { + sql.Append("SELECT "); + int startIndex = sql.Length; + + // COLUMNS + var join = _innerQuery.Tables.First(); + + for (int i = 0; i < join.Columns.Count; i++) + { + var c = join.Columns[i]; + + if (sql.Length > startIndex) + sql.Append(","); + + string token = string.Concat(join.Alias, ".", c.ColumnInfo.Name); + sql.Append(_innerQuery.Dialect.CreateToken(token)); + } + + } + + private void BuildOrderBy(StringBuilder sql) + { + sql.Append(_innerQuery.OrderBy.BuildQuery(false)); + } + + private void BuildGroupBy(StringBuilder sql) + { + var baseTable = _innerQuery.Tables.First(); + var primaryKeyColumn = baseTable.Columns.Single(c => c.ColumnInfo.IsPrimaryKey); + + string token = _innerQuery.Dialect.CreateToken(string.Concat(baseTable.Alias, ".", primaryKeyColumn.ColumnInfo.Name)); + sql.AppendFormat(" GROUP BY {0}", token); + } + } +} diff --git a/src/Marr.Data/QGen/SqliteRowCountQueryDecorator.cs b/src/Marr.Data/QGen/SqliteRowCountQueryDecorator.cs new file mode 100644 index 000000000..b88cac468 --- /dev/null +++ b/src/Marr.Data/QGen/SqliteRowCountQueryDecorator.cs @@ -0,0 +1,45 @@ +using System.Text; + +namespace Marr.Data.QGen +{ + public class SqliteRowCountQueryDecorator : IQuery + { + private SelectQuery _innerQuery; + + public SqliteRowCountQueryDecorator(SelectQuery innerQuery) + { + _innerQuery = innerQuery; + } + + public string Generate() + { + StringBuilder sql = new StringBuilder(); + + BuildSelectCountClause(sql); + + if (_innerQuery.IsJoin) + { + sql.Append(" FROM ("); + _innerQuery.BuildSelectClause(sql); + _innerQuery.BuildFromClause(sql); + _innerQuery.BuildJoinClauses(sql); + _innerQuery.BuildWhereClause(sql); + _innerQuery.BuildGroupBy(sql); + sql.Append(") "); + + return sql.ToString(); + } + + _innerQuery.BuildFromClause(sql); + _innerQuery.BuildJoinClauses(sql); + _innerQuery.BuildWhereClause(sql); + + return sql.ToString(); + } + + private void BuildSelectCountClause(StringBuilder sql) + { + sql.AppendLine("SELECT COUNT(*)"); + } + } +} \ No newline at end of file diff --git a/src/Marr.Data/QGen/Table.cs b/src/Marr.Data/QGen/Table.cs new file mode 100644 index 000000000..7d90a9178 --- /dev/null +++ b/src/Marr.Data/QGen/Table.cs @@ -0,0 +1,47 @@ +using System; +using Marr.Data.Mapping; + +namespace Marr.Data.QGen +{ + /// + /// This class represents a table in a query. + /// A table contains corresponding columns. + /// + public class Table + { + public Table(Type memberType) + : this(memberType, JoinType.None) + { } + + public Table(Type memberType, JoinType joinType) + { + EntityType = memberType; + Name = memberType.GetTableName(); + JoinType = joinType; + Columns = MapRepository.Instance.GetColumns(memberType); + } + + public bool IsBaseTable + { + get + { + return Alias == "t0"; + } + } + + public Type EntityType { get; private set; } + public virtual string Name { get; set; } + public JoinType JoinType { get; private set; } + public virtual ColumnMapCollection Columns { get; private set; } + public virtual string Alias { get; set; } + public string JoinClause { get; set; } + } + + public enum JoinType + { + None, + Inner, + Left, + Right + } +} diff --git a/src/Marr.Data/QGen/TableCollection.cs b/src/Marr.Data/QGen/TableCollection.cs new file mode 100644 index 000000000..75caeaa90 --- /dev/null +++ b/src/Marr.Data/QGen/TableCollection.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Collections; + +namespace Marr.Data.QGen +{ + /// + /// This class holds a collection of Table objects. + /// + public class TableCollection : IEnumerable + { + private List
_tables; + + public TableCollection() + { + _tables = new List
(); + } + + public void Add(Table table) + { + if (this.Any(t => t.EntityType == table.EntityType)) + { + // Already exists -- don't add + //return; This prevents joining on the same table! + } + + // Create an alias (ex: "t0", "t1", "t2", etc...) + table.Alias = string.Format("t{0}", _tables.Count); + _tables.Add(table); + } + + public void ReplaceBaseTable(View view) + { + _tables.RemoveAt(0); + Add(view); + } + + /// + /// Tries to find a table for a given member. + /// + public Table FindTable(Type declaringType) + { + return EnumerateViewsAndTables().Where(t => t.EntityType == declaringType).FirstOrDefault(); + } + + public Table this[int index] + { + get + { + return _tables[index]; + } + } + + public int Count + { + get + { + return _tables.Count; + } + } + + /// + /// Recursively enumerates through all tables, including tables embedded in views. + /// + /// + public IEnumerable
EnumerateViewsAndTables() + { + foreach (Table table in _tables) + { + if (table is View) + { + foreach (Table viewTable in (table as View)) + { + yield return viewTable; + } + } + else + { + yield return table; + } + } + } + + public IEnumerator
GetEnumerator() + { + return _tables.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _tables.GetEnumerator(); + } + } +} diff --git a/src/Marr.Data/QGen/UpdateQuery.cs b/src/Marr.Data/QGen/UpdateQuery.cs new file mode 100644 index 000000000..42d5c7986 --- /dev/null +++ b/src/Marr.Data/QGen/UpdateQuery.cs @@ -0,0 +1,56 @@ +using System.Text; +using System.Data.Common; +using Marr.Data.Mapping; +using Marr.Data.QGen.Dialects; + +namespace Marr.Data.QGen +{ + public class UpdateQuery : IQuery + { + protected Dialect Dialect { get; set; } + protected string Target { get; set; } + protected ColumnMapCollection Columns { get; set; } + protected DbCommand Command { get; set; } + protected string WhereClause { get; set; } + + public UpdateQuery(Dialect dialect, ColumnMapCollection columns, DbCommand command, string target, string whereClause) + { + Dialect = dialect; + Target = target; + Columns = columns; + Command = command; + WhereClause = whereClause; + } + + public string Generate() + { + StringBuilder sql = new StringBuilder(); + + sql.AppendFormat("UPDATE {0} SET ", Dialect.CreateToken(Target)); + + int startIndex = sql.Length; + + foreach (DbParameter p in Command.Parameters) + { + var c = Columns.GetByColumnName(p.ParameterName); + + if (c == null) + break; // All SET columns have been added + + if (sql.Length > startIndex) + sql.Append(","); + + if (!c.ColumnInfo.IsAutoIncrement) + { + sql.AppendFormat("{0}={1}{2}", Dialect.CreateToken(c.ColumnInfo.Name), Command.ParameterPrefix(), p.ParameterName); + } + } + + sql.AppendFormat(" {0}", WhereClause); + + return sql.ToString(); + } + + + } +} diff --git a/src/Marr.Data/QGen/UpdateQueryBuilder.cs b/src/Marr.Data/QGen/UpdateQueryBuilder.cs new file mode 100644 index 000000000..6e12d76ae --- /dev/null +++ b/src/Marr.Data/QGen/UpdateQueryBuilder.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using Marr.Data.Mapping; +using System.Linq.Expressions; +using Marr.Data.QGen.Dialects; + +namespace Marr.Data.QGen +{ + public class UpdateQueryBuilder + { + private DataMapper _db; + private string _tableName; + private T _entity; + private MappingHelper _mappingHelper; + private ColumnMapCollection _mappings; + private SqlModes _previousSqlMode; + private bool _generateQuery = true; + private TableCollection _tables; + private Expression> _filterExpression; + private Dialect _dialect; + private ColumnMapCollection _columnsToUpdate; + + public UpdateQueryBuilder() + { + // Used only for unit testing with mock frameworks + } + + public UpdateQueryBuilder(DataMapper db) + { + _db = db; + _tableName = MapRepository.Instance.GetTableName(typeof(T)); + _tables = new TableCollection(); + _tables.Add(new Table(typeof(T))); + _previousSqlMode = _db.SqlMode; + _mappingHelper = new MappingHelper(_db); + _mappings = MapRepository.Instance.GetColumns(typeof(T)); + _dialect = QueryFactory.CreateDialect(_db); + } + + public virtual UpdateQueryBuilder TableName(string tableName) + { + _tableName = tableName; + return this; + } + + public virtual UpdateQueryBuilder QueryText(string queryText) + { + _generateQuery = false; + _db.Command.CommandText = queryText; + return this; + } + + public virtual UpdateQueryBuilder Entity(T entity) + { + _entity = entity; + return this; + } + + public virtual UpdateQueryBuilder Where(Expression> filterExpression) + { + _filterExpression = filterExpression; + return this; + } + + public virtual UpdateQueryBuilder ColumnsIncluding(params Expression>[] properties) + { + List columnList = new List(); + + foreach (var column in properties) + { + columnList.Add(column.GetMemberName()); + } + + return ColumnsIncluding(columnList.ToArray()); + } + + public virtual UpdateQueryBuilder ColumnsIncluding(params string[] properties) + { + _columnsToUpdate = new ColumnMapCollection(); + + foreach (string propertyName in properties) + { + _columnsToUpdate.Add(_mappings.GetByFieldName(propertyName)); + } + + return this; + } + + public virtual UpdateQueryBuilder ColumnsExcluding(params Expression>[] properties) + { + List columnList = new List(); + + foreach (var column in properties) + { + columnList.Add(column.GetMemberName()); + } + + return ColumnsExcluding(columnList.ToArray()); + } + + public virtual UpdateQueryBuilder ColumnsExcluding(params string[] properties) + { + _columnsToUpdate = new ColumnMapCollection(); + + _columnsToUpdate.AddRange(_mappings); + + foreach (string propertyName in properties) + { + _columnsToUpdate.RemoveAll(c => c.FieldName == propertyName); + } + + return this; + } + + public virtual string BuildQuery() + { + if (_entity == null) + throw new ArgumentNullException("You must specify an entity to update."); + + // Override SqlMode since we know this will be a text query + _db.SqlMode = SqlModes.Text; + + var columnsToUpdate = _columnsToUpdate ?? _mappings; + + _mappingHelper.CreateParameters(_entity, columnsToUpdate, _generateQuery); + + string where = string.Empty; + if (_filterExpression != null) + { + var whereBuilder = new WhereBuilder(_db.Command, _dialect, _filterExpression, _tables, false, false); + where = whereBuilder.ToString(); + } + + IQuery query = QueryFactory.CreateUpdateQuery(columnsToUpdate, _db, _tableName, where); + + _db.Command.CommandText = query.Generate(); + + return _db.Command.CommandText; + } + + public virtual int Execute() + { + if (_generateQuery) + { + BuildQuery(); + } + else + { + _mappingHelper.CreateParameters(_entity, _mappings, _generateQuery); + } + + int rowsAffected = 0; + + try + { + _db.OpenConnection(); + rowsAffected = _db.Command.ExecuteNonQuery(); + _mappingHelper.SetOutputValues(_entity, _mappings.OutputFields); + } + finally + { + _db.CloseConnection(); + } + + + if (_generateQuery) + { + // Return to previous sql mode + _db.SqlMode = _previousSqlMode; + } + + return rowsAffected; + } + } +} diff --git a/src/Marr.Data/QGen/View.cs b/src/Marr.Data/QGen/View.cs new file mode 100644 index 000000000..f750c2e06 --- /dev/null +++ b/src/Marr.Data/QGen/View.cs @@ -0,0 +1,90 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Marr.Data.Mapping; + +namespace Marr.Data.QGen +{ + /// + /// This class represents a View. A view can hold multiple tables (and their columns). + /// + public class View : Table, IEnumerable
+ { + private string _viewName; + private Table[] _tables; + private ColumnMapCollection _columns; + + public View(string viewName, Table[] tables) + : base(tables[0].EntityType, JoinType.None) + { + _viewName = viewName; + _tables = tables; + } + + public Table[] Tables + { + get { return _tables; } + } + + public override string Name + { + get + { + return _viewName; + } + set + { + _viewName = value; + } + } + + public override string Alias + { + get + { + return base.Alias; + } + set + { + base.Alias = value; + + // Sync view tables + foreach (Table table in _tables) + { + table.Alias = value; + } + } + } + + /// + /// Gets all the columns from all the tables included in the view. + /// + public override ColumnMapCollection Columns + { + get + { + if (_columns == null) + { + var allColumns = _tables.SelectMany(t => t.Columns); + _columns = new ColumnMapCollection(); + _columns.AddRange(allColumns); + } + + return _columns; + } + } + + public IEnumerator
GetEnumerator() + { + foreach (Table table in _tables) + { + yield return table; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Marr.Data/QGen/WhereBuilder.cs b/src/Marr.Data/QGen/WhereBuilder.cs new file mode 100644 index 000000000..0a7c217f7 --- /dev/null +++ b/src/Marr.Data/QGen/WhereBuilder.cs @@ -0,0 +1,325 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq.Expressions; +using System.Data.Common; +using Marr.Data.Parameters; +using System.Reflection; +using Marr.Data.QGen.Dialects; + +namespace Marr.Data.QGen +{ + /// + /// This class utilizes the ExpressionVisitor base class, and it is responsible for creating the "WHERE" clause. + /// It builds a protected StringBuilder class whose output is created when the ToString method is called. + /// It also has some methods that coincide with Linq methods, to provide Linq compatibility. + /// + /// + public class WhereBuilder : ExpressionVisitor + { + private string _constantWhereClause; + private MapRepository _repos; + private DbCommand _command; + private string _paramPrefix; + private bool _isLeftSide = true; + protected bool _useAltName; + protected Dialect _dialect; + protected StringBuilder _sb; + protected TableCollection _tables; + protected bool _tablePrefix; + + public WhereBuilder(string whereClause, bool useAltName) + { + _constantWhereClause = whereClause; + _useAltName = useAltName; + } + + public WhereBuilder(DbCommand command, Dialect dialect, Expression filter, TableCollection tables, bool useAltName, bool tablePrefix) + { + _repos = MapRepository.Instance; + _command = command; + _dialect = dialect; + _paramPrefix = command.ParameterPrefix(); + _sb = new StringBuilder(); + _useAltName = useAltName; + _tables = tables; + _tablePrefix = tablePrefix; + + if (filter != null) + { + _sb.AppendFormat("{0} ", PrefixText); + base.Visit(filter); + } + } + + protected virtual string PrefixText + { + get + { + return "WHERE"; + } + } + + protected override Expression VisitBinary(BinaryExpression expression) + { + _sb.Append("("); + + _isLeftSide = true; + Visit(expression.Left); + + _sb.AppendFormat(" {0} ", Decode(expression)); + + _isLeftSide = false; + Visit(expression.Right); + + _sb.Append(")"); + + return expression; + } + + protected override Expression VisitMethodCall(MethodCallExpression expression) + { + string method = (expression as MethodCallExpression).Method.Name; + switch (method) + { + case "Contains": + Write_Contains(expression); + break; + + case "StartsWith": + Write_StartsWith(expression); + break; + + case "EndsWith": + Write_EndsWith(expression); + break; + + case "In": + Write_In(expression); + break; + + default: + string msg = string.Format("'{0}' expressions are not yet implemented in the where clause expression tree parser.", method); + throw new NotImplementedException(msg); + } + + return expression; + } + + protected override Expression VisitMemberAccess(MemberExpression expression) + { + if (_isLeftSide) + { + string fqColumn = GetFullyQualifiedColumnName(expression.Member, expression.Expression.Type); + _sb.Append(fqColumn); + } + else + { + // Add parameter to Command.Parameters + string paramName = string.Concat(_paramPrefix, "P", _command.Parameters.Count.ToString()); + _sb.Append(paramName); + + object value = GetRightValue(expression); + new ParameterChainMethods(_command, paramName, value); + } + + return expression; + } + + protected override Expression VisitConstant(ConstantExpression expression) + { + if (expression.Value != null) + { + // Add parameter to Command.Parameters + string paramName = string.Concat(_paramPrefix, "P", _command.Parameters.Count.ToString()); + + _sb.Append(paramName); + + var parameter = new ParameterChainMethods(_command, paramName, expression.Value).Parameter; + } + else + { + _sb.Append("NULL"); + } + + return expression; + } + + private object GetRightValue(Expression expression) + { + object rightValue = null; + + var simpleConstExp = expression as ConstantExpression; + if (simpleConstExp == null) // Value is not directly passed in as a constant + { + MemberExpression memberExp = expression as MemberExpression; + ConstantExpression constExp = null; + + // Value may be nested in multiple levels of objects/properties, so traverse the MemberExpressions + // until a ConstantExpression property value is found, and then unwind the stack to get the value. + var memberNames = new Stack(); + + while (memberExp != null) + { + memberNames.Push(memberExp.Member.Name); + + // Function calls are not supported - user needs to simplify their Where expression. + var methodExp = memberExp.Expression as MethodCallExpression; + if (methodExp != null) + { + var errMsg = string.Format("Function calls are not supported by the Where clause expression parser. Please evaluate your function call, '{0}', manually and then use the resulting paremeter value in your Where expression.", methodExp.Method.Name); + throw new NotSupportedException(errMsg); + } + + constExp = memberExp.Expression as ConstantExpression; + memberExp = memberExp.Expression as MemberExpression; + } + + object entity = constExp.Value; + while (memberNames.Count > 0) + { + string entityName = memberNames.Pop(); + entity = _repos.ReflectionStrategy.GetFieldValue(entity, entityName); + } + rightValue = entity; + } + else // Value is passed in directly as a constant + { + rightValue = simpleConstExp.Value; + } + + return rightValue; + } + + protected string GetFullyQualifiedColumnName(MemberInfo member, Type declaringType) + { + if (_tablePrefix) + { + Table table = _tables.FindTable(declaringType); + + if (table == null) + { + string msg = string.Format("The property '{0} -> {1}' you are trying to reference in the 'WHERE' statement belongs to an entity that has not been joined in your query. To reference this property, you must join the '{0}' entity using the Join method.", + declaringType, + member.Name); + + throw new DataMappingException(msg); + } + + string columnName = DataHelper.GetColumnName(declaringType, member.Name, _useAltName); + return _dialect.CreateToken(string.Format("{0}.{1}", table.Alias, columnName)); + } + else + { + string columnName = DataHelper.GetColumnName(declaringType, member.Name, _useAltName); + return _dialect.CreateToken(columnName); + } + } + + private string Decode(BinaryExpression expression) + { + bool isRightSideNullConstant = expression.Right.NodeType == + ExpressionType.Constant && + ((ConstantExpression)expression.Right).Value == null; + + if (isRightSideNullConstant) + { + switch (expression.NodeType) + { + case ExpressionType.Equal: return "IS"; + case ExpressionType.NotEqual: return "IS NOT"; + } + } + + switch (expression.NodeType) + { + case ExpressionType.AndAlso: return "AND"; + case ExpressionType.And: return "AND"; + case ExpressionType.Equal: return "="; + case ExpressionType.GreaterThan: return ">"; + case ExpressionType.GreaterThanOrEqual: return ">="; + case ExpressionType.LessThan: return "<"; + case ExpressionType.LessThanOrEqual: return "<="; + case ExpressionType.NotEqual: return "<>"; + case ExpressionType.OrElse: return "OR"; + case ExpressionType.Or: return "OR"; + default: throw new NotSupportedException(string.Format("{0} statement is not supported", expression.NodeType.ToString())); + } + } + + private void Write_Contains(MethodCallExpression body) + { + // Add parameter to Command.Parameters + object value = GetRightValue(body.Arguments[0]); + string paramName = string.Concat(_paramPrefix, "P", _command.Parameters.Count.ToString()); + var parameter = new ParameterChainMethods(_command, paramName, value).Parameter; + + MemberExpression memberExp = (body.Object as MemberExpression); + string fqColumn = GetFullyQualifiedColumnName(memberExp.Member, memberExp.Expression.Type); + _sb.AppendFormat(_dialect.ContainsFormat, fqColumn, paramName); + } + + private void Write_In(MethodCallExpression body) + { + object value = GetRightValue(body.Arguments[1]); + //string paramName = string.Concat(_paramPrefix, "P", _command.Parameters.Count.ToString()); + //var parameter = new ParameterChainMethods(_command, paramName, value).Parameter; + + MemberExpression memberExp = (body.Arguments[0] as MemberExpression); + string fqColumn = GetFullyQualifiedColumnName(memberExp.Member, memberExp.Expression.Type); + _sb.AppendFormat(_dialect.InFormat, fqColumn, string.Join(",", value as List)); + } + + private void Write_StartsWith(MethodCallExpression body) + { + // Add parameter to Command.Parameters + object value = GetRightValue(body.Arguments[0]); + string paramName = string.Concat(_paramPrefix, "P", _command.Parameters.Count.ToString()); + var parameter = new ParameterChainMethods(_command, paramName, value).Parameter; + + MemberExpression memberExp = (body.Object as MemberExpression); + string fqColumn = GetFullyQualifiedColumnName(memberExp.Member, memberExp.Expression.Type); + _sb.AppendFormat(_dialect.StartsWithFormat, fqColumn, paramName); + } + + private void Write_EndsWith(MethodCallExpression body) + { + // Add parameter to Command.Parameters + object value = GetRightValue(body.Arguments[0]); + string paramName = string.Concat(_paramPrefix, "P", _command.Parameters.Count.ToString()); + var parameter = new ParameterChainMethods(_command, paramName, value).Parameter; + + MemberExpression memberExp = (body.Object as MemberExpression); + string fqColumn = GetFullyQualifiedColumnName(memberExp.Member, memberExp.Expression.Type); + _sb.AppendFormat(_dialect.EndsWithFormat, fqColumn, paramName); + } + + /// + /// Appends the current where clause with another where clause. + /// + /// The second where clause that is being appended. + /// AND / OR + internal void Append(WhereBuilder where, WhereAppendType appendType) + { + _constantWhereClause = string.Format("{0} {1} {2}", + ToString(), + appendType.ToString(), + where.ToString().Replace("WHERE ", string.Empty)); + } + + public override string ToString() + { + if (string.IsNullOrEmpty(_constantWhereClause)) + { + return _sb.ToString(); + } + return _constantWhereClause; + } + } + + internal enum WhereAppendType + { + AND, + OR + } +} diff --git a/src/Marr.Data/Reflection/IReflectionStrategy.cs b/src/Marr.Data/Reflection/IReflectionStrategy.cs new file mode 100644 index 000000000..e73a0bc8f --- /dev/null +++ b/src/Marr.Data/Reflection/IReflectionStrategy.cs @@ -0,0 +1,17 @@ +using System; + +namespace Marr.Data.Reflection +{ + public interface IReflectionStrategy + { + object GetFieldValue(object entity, string fieldName); + + GetterDelegate BuildGetter(Type type, string memberName); + SetterDelegate BuildSetter(Type type, string memberName); + + object CreateInstance(Type type); + } + + public delegate void SetterDelegate(object instance, object value); + public delegate object GetterDelegate(object instance); +} diff --git a/src/Marr.Data/Reflection/ReflectionHelper.cs b/src/Marr.Data/Reflection/ReflectionHelper.cs new file mode 100644 index 000000000..c8ca97308 --- /dev/null +++ b/src/Marr.Data/Reflection/ReflectionHelper.cs @@ -0,0 +1,67 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +using System; +using System.Reflection; + +namespace Marr.Data.Reflection +{ + public class ReflectionHelper + { + /// + /// Converts a DBNull.Value to a null for a reference field, + /// or the default value of a value field. + /// + /// + /// + public static object GetDefaultValue(Type fieldType) + { + if (fieldType.IsGenericType) + { + return null; + } + if (fieldType.IsValueType) + { + return Activator.CreateInstance(fieldType); + } + return null; + } + + /// + /// Gets the CLR data type of a MemberInfo. + /// If the type is nullable, returns the underlying type. + /// + /// + /// + public static Type GetMemberType(MemberInfo member) + { + Type memberType = null; + if (member.MemberType == MemberTypes.Property) + memberType = (member as PropertyInfo).PropertyType; + else if (member.MemberType == MemberTypes.Field) + memberType = (member as FieldInfo).FieldType; + else + memberType = typeof(object); + + // Handle nullable types - get underlying type + if (memberType.IsGenericType && memberType.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + memberType = memberType.GetGenericArguments()[0]; + } + + return memberType; + } + } +} diff --git a/src/Marr.Data/Reflection/SimpleReflectionStrategy.cs b/src/Marr.Data/Reflection/SimpleReflectionStrategy.cs new file mode 100644 index 000000000..9e741b013 --- /dev/null +++ b/src/Marr.Data/Reflection/SimpleReflectionStrategy.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; + +namespace Marr.Data.Reflection +{ + public class SimpleReflectionStrategy : IReflectionStrategy + { + + private static readonly Dictionary MemberCache = new Dictionary(); + private static readonly Dictionary GetterCache = new Dictionary(); + private static readonly Dictionary SetterCache = new Dictionary(); + + + private static MemberInfo GetMember(Type entityType, string name) + { + MemberInfo member; + var key = entityType.FullName + name; + if (!MemberCache.TryGetValue(key, out member)) + { + member = entityType.GetMember(name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)[0]; + MemberCache[key] = member; + } + + return member; + } + + /// + /// Gets an entity field value by name. + /// + public object GetFieldValue(object entity, string fieldName) + { + var member = GetMember(entity.GetType(), fieldName); + + if (member.MemberType == MemberTypes.Field) + { + return (member as FieldInfo).GetValue(entity); + } + if (member.MemberType == MemberTypes.Property) + { + return BuildGetter(entity.GetType(), fieldName)(entity); + } + throw new DataMappingException(string.Format("The DataMapper could not get the value for {0}.{1}.", entity.GetType().Name, fieldName)); + } + + + /// + /// Instantiates a type using the FastReflector library for increased speed. + /// + /// + /// + public object CreateInstance(Type type) + { + return Activator.CreateInstance(type); + } + + + + + public GetterDelegate BuildGetter(Type type, string memberName) + { + GetterDelegate getter; + var key = type.FullName + memberName; + if (!GetterCache.TryGetValue(key, out getter)) + { + getter = GetPropertyGetter((PropertyInfo)GetMember(type, memberName)); + } + + return getter; + } + + public SetterDelegate BuildSetter(Type type, string memberName) + { + SetterDelegate setter; + var key = type.FullName + memberName; + if (!SetterCache.TryGetValue(key, out setter)) + { + setter = GetPropertySetter((PropertyInfo)GetMember(type, memberName)); + } + + return setter; + } + + + private static SetterDelegate GetPropertySetter(PropertyInfo propertyInfo) + { + var propertySetMethod = propertyInfo.GetSetMethod(); + if (propertySetMethod == null) return null; + +#if NO_EXPRESSIONS + return (o, convertedValue) => + { + propertySetMethod.Invoke(o, new[] { convertedValue }); + return; + }; +#else + var instance = Expression.Parameter(typeof(object), "i"); + var argument = Expression.Parameter(typeof(object), "a"); + + var instanceParam = Expression.Convert(instance, propertyInfo.DeclaringType); + var valueParam = Expression.Convert(argument, propertyInfo.PropertyType); + + var setterCall = Expression.Call(instanceParam, propertyInfo.GetSetMethod(), valueParam); + + return Expression.Lambda(setterCall, instance, argument).Compile(); +#endif + } + + private static GetterDelegate GetPropertyGetter(PropertyInfo propertyInfo) + { + + var getMethodInfo = propertyInfo.GetGetMethod(); + if (getMethodInfo == null) return null; + +#if NO_EXPRESSIONS + return o => propertyInfo.GetGetMethod().Invoke(o, new object[] { }); +#else + try + { + var oInstanceParam = Expression.Parameter(typeof(object), "oInstanceParam"); + var instanceParam = Expression.Convert(oInstanceParam, propertyInfo.DeclaringType); + + var exprCallPropertyGetFn = Expression.Call(instanceParam, getMethodInfo); + var oExprCallPropertyGetFn = Expression.Convert(exprCallPropertyGetFn, typeof(object)); + + var propertyGetFn = Expression.Lambda + ( + oExprCallPropertyGetFn, + oInstanceParam + ).Compile(); + + return propertyGetFn; + + } + catch (Exception ex) + { + Console.Write(ex.Message); + throw; + } +#endif + } + + } +} diff --git a/src/Marr.Data/SqlModesEnum.cs b/src/Marr.Data/SqlModesEnum.cs new file mode 100644 index 000000000..d9382bb93 --- /dev/null +++ b/src/Marr.Data/SqlModesEnum.cs @@ -0,0 +1,23 @@ +/* Copyright (C) 2008 - 2011 Jordan Marr + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 3 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library. If not, see . */ + +namespace Marr.Data +{ + public enum SqlModes + { + StoredProcedure, + Text + } +} diff --git a/src/Marr.Data/UnitOfWork.cs b/src/Marr.Data/UnitOfWork.cs new file mode 100644 index 000000000..7e071c77c --- /dev/null +++ b/src/Marr.Data/UnitOfWork.cs @@ -0,0 +1,127 @@ +using System; +using System.Data; +using System.Runtime.Serialization; + +namespace Marr.Data +{ + /// + /// The UnitOfWork class can be used to manage the lifetime of an IDataMapper, from creation to disposal. + /// When used in a "using" statement, the UnitOfWork will create and dispose an IDataMapper. + /// When the SharedContext property is used in a "using" statement, + /// it will create a parent unit of work that will share a single IDataMapper with other units of work, + /// and the IDataMapper will not be disposed until the shared context is disposed. + /// If more than one shared context is created, the IDataMapper will be disposed when the outer most + /// shared context is disposed. + /// + /// + /// It should be noted that the Dispose method on the UnitOfWork class only affects the managed IDataMapper. + /// The UnitOfWork instance itself is not affected by the Dispose method. + /// + public class UnitOfWork : IDisposable + { + private Func _dbConstructor; + private IDataMapper _lazyLoadedDB; + private short _transactionCount; + + public UnitOfWork(Func dbConstructor) + { + _dbConstructor = dbConstructor; + } + + /// + /// Gets an IDataMapper object whose lifetime is managed by the UnitOfWork class. + /// + public IDataMapper DB + { + get + { + if (_lazyLoadedDB == null) + { + _lazyLoadedDB = _dbConstructor.Invoke(); + } + + return _lazyLoadedDB; + } + } + + /// + /// Instructs the UnitOfWork to share a single IDataMapper instance. + /// + public UnitOfWorkSharedContext SharedContext + { + get + { + return new UnitOfWorkSharedContext(this); + } + } + + public void BeginTransaction(IsolationLevel isolationLevel) + { + // Only allow one transaction to begin + if (_transactionCount < 1) + { + DB.BeginTransaction(isolationLevel); + } + + _transactionCount++; + } + + public void Commit() + { + // Only allow the outermost transaction to commit (all nested transactions must succeed) + if (_transactionCount == 1) + { + DB.Commit(); + } + + _transactionCount--; + } + + public void RollBack() + { + // Any level transaction should be allowed to rollback + DB.RollBack(); + + // Throw an exception if a nested ShareContext transaction rolls back + if (_transactionCount > 1) + { + throw new NestedSharedContextRollBackException(); + } + + _transactionCount--; + } + + public void Dispose() + { + if (!IsShared) + { + ForceDispose(); + } + } + + internal bool IsShared { get; set; } + + private void ForceDispose() + { + _transactionCount = 0; + + if (_lazyLoadedDB != null) + { + _lazyLoadedDB.Dispose(); + _lazyLoadedDB = null; + } + } + } + + [Serializable] + public class NestedSharedContextRollBackException : Exception + { + public NestedSharedContextRollBackException() { } + public NestedSharedContextRollBackException(string message) : base(message) { } + public NestedSharedContextRollBackException(string message, Exception inner) : base(message, inner) { } + protected NestedSharedContextRollBackException( + SerializationInfo info, + StreamingContext context) + : base(info, context) { } + } +} diff --git a/src/Marr.Data/UnitOfWorkSharedContext.cs b/src/Marr.Data/UnitOfWorkSharedContext.cs new file mode 100644 index 000000000..9d92f1b6e --- /dev/null +++ b/src/Marr.Data/UnitOfWorkSharedContext.cs @@ -0,0 +1,38 @@ +using System; + +namespace Marr.Data +{ + /// + /// Works in conjunction with the UnitOfWork to create a new + /// shared context that will preserve a single IDataMapper. + /// + public class UnitOfWorkSharedContext : IDisposable + { + private UnitOfWork _mgr; + private bool _isParentContext; + + public UnitOfWorkSharedContext(UnitOfWork mgr) + { + _mgr = mgr; + + if (_mgr.IsShared) + { + _isParentContext = false; + } + else + { + _isParentContext = true; + _mgr.IsShared = true; + } + } + + public void Dispose() + { + if (_isParentContext) + { + _mgr.IsShared = false; + _mgr.Dispose(); + } + } + } +} diff --git a/src/Radarr.sln b/src/Radarr.sln index 23dd99291..ee2f13e32 100644 --- a/src/Radarr.sln +++ b/src/Radarr.sln @@ -9,6 +9,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test.Common", "Test.Common" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WindowsServiceHelpers", "WindowsServiceHelpers", "{F9E67978-5CD6-4A5F-827B-4249711C0B02}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ServiceHelpers", "ServiceHelpers", "{CF5BF374-71E4-485E-A74C-39B581323D9A}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Host", "Host", "{486ADF86-DD89-4E19-B805-9D94F19800D9}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "External", "External", "{F6E3A728-AE77-4D02-BAC8-82FBC1402DDA}" @@ -23,6 +25,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Radarr.Api.V3", "Radarr.Api EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Radarr.Http", "Radarr.Http\Radarr.Http.csproj", "{F8A02FD4-A7A4-40D0-BB81-6319105A3302}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Marr.Data", "Marr.Data\Marr.Data.csproj", "{8D7D5F17-96BB-4EDD-A6E6-3BCA2D6B401A}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonoTorrent", "MonoTorrent\MonoTorrent.csproj", "{BE8533CC-A1ED-46A6-811F-2FA29CC6AD80}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Radarr.Api.Test", "NzbDrone.Api.Test\Radarr.Api.Test.csproj", "{E2EA47B1-6996-417D-A6EC-28C4F202715C}" @@ -103,6 +107,14 @@ Global {F8A02FD4-A7A4-40D0-BB81-6319105A3302}.Release|Posix.Build.0 = Release|Any CPU {F8A02FD4-A7A4-40D0-BB81-6319105A3302}.Release|Windows.ActiveCfg = Release|Any CPU {F8A02FD4-A7A4-40D0-BB81-6319105A3302}.Release|Windows.Build.0 = Release|Any CPU + {8D7D5F17-96BB-4EDD-A6E6-3BCA2D6B401A}.Debug|Posix.ActiveCfg = Debug|Any CPU + {8D7D5F17-96BB-4EDD-A6E6-3BCA2D6B401A}.Debug|Posix.Build.0 = Debug|Any CPU + {8D7D5F17-96BB-4EDD-A6E6-3BCA2D6B401A}.Debug|Windows.ActiveCfg = Debug|Any CPU + {8D7D5F17-96BB-4EDD-A6E6-3BCA2D6B401A}.Debug|Windows.Build.0 = Debug|Any CPU + {8D7D5F17-96BB-4EDD-A6E6-3BCA2D6B401A}.Release|Posix.ActiveCfg = Release|Any CPU + {8D7D5F17-96BB-4EDD-A6E6-3BCA2D6B401A}.Release|Posix.Build.0 = Release|Any CPU + {8D7D5F17-96BB-4EDD-A6E6-3BCA2D6B401A}.Release|Windows.ActiveCfg = Release|Any CPU + {8D7D5F17-96BB-4EDD-A6E6-3BCA2D6B401A}.Release|Windows.Build.0 = Release|Any CPU {BE8533CC-A1ED-46A6-811F-2FA29CC6AD80}.Debug|Posix.ActiveCfg = Debug|Any CPU {BE8533CC-A1ED-46A6-811F-2FA29CC6AD80}.Debug|Posix.Build.0 = Debug|Any CPU {BE8533CC-A1ED-46A6-811F-2FA29CC6AD80}.Debug|Windows.ActiveCfg = Debug|Any CPU @@ -300,6 +312,7 @@ Global GlobalSection(NestedProjects) = preSolution {47697CDB-27B6-4B05-B4F8-0CBE6F6EDF97} = {57A04B72-8088-4F75-A582-1158CF8291F7} {4EACDBBC-BCD7-4765-A57B-3E08331E4749} = {57A04B72-8088-4F75-A582-1158CF8291F7} + {8D7D5F17-96BB-4EDD-A6E6-3BCA2D6B401A} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} {BE8533CC-A1ED-46A6-811F-2FA29CC6AD80} = {F6E3A728-AE77-4D02-BAC8-82FBC1402DDA} {E2EA47B1-6996-417D-A6EC-28C4F202715C} = {57A04B72-8088-4F75-A582-1158CF8291F7} {2356C987-F992-4084-9DA2-5DAD1DA35E85} = {57A04B72-8088-4F75-A582-1158CF8291F7}