diff options
author | Rob Mensching <rob@firegiant.com> | 2021-05-11 07:36:37 -0700 |
---|---|---|
committer | Rob Mensching <rob@firegiant.com> | 2021-05-11 07:36:37 -0700 |
commit | 3f583916719eeef598d10a5d4e14ef14f008243b (patch) | |
tree | 3d528e0ddb5c0550954217c97059d2f19cd6152a /src/dtf/WixToolset.Dtf.WindowsInstaller.Linq | |
parent | 2e5ab696b8b4666d551b2a0532b95fb7fe6dbe03 (diff) | |
download | wix-3f583916719eeef598d10a5d4e14ef14f008243b.tar.gz wix-3f583916719eeef598d10a5d4e14ef14f008243b.tar.bz2 wix-3f583916719eeef598d10a5d4e14ef14f008243b.zip |
Merge Dtf
Diffstat (limited to 'src/dtf/WixToolset.Dtf.WindowsInstaller.Linq')
8 files changed, 2240 insertions, 0 deletions
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/AssemblyInfo.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/AssemblyInfo.cs new file mode 100644 index 00000000..94abf1dc --- /dev/null +++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/AssemblyInfo.cs | |||
@@ -0,0 +1,6 @@ | |||
1 | // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. | ||
2 | |||
3 | using System.Diagnostics.CodeAnalysis; | ||
4 | |||
5 | [assembly: SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Scope = "member", Target = "WixToolset.Dtf.WindowsInstaller.Linq.QTable`1.System.Linq.IQueryable<TRecord>.CreateQuery(System.Linq.Expressions.Expression):System.Linq.IQueryable`1<TElement>")] | ||
6 | [assembly: SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter", Scope = "member", Target = "WixToolset.Dtf.WindowsInstaller.Linq.QTable`1.System.Linq.IQueryable<TRecord>.Execute(System.Linq.Expressions.Expression):TResult")] | ||
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Attributes.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Attributes.cs new file mode 100644 index 00000000..60008bc8 --- /dev/null +++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Attributes.cs | |||
@@ -0,0 +1,60 @@ | |||
1 | // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. | ||
2 | |||
3 | namespace WixToolset.Dtf.WindowsInstaller.Linq | ||
4 | { | ||
5 | using System; | ||
6 | |||
7 | /// <summary> | ||
8 | /// Apply to a subclass of QRecord to indicate the name of | ||
9 | /// the table the record type is to be used with. | ||
10 | /// </summary> | ||
11 | /// <remarks> | ||
12 | /// If this attribute is not used on a record type, the default | ||
13 | /// table name will be derived from the record type name. (An | ||
14 | /// optional underscore suffix is stripped.) | ||
15 | /// </remarks> | ||
16 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] | ||
17 | public class DatabaseTableAttribute : Attribute | ||
18 | { | ||
19 | /// <summary> | ||
20 | /// Creates a new DatabaseTableAttribute for the specified table. | ||
21 | /// </summary> | ||
22 | /// <param name="table">name of the table associated with the record type</param> | ||
23 | public DatabaseTableAttribute(string table) | ||
24 | { | ||
25 | this.Table = table; | ||
26 | } | ||
27 | |||
28 | /// <summary> | ||
29 | /// Gets or sets the table associated with the record type. | ||
30 | /// </summary> | ||
31 | public string Table { get; set; } | ||
32 | } | ||
33 | |||
34 | /// <summary> | ||
35 | /// Apply to a property on a subclass of QRecord to indicate | ||
36 | /// the name of the column the property is to be associated with. | ||
37 | /// </summary> | ||
38 | /// <remarks> | ||
39 | /// If this attribute is not used on a property, the default | ||
40 | /// column name will be the same as the property name. | ||
41 | /// </remarks> | ||
42 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] | ||
43 | public class DatabaseColumnAttribute : Attribute | ||
44 | { | ||
45 | /// <summary> | ||
46 | /// Creates a new DatabaseColumnAttribute which maps a | ||
47 | /// record property to a column. | ||
48 | /// </summary> | ||
49 | /// <param name="column">name of the column associated with the property</param> | ||
50 | public DatabaseColumnAttribute(string column) | ||
51 | { | ||
52 | this.Column = column; | ||
53 | } | ||
54 | |||
55 | /// <summary> | ||
56 | /// Gets or sets the column associated with the record property. | ||
57 | /// </summary> | ||
58 | public string Column { get; set; } | ||
59 | } | ||
60 | } | ||
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Entities.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Entities.cs new file mode 100644 index 00000000..1c51b861 --- /dev/null +++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Entities.cs | |||
@@ -0,0 +1,150 @@ | |||
1 | // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. | ||
2 | |||
3 | namespace WixToolset.Dtf.WindowsInstaller.Linq.Entities | ||
4 | { | ||
5 | // Silence warnings about style and doc-comments | ||
6 | #if !CODE_ANALYSIS | ||
7 | #pragma warning disable 1591 | ||
8 | #region Generated code | ||
9 | |||
10 | public class Component_ : QRecord | ||
11 | { | ||
12 | public string Component { get { return this[0]; } set { this[0] = value; } } | ||
13 | public string ComponentId { get { return this[1]; } set { this[1] = value; } } | ||
14 | public string Directory_ { get { return this[2]; } set { this[2] = value; } } | ||
15 | public string Condition { get { return this[4]; } set { this[4] = value; } } | ||
16 | public string KeyPath { get { return this[5]; } set { this[5] = value; } } | ||
17 | public ComponentAttributes Attributes | ||
18 | { get { return (ComponentAttributes) this.I(3); } set { this[3] = ((int) value).ToString(); } } | ||
19 | } | ||
20 | |||
21 | public class CreateFolder_ : QRecord | ||
22 | { | ||
23 | public string Directory_ { get { return this[0]; } set { this[0] = value; } } | ||
24 | public string Component_ { get { return this[1]; } set { this[1] = value; } } | ||
25 | } | ||
26 | |||
27 | public class CustomAction_ : QRecord | ||
28 | { | ||
29 | public string Action { get { return this[0]; } set { this[0] = value; } } | ||
30 | public string Source { get { return this[2]; } set { this[2] = value; } } | ||
31 | public string Target { get { return this[3]; } set { this[3] = value; } } | ||
32 | public CustomActionTypes Type | ||
33 | { get { return (CustomActionTypes) this.I(1); } set { this[1] = ((int) value).ToString(); } } | ||
34 | } | ||
35 | |||
36 | public class Directory_ : QRecord | ||
37 | { | ||
38 | public string Directory { get { return this[0]; } set { this[0] = value; } } | ||
39 | public string Directory_Parent { get { return this[1]; } set { this[1] = value; } } | ||
40 | public string DefaultDir { get { return this[2]; } set { this[2] = value; } } | ||
41 | } | ||
42 | |||
43 | public class DuplicateFile_ : QRecord | ||
44 | { | ||
45 | public string FileKey { get { return this[0]; } set { this[0] = value; } } | ||
46 | public string Component_ { get { return this[1]; } set { this[1] = value; } } | ||
47 | public string File_ { get { return this[2]; } set { this[2] = value; } } | ||
48 | public string DestName { get { return this[4]; } set { this[4] = value; } } | ||
49 | public string DestFolder { get { return this[5]; } set { this[5] = value; } } | ||
50 | } | ||
51 | |||
52 | public class Feature_ : QRecord | ||
53 | { | ||
54 | public string Feature { get { return this[0]; } set { this[0] = value; } } | ||
55 | public string Feature_Parent { get { return this[1]; } set { this[1] = value; } } | ||
56 | public string Title { get { return this[2]; } set { this[2] = value; } } | ||
57 | public string Description { get { return this[3]; } set { this[3] = value; } } | ||
58 | public int? Display { get { return this.NI(4); } set { this[4] = value.ToString(); } } | ||
59 | public int Level { get { return this.I(5); } set { this[5] = value.ToString(); } } | ||
60 | public string Directory_ { get { return this[6]; } set { this[6] = value; } } | ||
61 | public FeatureAttributes Attributes | ||
62 | { get { return (FeatureAttributes) this.I(7); } set { this[7] = ((int) value).ToString(); } } | ||
63 | } | ||
64 | |||
65 | [DatabaseTable("FeatureComponents")] | ||
66 | public class FeatureComponent_ : QRecord | ||
67 | { | ||
68 | public string Feature_ { get { return this[0]; } set { this[0] = value; } } | ||
69 | public string Component_ { get { return this[1]; } set { this[1] = value; } } | ||
70 | } | ||
71 | |||
72 | public class File_ : QRecord | ||
73 | { | ||
74 | public string File { get { return this[0]; } set { this[0] = value; } } | ||
75 | public string Component_ { get { return this[1]; } set { this[1] = value; } } | ||
76 | public string FileName { get { return this[2]; } set { this[2] = value; } } | ||
77 | public int FileSize { get { return this.I(3); } set { this[3] = value.ToString(); } } | ||
78 | public string Version { get { return this[4]; } set { this[4] = value; } } | ||
79 | public string Language { get { return this[5]; } set { this[5] = value; } } | ||
80 | public int Sequence { get { return this.I(7); } set { this[7] = value.ToString(); } } | ||
81 | public FileAttributes Attributes | ||
82 | { get { return (FileAttributes) this.I(6); } set { this[6] = ((int) value).ToString(); } } | ||
83 | } | ||
84 | |||
85 | [DatabaseTable("MsiFileHash")] | ||
86 | public class FileHash_ : QRecord | ||
87 | { | ||
88 | public string File_ { get { return this[0]; } set { this[0] = value; } } | ||
89 | public int Options { get { return this.I(1); } set { this[1] = value.ToString(); } } | ||
90 | public int HashPart1 { get { return this.I(2); } set { this[2] = value.ToString(); } } | ||
91 | public int HashPart2 { get { return this.I(3); } set { this[3] = value.ToString(); } } | ||
92 | public int HashPart3 { get { return this.I(4); } set { this[4] = value.ToString(); } } | ||
93 | public int HashPart4 { get { return this.I(5); } set { this[5] = value.ToString(); } } | ||
94 | } | ||
95 | |||
96 | [DatabaseTable("InstallExecuteSequence")] | ||
97 | public class InstallSequence_ : QRecord | ||
98 | { | ||
99 | public string Action { get { return this[0]; } set { this[0] = value; } } | ||
100 | public string Condition { get { return this[1]; } set { this[1] = value; } } | ||
101 | public int Sequence { get { return this.I(2); } set { this[2] = value.ToString(); } } | ||
102 | } | ||
103 | |||
104 | public class LaunchCondition_ : QRecord | ||
105 | { | ||
106 | public string Condition { get { return this[0]; } set { this[0] = value; } } | ||
107 | public string Description { get { return this[1]; } set { this[1] = value; } } | ||
108 | } | ||
109 | |||
110 | public class Media_ : QRecord | ||
111 | { | ||
112 | public int DiskId { get { return this.I(0); } set { this[0] = value.ToString(); } } | ||
113 | public int LastSequence { get { return this.I(1); } set { this[1] = value.ToString(); } } | ||
114 | public string DiskPrompt { get { return this[2]; } set { this[2] = value; } } | ||
115 | public string Cabinet { get { return this[3]; } set { this[3] = value; } } | ||
116 | public string VolumeLabel { get { return this[4]; } set { this[4] = value; } } | ||
117 | public string Source { get { return this[5]; } set { this[5] = value; } } | ||
118 | } | ||
119 | |||
120 | public class Property_ : QRecord | ||
121 | { | ||
122 | public string Property { get { return this[0]; } set { this[0] = value; } } | ||
123 | public string Value { get { return this[1]; } set { this[1] = value; } } | ||
124 | } | ||
125 | |||
126 | public class Registry_ : QRecord | ||
127 | { | ||
128 | public string Registry { get { return this[0]; } set { this[0] = value; } } | ||
129 | public string Key { get { return this[2]; } set { this[2] = value; } } | ||
130 | public string Name { get { return this[3]; } set { this[3] = value; } } | ||
131 | public string Value { get { return this[4]; } set { this[4] = value; } } | ||
132 | public string Component_ { get { return this[5]; } set { this[5] = value; } } | ||
133 | public RegistryRoot Root | ||
134 | { get { return (RegistryRoot) this.I(1); } set { this[0] = ((int) value).ToString(); } } | ||
135 | } | ||
136 | |||
137 | public class RemoveFile_ : QRecord | ||
138 | { | ||
139 | public string FileKey { get { return this[0]; } set { this[0] = value; } } | ||
140 | public string Component_ { get { return this[2]; } set { this[2] = value; } } | ||
141 | public string FileName { get { return this[3]; } set { this[3] = value; } } | ||
142 | public string DirProperty { get { return this[4]; } set { this[4] = value; } } | ||
143 | public RemoveFileModes InstallMode | ||
144 | { get { return (RemoveFileModes) this.I(5); } set { this[5] = ((int) value).ToString(); } } | ||
145 | } | ||
146 | |||
147 | #endregion // Generated code | ||
148 | #pragma warning restore 1591 | ||
149 | #endif // !CODE_ANALYSIS | ||
150 | } | ||
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QDatabase.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QDatabase.cs new file mode 100644 index 00000000..b4de2f60 --- /dev/null +++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QDatabase.cs | |||
@@ -0,0 +1,214 @@ | |||
1 | // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. | ||
2 | |||
3 | namespace WixToolset.Dtf.WindowsInstaller.Linq | ||
4 | { | ||
5 | using System; | ||
6 | using System.IO; | ||
7 | using WixToolset.Dtf.WindowsInstaller.Linq.Entities; | ||
8 | |||
9 | /// <summary> | ||
10 | /// Allows any Database instance to be converted into a queryable database. | ||
11 | /// </summary> | ||
12 | public static class Queryable | ||
13 | { | ||
14 | /// <summary> | ||
15 | /// Converts any Database instance into a queryable database. | ||
16 | /// </summary> | ||
17 | /// <param name="db"></param> | ||
18 | /// <returns>Queryable database instance that operates on the same | ||
19 | /// MSI handle.</returns> | ||
20 | /// <remarks> | ||
21 | /// This extension method is meant for convenient on-the-fly conversion. | ||
22 | /// If the existing database instance already happens to be a QDatabase, | ||
23 | /// then it is returned unchanged. Otherwise since the new database | ||
24 | /// carries the same MSI handle, only one of the instances needs to be | ||
25 | /// closed, not both. | ||
26 | /// </remarks> | ||
27 | public static QDatabase AsQueryable(this Database db) | ||
28 | { | ||
29 | QDatabase qdb = db as QDatabase; | ||
30 | if (qdb == null && db != null) | ||
31 | { | ||
32 | qdb = new QDatabase(db.Handle, true, db.FilePath, db.OpenMode); | ||
33 | } | ||
34 | return qdb; | ||
35 | } | ||
36 | } | ||
37 | |||
38 | /// <summary> | ||
39 | /// Queryable MSI database - extends the base Database class with | ||
40 | /// LINQ query functionality along with predefined entity types | ||
41 | /// for common tables. | ||
42 | /// </summary> | ||
43 | public class QDatabase : Database | ||
44 | { | ||
45 | /// <summary> | ||
46 | /// Opens an existing database in read-only mode. | ||
47 | /// </summary> | ||
48 | /// <param name="filePath">Path to the database file.</param> | ||
49 | /// <exception cref="InstallerException">the database could not be created/opened</exception> | ||
50 | /// <remarks> | ||
51 | /// Because this constructor initiates database access, it cannot be used with a | ||
52 | /// running installation. | ||
53 | /// <para>The Database object should be <see cref="InstallerHandle.Close"/>d after use. | ||
54 | /// The finalizer will close the handle if it is still open, however due to the nondeterministic | ||
55 | /// nature of finalization it is best that the handle be closed manually as soon as it is no | ||
56 | /// longer needed, as leaving lots of unused handles open can degrade performance.</para> | ||
57 | /// </remarks> | ||
58 | public QDatabase(string filePath) | ||
59 | : base(filePath) | ||
60 | { | ||
61 | } | ||
62 | |||
63 | /// <summary> | ||
64 | /// Opens an existing database with another database as output. | ||
65 | /// </summary> | ||
66 | /// <param name="filePath">Path to the database to be read.</param> | ||
67 | /// <param name="outputPath">Open mode for the database</param> | ||
68 | /// <returns>Database object representing the created or opened database</returns> | ||
69 | /// <exception cref="InstallerException">the database could not be created/opened</exception> | ||
70 | /// <remarks> | ||
71 | /// When a database is opened as the output of another database, the summary information stream | ||
72 | /// of the output database is actually a read-only mirror of the original database and thus cannot | ||
73 | /// be changed. Additionally, it is not persisted with the database. To create or modify the | ||
74 | /// summary information for the output database it must be closed and re-opened. | ||
75 | /// <para>The returned Database object should be <see cref="InstallerHandle.Close"/>d after use. | ||
76 | /// The finalizer will close the handle if it is still open, however due to the nondeterministic | ||
77 | /// nature of finalization it is best that the handle be closed manually as soon as it is no | ||
78 | /// longer needed, as leaving lots of unused handles open can degrade performance.</para> | ||
79 | /// </remarks> | ||
80 | public QDatabase(string filePath, string outputPath) | ||
81 | : base(filePath, outputPath) | ||
82 | { | ||
83 | } | ||
84 | |||
85 | /// <summary> | ||
86 | /// Opens an existing database or creates a new one. | ||
87 | /// </summary> | ||
88 | /// <param name="filePath">Path to the database file. If an empty string | ||
89 | /// is supplied, a temporary database is created that is not persisted.</param> | ||
90 | /// <param name="mode">Open mode for the database</param> | ||
91 | /// <exception cref="InstallerException">the database could not be created/opened</exception> | ||
92 | /// <remarks> | ||
93 | /// To make and save changes to a database first open the database in transaction, | ||
94 | /// create or, or direct mode. After making the changes, always call the Commit method | ||
95 | /// before closing the database handle. The Commit method flushes all buffers. | ||
96 | /// <para>Always call the Commit method on a database that has been opened in direct | ||
97 | /// mode before closing the database. Failure to do this may corrupt the database.</para> | ||
98 | /// <para>Because this constructor initiates database access, it cannot be used with a | ||
99 | /// running installation.</para> | ||
100 | /// <para>The Database object should be <see cref="InstallerHandle.Close"/>d after use. | ||
101 | /// The finalizer will close the handle if it is still open, however due to the nondeterministic | ||
102 | /// nature of finalization it is best that the handle be closed manually as soon as it is no | ||
103 | /// longer needed, as leaving lots of unused handles open can degrade performance.</para> | ||
104 | /// </remarks> | ||
105 | public QDatabase(string filePath, DatabaseOpenMode mode) | ||
106 | : base(filePath, mode) | ||
107 | { | ||
108 | } | ||
109 | |||
110 | /// <summary> | ||
111 | /// Creates a new database from an MSI handle. | ||
112 | /// </summary> | ||
113 | /// <param name="handle">Native MSI database handle.</param> | ||
114 | /// <param name="ownsHandle">True if the handle should be closed | ||
115 | /// when the database object is disposed</param> | ||
116 | /// <param name="filePath">Path of the database file, if known</param> | ||
117 | /// <param name="openMode">Mode the handle was originally opened in</param> | ||
118 | protected internal QDatabase( | ||
119 | IntPtr handle, bool ownsHandle, string filePath, DatabaseOpenMode openMode) | ||
120 | : base(handle, ownsHandle, filePath, openMode) | ||
121 | { | ||
122 | } | ||
123 | |||
124 | /// <summary> | ||
125 | /// Gets or sets a log where all MSI SQL queries are written. | ||
126 | /// </summary> | ||
127 | /// <remarks> | ||
128 | /// The log can be useful for debugging, or simply to watch the LINQ magic in action. | ||
129 | /// </remarks> | ||
130 | public TextWriter Log { get; set; } | ||
131 | |||
132 | /// <summary> | ||
133 | /// Gets a queryable table from the datbaase. | ||
134 | /// </summary> | ||
135 | /// <param name="table">name of the table</param> | ||
136 | public QTable<QRecord> this[string table] | ||
137 | { | ||
138 | get | ||
139 | { | ||
140 | return new QTable<QRecord>(this, table); | ||
141 | } | ||
142 | } | ||
143 | |||
144 | #if !CODE_ANALYSIS | ||
145 | #region Queryable tables | ||
146 | |||
147 | /// <summary>Queryable standard table with predefined specialized record type.</summary> | ||
148 | public QTable<Component_> Components | ||
149 | { get { return new QTable<Component_>(this); } } | ||
150 | |||
151 | /// <summary>Queryable standard table with predefined specialized record type.</summary> | ||
152 | public QTable<CreateFolder_> CreateFolders | ||
153 | { get { return new QTable<CreateFolder_>(this); } } | ||
154 | |||
155 | /// <summary>Queryable standard table with predefined specialized record type.</summary> | ||
156 | public QTable<CustomAction_> CustomActions | ||
157 | { get { return new QTable<CustomAction_>(this); } } | ||
158 | |||
159 | /// <summary>Queryable standard table with predefined specialized record type.</summary> | ||
160 | public QTable<Directory_> Directories | ||
161 | { get { return new QTable<Directory_>(this); } } | ||
162 | |||
163 | /// <summary>Queryable standard table with predefined specialized record type.</summary> | ||
164 | public QTable<DuplicateFile_> DuplicateFiles | ||
165 | { get { return new QTable<DuplicateFile_>(this); } } | ||
166 | |||
167 | /// <summary>Queryable standard table with predefined specialized record type.</summary> | ||
168 | public QTable<Feature_> Features | ||
169 | { get { return new QTable<Feature_>(this); } } | ||
170 | |||
171 | /// <summary>Queryable standard table with predefined specialized record type.</summary> | ||
172 | public QTable<FeatureComponent_> FeatureComponents | ||
173 | { get { return new QTable<FeatureComponent_>(this); } } | ||
174 | |||
175 | /// <summary>Queryable standard table with predefined specialized record type.</summary> | ||
176 | public QTable<File_> Files | ||
177 | { get { return new QTable<File_>(this); } } | ||
178 | |||
179 | /// <summary>Queryable standard table with predefined specialized record type.</summary> | ||
180 | public QTable<FileHash_> FileHashes | ||
181 | { get { return new QTable<FileHash_>(this); } } | ||
182 | |||
183 | /// <summary>Queryable standard table with predefined specialized record type.</summary> | ||
184 | public QTable<InstallSequence_> InstallExecuteSequences | ||
185 | { get { return new QTable<InstallSequence_>(this, "InstallExecuteSequence"); } } | ||
186 | |||
187 | /// <summary>Queryable standard table with predefined specialized record type.</summary> | ||
188 | public QTable<InstallSequence_> InstallUISequences | ||
189 | { get { return new QTable<InstallSequence_>(this, "InstallUISequence"); } } | ||
190 | |||
191 | /// <summary>Queryable standard table with predefined specialized record type.</summary> | ||
192 | public QTable<LaunchCondition_> LaunchConditions | ||
193 | { get { return new QTable<LaunchCondition_>(this); } } | ||
194 | |||
195 | /// <summary>Queryable standard table with predefined specialized record type.</summary> | ||
196 | public QTable<Media_> Medias | ||
197 | { get { return new QTable<Media_>(this); } } | ||
198 | |||
199 | /// <summary>Queryable standard table with predefined specialized record type.</summary> | ||
200 | public QTable<Property_> Properties | ||
201 | { get { return new QTable<Property_>(this); } } | ||
202 | |||
203 | /// <summary>Queryable standard table with predefined specialized record type.</summary> | ||
204 | public QTable<Registry_> Registries | ||
205 | { get { return new QTable<Registry_>(this); } } | ||
206 | |||
207 | /// <summary>Queryable standard table with predefined specialized record type.</summary> | ||
208 | public QTable<RemoveFile_> RemoveFiles | ||
209 | { get { return new QTable<RemoveFile_>(this); } } | ||
210 | |||
211 | #endregion // Queryable tables | ||
212 | #endif // !CODE_ANALYSIS | ||
213 | } | ||
214 | } | ||
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QRecord.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QRecord.cs new file mode 100644 index 00000000..4b3145fd --- /dev/null +++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QRecord.cs | |||
@@ -0,0 +1,501 @@ | |||
1 | // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. | ||
2 | |||
3 | namespace WixToolset.Dtf.WindowsInstaller.Linq | ||
4 | { | ||
5 | using System; | ||
6 | using System.IO; | ||
7 | using System.Text; | ||
8 | using System.Globalization; | ||
9 | using System.Collections.Generic; | ||
10 | using System.Diagnostics.CodeAnalysis; | ||
11 | |||
12 | /// <summary> | ||
13 | /// Generic record entity for queryable databases, | ||
14 | /// and base for strongly-typed entity subclasses. | ||
15 | /// </summary> | ||
16 | /// <remarks> | ||
17 | /// Several predefined specialized subclasses are provided for common | ||
18 | /// standard tables. Subclasses for additional standard tables | ||
19 | /// or custom tables are not necessary, but they are easy to create | ||
20 | /// and make the coding experience much nicer. | ||
21 | /// <para>When creating subclasses, the following attributes may be | ||
22 | /// useful: <see cref="DatabaseTableAttribute"/>, | ||
23 | /// <see cref="DatabaseColumnAttribute"/></para> | ||
24 | /// </remarks> | ||
25 | public class QRecord | ||
26 | { | ||
27 | /// <summary> | ||
28 | /// Do not call. Use QTable.NewRecord() instead. | ||
29 | /// </summary> | ||
30 | /// <remarks> | ||
31 | /// Subclasses must also provide a public parameterless constructor. | ||
32 | /// <para>QRecord constructors are only public due to implementation | ||
33 | /// reasons (to satisfy the new() constraint on the QTable generic | ||
34 | /// class). They are not intended to be called by user code other than | ||
35 | /// a subclass constructor. If the constructor is invoked directly, | ||
36 | /// the record instance will not be properly initialized (associated | ||
37 | /// with a database table) and calls to methods on the instance | ||
38 | /// will throw a NullReferenceException.</para> | ||
39 | /// </remarks> | ||
40 | /// <seealso cref="QTable<TRecord>.NewRecord()"/> | ||
41 | public QRecord() | ||
42 | { | ||
43 | } | ||
44 | |||
45 | internal QDatabase Database { get; set; } | ||
46 | |||
47 | internal TableInfo TableInfo { get; set; } | ||
48 | |||
49 | internal IList<string> Values { get; set; } | ||
50 | |||
51 | internal bool Exists { get; set; } | ||
52 | |||
53 | /// <summary> | ||
54 | /// Gets the number of fields in the record. | ||
55 | /// </summary> | ||
56 | public int FieldCount | ||
57 | { | ||
58 | get | ||
59 | { | ||
60 | return this.Values.Count; | ||
61 | } | ||
62 | } | ||
63 | |||
64 | /// <summary> | ||
65 | /// Gets or sets a record field. | ||
66 | /// </summary> | ||
67 | /// <param name="field">column name of the field</param> | ||
68 | /// <remarks> | ||
69 | /// Setting a field value will automatically update the database. | ||
70 | /// </remarks> | ||
71 | public string this[string field] | ||
72 | { | ||
73 | get | ||
74 | { | ||
75 | if (field == null) | ||
76 | { | ||
77 | throw new ArgumentNullException("field"); | ||
78 | } | ||
79 | |||
80 | int index = this.TableInfo.Columns.IndexOf(field); | ||
81 | if (index < 0) | ||
82 | { | ||
83 | throw new ArgumentOutOfRangeException("field"); | ||
84 | } | ||
85 | |||
86 | return this[index]; | ||
87 | } | ||
88 | |||
89 | set | ||
90 | { | ||
91 | if (field == null) | ||
92 | { | ||
93 | throw new ArgumentNullException("field"); | ||
94 | } | ||
95 | |||
96 | this.Update(new string[] { field }, new string[] { value }); | ||
97 | } | ||
98 | } | ||
99 | |||
100 | /// <summary> | ||
101 | /// Gets or sets a record field. | ||
102 | /// </summary> | ||
103 | /// <param name="index">zero-based column index of the field</param> | ||
104 | /// <remarks> | ||
105 | /// Setting a field value will automatically update the database. | ||
106 | /// </remarks> | ||
107 | public string this[int index] | ||
108 | { | ||
109 | get | ||
110 | { | ||
111 | if (index < 0 || index >= this.FieldCount) | ||
112 | { | ||
113 | throw new ArgumentOutOfRangeException("index"); | ||
114 | } | ||
115 | |||
116 | return this.Values[index]; | ||
117 | } | ||
118 | |||
119 | set | ||
120 | { | ||
121 | if (index < 0 || index >= this.FieldCount) | ||
122 | { | ||
123 | throw new ArgumentOutOfRangeException("index"); | ||
124 | } | ||
125 | |||
126 | this.Update(new int[] { index }, new string[] { value }); | ||
127 | } | ||
128 | } | ||
129 | |||
130 | /// <summary> | ||
131 | /// Used by subclasses to get a field as an integer. | ||
132 | /// </summary> | ||
133 | /// <param name="index">zero-based column index of the field</param> | ||
134 | [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "I")] | ||
135 | protected int I(int index) | ||
136 | { | ||
137 | string value = this[index]; | ||
138 | return value.Length > 0 ? | ||
139 | Int32.Parse(value, CultureInfo.InvariantCulture) : 0; | ||
140 | } | ||
141 | |||
142 | /// <summary> | ||
143 | /// Used by subclasses to get a field as a nullable integer. | ||
144 | /// </summary> | ||
145 | /// <param name="index">zero-based column index of the field</param> | ||
146 | protected int? NI(int index) | ||
147 | { | ||
148 | string value = this[index]; | ||
149 | return value.Length > 0 ? | ||
150 | new int?(Int32.Parse(value, CultureInfo.InvariantCulture)) : null; | ||
151 | } | ||
152 | |||
153 | /// <summary> | ||
154 | /// Dumps all record fields to a string. | ||
155 | /// </summary> | ||
156 | public override string ToString() | ||
157 | { | ||
158 | StringBuilder buf = new StringBuilder(this.GetType().Name); | ||
159 | buf.Append(" {"); | ||
160 | for (int i = 0; i < this.FieldCount; i++) | ||
161 | { | ||
162 | buf.AppendFormat("{0} {1} = {2}", | ||
163 | (i > 0 ? "," : String.Empty), | ||
164 | this.TableInfo.Columns[i].Name, | ||
165 | this[i]); | ||
166 | } | ||
167 | buf.Append(" }"); | ||
168 | return buf.ToString(); | ||
169 | } | ||
170 | |||
171 | /// <summary> | ||
172 | /// Update multiple fields in the record (and the database). | ||
173 | /// </summary> | ||
174 | /// <param name="fields">column names of fields to update</param> | ||
175 | /// <param name="values">new values for each field being updated</param> | ||
176 | public void Update(IList<string> fields, IList<string> values) | ||
177 | { | ||
178 | if (fields == null) | ||
179 | { | ||
180 | throw new ArgumentNullException("fields"); | ||
181 | } | ||
182 | |||
183 | if (values == null) | ||
184 | { | ||
185 | throw new ArgumentNullException("values"); | ||
186 | } | ||
187 | |||
188 | if (fields.Count == 0 || values.Count == 0 || | ||
189 | fields.Count > this.FieldCount || | ||
190 | values.Count != fields.Count) | ||
191 | { | ||
192 | throw new ArgumentOutOfRangeException("fields"); | ||
193 | } | ||
194 | |||
195 | int[] indexes = new int[fields.Count]; | ||
196 | for (int i = 0; i < indexes.Length; i++) | ||
197 | { | ||
198 | if (fields[i] == null) | ||
199 | { | ||
200 | throw new ArgumentNullException("fields[" + i + "]"); | ||
201 | } | ||
202 | |||
203 | indexes[i] = this.TableInfo.Columns.IndexOf(fields[i]); | ||
204 | |||
205 | if (indexes[i] < 0) | ||
206 | { | ||
207 | throw new ArgumentOutOfRangeException("fields[" + i + "]"); | ||
208 | } | ||
209 | } | ||
210 | |||
211 | this.Update(indexes, values); | ||
212 | } | ||
213 | |||
214 | /// <summary> | ||
215 | /// Update multiple fields in the record (and the database). | ||
216 | /// </summary> | ||
217 | /// <param name="indexes">column indexes of fields to update</param> | ||
218 | /// <param name="values">new values for each field being updated</param> | ||
219 | /// <remarks> | ||
220 | /// The record (primary keys) must already exist in the table. | ||
221 | /// <para>Updating primary key fields is not yet implemented; use Delete() | ||
222 | /// and Insert() instead.</para> | ||
223 | /// </remarks> | ||
224 | public void Update(IList<int> indexes, IList<string> values) | ||
225 | { | ||
226 | if (indexes == null) | ||
227 | { | ||
228 | throw new ArgumentNullException("indexes"); | ||
229 | } | ||
230 | |||
231 | if (values == null) | ||
232 | { | ||
233 | throw new ArgumentNullException("values"); | ||
234 | } | ||
235 | |||
236 | if (indexes.Count == 0 || values.Count == 0 || | ||
237 | indexes.Count > this.FieldCount || | ||
238 | values.Count != indexes.Count) | ||
239 | { | ||
240 | throw new ArgumentOutOfRangeException("indexes"); | ||
241 | } | ||
242 | |||
243 | bool primaryKeyChanged = false; | ||
244 | for (int i = 0; i < indexes.Count; i++) | ||
245 | { | ||
246 | int index = indexes[i]; | ||
247 | if (index < 0 || index >= this.FieldCount) | ||
248 | { | ||
249 | throw new ArgumentOutOfRangeException("index[" + i + "]"); | ||
250 | } | ||
251 | |||
252 | ColumnInfo col = this.TableInfo.Columns[index]; | ||
253 | if (this.TableInfo.PrimaryKeys.Contains(col.Name)) | ||
254 | { | ||
255 | if (values[i] == null) | ||
256 | { | ||
257 | throw new ArgumentNullException("values[" + i + "]"); | ||
258 | } | ||
259 | |||
260 | primaryKeyChanged = true; | ||
261 | } | ||
262 | else if (values[i] == null) | ||
263 | { | ||
264 | if (col.IsRequired) | ||
265 | { | ||
266 | throw new ArgumentNullException("values[" + i + "]"); | ||
267 | } | ||
268 | } | ||
269 | |||
270 | this.Values[index] = values[i]; | ||
271 | } | ||
272 | |||
273 | if (this.Exists) | ||
274 | { | ||
275 | if (!primaryKeyChanged) | ||
276 | { | ||
277 | int updateRecSize = indexes.Count + this.TableInfo.PrimaryKeys.Count; | ||
278 | using (Record updateRec = this.Database.CreateRecord(updateRecSize)) | ||
279 | { | ||
280 | StringBuilder s = new StringBuilder("UPDATE `"); | ||
281 | s.Append(this.TableInfo.Name); | ||
282 | s.Append("` SET"); | ||
283 | |||
284 | for (int i = 0; i < indexes.Count; i++) | ||
285 | { | ||
286 | ColumnInfo col = this.TableInfo.Columns[indexes[i]]; | ||
287 | if (col.Type == typeof(Stream)) | ||
288 | { | ||
289 | throw new NotSupportedException( | ||
290 | "Cannot update stream columns via QRecord."); | ||
291 | } | ||
292 | |||
293 | int index = indexes[i]; | ||
294 | s.AppendFormat("{0} `{1}` = ?", | ||
295 | (i > 0 ? "," : String.Empty), | ||
296 | col.Name); | ||
297 | |||
298 | if (values[i] != null) | ||
299 | { | ||
300 | updateRec[i + 1] = values[i]; | ||
301 | } | ||
302 | } | ||
303 | |||
304 | for (int i = 0; i < this.TableInfo.PrimaryKeys.Count; i++) | ||
305 | { | ||
306 | string key = this.TableInfo.PrimaryKeys[i]; | ||
307 | s.AppendFormat(" {0} `{1}` = ?", (i == 0 ? "WHERE" : "AND"), key); | ||
308 | int index = this.TableInfo.Columns.IndexOf(key); | ||
309 | updateRec[indexes.Count + i + 1] = this.Values[index]; | ||
310 | |||
311 | } | ||
312 | |||
313 | string updateSql = s.ToString(); | ||
314 | TextWriter log = this.Database.Log; | ||
315 | if (log != null) | ||
316 | { | ||
317 | log.WriteLine(); | ||
318 | log.WriteLine(updateSql); | ||
319 | for (int field = 1; field <= updateRecSize; field++) | ||
320 | { | ||
321 | log.WriteLine(" ? = " + updateRec.GetString(field)); | ||
322 | } | ||
323 | } | ||
324 | |||
325 | this.Database.Execute(updateSql, updateRec); | ||
326 | } | ||
327 | } | ||
328 | else | ||
329 | { | ||
330 | throw new NotImplementedException( | ||
331 | "Update() cannot handle changed primary keys yet."); | ||
332 | // TODO: | ||
333 | // query using old values | ||
334 | // update values | ||
335 | // View.Replace | ||
336 | } | ||
337 | } | ||
338 | } | ||
339 | |||
340 | /// <summary> | ||
341 | /// Inserts the record in the database. | ||
342 | /// </summary> | ||
343 | /// <remarks> | ||
344 | /// The record (primary keys) may not already exist in the table. | ||
345 | /// <para>Use <see cref="QTable<TRecord>.NewRecord()"/> to get a new | ||
346 | /// record. Prmary keys and all required fields | ||
347 | /// must be filled in before insertion.</para> | ||
348 | /// </remarks> | ||
349 | public void Insert() | ||
350 | { | ||
351 | this.Insert(false); | ||
352 | } | ||
353 | |||
354 | /// <summary> | ||
355 | /// Inserts the record into the table. | ||
356 | /// </summary> | ||
357 | /// <param name="temporary">true if the record is temporarily | ||
358 | /// inserted, to be visible only as long as the database is open</param> | ||
359 | /// <remarks> | ||
360 | /// The record (primary keys) may not already exist in the table. | ||
361 | /// <para>Use <see cref="QTable<TRecord>.NewRecord()"/> to get a new | ||
362 | /// record. Prmary keys and all required fields | ||
363 | /// must be filled in before insertion.</para> | ||
364 | /// </remarks> | ||
365 | public void Insert(bool temporary) | ||
366 | { | ||
367 | using (Record updateRec = this.Database.CreateRecord(this.FieldCount)) | ||
368 | { | ||
369 | string insertSql = this.TableInfo.SqlInsertString; | ||
370 | if (temporary) | ||
371 | { | ||
372 | insertSql += " TEMPORARY"; | ||
373 | } | ||
374 | |||
375 | TextWriter log = this.Database.Log; | ||
376 | if (log != null) | ||
377 | { | ||
378 | log.WriteLine(); | ||
379 | log.WriteLine(insertSql); | ||
380 | } | ||
381 | |||
382 | for (int index = 0; index < this.FieldCount; index++) | ||
383 | { | ||
384 | ColumnInfo col = this.TableInfo.Columns[index]; | ||
385 | if (col.Type == typeof(Stream)) | ||
386 | { | ||
387 | throw new NotSupportedException( | ||
388 | "Cannot insert stream columns via QRecord."); | ||
389 | } | ||
390 | |||
391 | if (this.Values[index] != null) | ||
392 | { | ||
393 | updateRec[index + 1] = this.Values[index]; | ||
394 | } | ||
395 | |||
396 | if (log != null) | ||
397 | { | ||
398 | log.WriteLine(" ? = " + this.Values[index]); | ||
399 | } | ||
400 | } | ||
401 | |||
402 | this.Database.Execute(insertSql, updateRec); | ||
403 | this.Exists = true; | ||
404 | } | ||
405 | } | ||
406 | |||
407 | /// <summary> | ||
408 | /// Deletes the record from the table if it exists. | ||
409 | /// </summary> | ||
410 | public void Delete() | ||
411 | { | ||
412 | using (Record keyRec = this.Database.CreateRecord(this.TableInfo.PrimaryKeys.Count)) | ||
413 | { | ||
414 | StringBuilder s = new StringBuilder("DELETE FROM `"); | ||
415 | s.Append(this.TableInfo.Name); | ||
416 | s.Append("`"); | ||
417 | for (int i = 0; i < this.TableInfo.PrimaryKeys.Count; i++) | ||
418 | { | ||
419 | string key = this.TableInfo.PrimaryKeys[i]; | ||
420 | s.AppendFormat(" {0} `{1}` = ?", (i == 0 ? "WHERE" : "AND"), key); | ||
421 | int index = this.TableInfo.Columns.IndexOf(key); | ||
422 | keyRec[i + 1] = this.Values[index]; | ||
423 | } | ||
424 | |||
425 | string deleteSql = s.ToString(); | ||
426 | |||
427 | TextWriter log = this.Database.Log; | ||
428 | if (log != null) | ||
429 | { | ||
430 | log.WriteLine(); | ||
431 | log.WriteLine(deleteSql); | ||
432 | |||
433 | for (int i = 0; i < this.TableInfo.PrimaryKeys.Count; i++) | ||
434 | { | ||
435 | log.WriteLine(" ? = " + keyRec.GetString(i + 1)); | ||
436 | } | ||
437 | } | ||
438 | |||
439 | this.Database.Execute(deleteSql, keyRec); | ||
440 | this.Exists = false; | ||
441 | } | ||
442 | } | ||
443 | |||
444 | /// <summary> | ||
445 | /// Not yet implemented. | ||
446 | /// </summary> | ||
447 | public void Refresh() | ||
448 | { | ||
449 | throw new NotImplementedException(); | ||
450 | } | ||
451 | |||
452 | /// <summary> | ||
453 | /// Not yet implemented. | ||
454 | /// </summary> | ||
455 | public void Assign() | ||
456 | { | ||
457 | throw new NotImplementedException(); | ||
458 | } | ||
459 | |||
460 | /// <summary> | ||
461 | /// Not yet implemented. | ||
462 | /// </summary> | ||
463 | public bool Merge() | ||
464 | { | ||
465 | throw new NotImplementedException(); | ||
466 | } | ||
467 | |||
468 | /// <summary> | ||
469 | /// Not yet implemented. | ||
470 | /// </summary> | ||
471 | public ICollection<ValidationErrorInfo> Validate() | ||
472 | { | ||
473 | throw new NotImplementedException(); | ||
474 | } | ||
475 | |||
476 | /// <summary> | ||
477 | /// Not yet implemented. | ||
478 | /// </summary> | ||
479 | [SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")] | ||
480 | public ICollection<ValidationErrorInfo> ValidateNew() | ||
481 | { | ||
482 | throw new NotImplementedException(); | ||
483 | } | ||
484 | |||
485 | /// <summary> | ||
486 | /// Not yet implemented. | ||
487 | /// </summary> | ||
488 | public ICollection<ValidationErrorInfo> ValidateFields() | ||
489 | { | ||
490 | throw new NotImplementedException(); | ||
491 | } | ||
492 | |||
493 | /// <summary> | ||
494 | /// Not yet implemented. | ||
495 | /// </summary> | ||
496 | public ICollection<ValidationErrorInfo> ValidateDelete() | ||
497 | { | ||
498 | throw new NotImplementedException(); | ||
499 | } | ||
500 | } | ||
501 | } | ||
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QTable.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QTable.cs new file mode 100644 index 00000000..e0e1c154 --- /dev/null +++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/QTable.cs | |||
@@ -0,0 +1,296 @@ | |||
1 | // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. | ||
2 | |||
3 | namespace WixToolset.Dtf.WindowsInstaller.Linq | ||
4 | { | ||
5 | using System; | ||
6 | using System.IO; | ||
7 | using System.Collections; | ||
8 | using System.Collections.Generic; | ||
9 | using System.Reflection; | ||
10 | using System.Linq; | ||
11 | using System.Linq.Expressions; | ||
12 | using System.Diagnostics.CodeAnalysis; | ||
13 | |||
14 | /// <summary> | ||
15 | /// Represents one table in a LINQ-queryable Database. | ||
16 | /// </summary> | ||
17 | /// <typeparam name="TRecord">type that represents one record in the table</typeparam> | ||
18 | /// <remarks> | ||
19 | /// This class is the primary gateway to all LINQ to MSI query functionality. | ||
20 | /// <para>The TRecord generic parameter may be the general <see cref="QRecord" /> | ||
21 | /// class, or a specialized subclass of QRecord.</para> | ||
22 | /// </remarks> | ||
23 | [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")] | ||
24 | public sealed class QTable<TRecord> : IOrderedQueryable<TRecord>, IQueryProvider | ||
25 | where TRecord : QRecord, new() | ||
26 | { | ||
27 | private QDatabase db; | ||
28 | private TableInfo tableInfo; | ||
29 | |||
30 | /// <summary> | ||
31 | /// Infers the name of the table this instance will be | ||
32 | /// associated with. | ||
33 | /// </summary> | ||
34 | /// <returns>table name</returns> | ||
35 | /// <remarks> | ||
36 | /// The table name is retrieved from a DatabaseTableAttribute | ||
37 | /// on the record type if it exists; otherwise the name is | ||
38 | /// derived from the name of the record type itself. | ||
39 | /// (An optional underscore suffix on the record type name is dropped.) | ||
40 | /// </remarks> | ||
41 | private static string InferTableName() | ||
42 | { | ||
43 | foreach (DatabaseTableAttribute attr in typeof(TRecord).GetCustomAttributes( | ||
44 | typeof(DatabaseTableAttribute), false)) | ||
45 | { | ||
46 | string tableName = attr.Table; | ||
47 | if (!String.IsNullOrEmpty(tableName)) | ||
48 | { | ||
49 | return tableName; | ||
50 | } | ||
51 | } | ||
52 | |||
53 | string recordTypeName = typeof(TRecord).Name; | ||
54 | if (recordTypeName[recordTypeName.Length - 1] == '_') | ||
55 | { | ||
56 | return recordTypeName.Substring(0, recordTypeName.Length - 1); | ||
57 | } | ||
58 | else | ||
59 | { | ||
60 | return recordTypeName; | ||
61 | } | ||
62 | } | ||
63 | |||
64 | /// <summary> | ||
65 | /// Creates a new QTable, inferring the table name | ||
66 | /// from the name of the record type parameter. | ||
67 | /// </summary> | ||
68 | /// <param name="db">database that contains the table</param> | ||
69 | public QTable(QDatabase db) | ||
70 | : this(db, InferTableName()) | ||
71 | { | ||
72 | } | ||
73 | |||
74 | /// <summary> | ||
75 | /// Creates a new QTable with an explicit table name. | ||
76 | /// </summary> | ||
77 | /// <param name="db">database that contains the table</param> | ||
78 | /// <param name="table">name of the table</param> | ||
79 | public QTable(QDatabase db, string table) | ||
80 | { | ||
81 | if (db == null) | ||
82 | { | ||
83 | throw new ArgumentNullException("db"); | ||
84 | } | ||
85 | |||
86 | if (String.IsNullOrEmpty(table)) | ||
87 | { | ||
88 | throw new ArgumentNullException("table"); | ||
89 | } | ||
90 | |||
91 | this.db = db; | ||
92 | this.tableInfo = db.Tables[table]; | ||
93 | if (this.tableInfo == null) | ||
94 | { | ||
95 | throw new ArgumentException( | ||
96 | "Table does not exist in database: " + table); | ||
97 | } | ||
98 | } | ||
99 | |||
100 | /// <summary> | ||
101 | /// Gets schema information about the table. | ||
102 | /// </summary> | ||
103 | public TableInfo TableInfo | ||
104 | { | ||
105 | get | ||
106 | { | ||
107 | return this.tableInfo; | ||
108 | } | ||
109 | } | ||
110 | |||
111 | /// <summary> | ||
112 | /// Gets the database this table is associated with. | ||
113 | /// </summary> | ||
114 | public QDatabase Database | ||
115 | { | ||
116 | get | ||
117 | { | ||
118 | return this.db; | ||
119 | } | ||
120 | } | ||
121 | |||
122 | /// <summary> | ||
123 | /// Enumerates over all records in the table. | ||
124 | /// </summary> | ||
125 | /// <returns></returns> | ||
126 | public IEnumerator<TRecord> GetEnumerator() | ||
127 | { | ||
128 | string query = this.tableInfo.SqlSelectString; | ||
129 | |||
130 | TextWriter log = this.db.Log; | ||
131 | if (log != null) | ||
132 | { | ||
133 | log.WriteLine(); | ||
134 | log.WriteLine(query); | ||
135 | } | ||
136 | |||
137 | using (View view = db.OpenView(query)) | ||
138 | { | ||
139 | view.Execute(); | ||
140 | |||
141 | ColumnCollection columns = this.tableInfo.Columns; | ||
142 | int columnCount = columns.Count; | ||
143 | bool[] isBinary = new bool[columnCount]; | ||
144 | |||
145 | for (int i = 0; i < isBinary.Length; i++) | ||
146 | { | ||
147 | isBinary[i] = columns[i].Type == typeof(System.IO.Stream); | ||
148 | } | ||
149 | |||
150 | foreach (Record rec in view) using (rec) | ||
151 | { | ||
152 | string[] values = new string[columnCount]; | ||
153 | for (int i = 0; i < values.Length; i++) | ||
154 | { | ||
155 | values[i] = isBinary[i] ? "[Binary Data]" : rec.GetString(i + 1); | ||
156 | } | ||
157 | |||
158 | TRecord trec = new TRecord(); | ||
159 | trec.Database = this.Database; | ||
160 | trec.TableInfo = this.TableInfo; | ||
161 | trec.Values = values; | ||
162 | trec.Exists = true; | ||
163 | yield return trec; | ||
164 | } | ||
165 | } | ||
166 | } | ||
167 | |||
168 | IEnumerator IEnumerable.GetEnumerator() | ||
169 | { | ||
170 | return ((IEnumerable<TRecord>) this).GetEnumerator(); | ||
171 | } | ||
172 | |||
173 | IQueryable<TElement> IQueryProvider.CreateQuery<TElement>(Expression expression) | ||
174 | { | ||
175 | if (expression == null) | ||
176 | { | ||
177 | throw new ArgumentNullException("expression"); | ||
178 | } | ||
179 | |||
180 | Query<TElement> q = new Query<TElement>(this.Database, expression); | ||
181 | |||
182 | MethodCallExpression methodCallExpression = (MethodCallExpression) expression; | ||
183 | string methodName = methodCallExpression.Method.Name; | ||
184 | if (methodName == "Where") | ||
185 | { | ||
186 | LambdaExpression argumentExpression = (LambdaExpression) | ||
187 | ((UnaryExpression) methodCallExpression.Arguments[1]).Operand; | ||
188 | q.BuildQuery(this.TableInfo, argumentExpression); | ||
189 | } | ||
190 | else if (methodName == "OrderBy") | ||
191 | { | ||
192 | LambdaExpression argumentExpression = (LambdaExpression) | ||
193 | ((UnaryExpression) methodCallExpression.Arguments[1]).Operand; | ||
194 | q.BuildSequence(this.TableInfo, argumentExpression); | ||
195 | } | ||
196 | else if (methodName == "Select") | ||
197 | { | ||
198 | LambdaExpression argumentExpression = (LambdaExpression) | ||
199 | ((UnaryExpression) methodCallExpression.Arguments[1]).Operand; | ||
200 | q.BuildNullQuery(this.TableInfo, typeof(TRecord), argumentExpression); | ||
201 | q.BuildProjection(null, argumentExpression); | ||
202 | } | ||
203 | else if (methodName == "Join") | ||
204 | { | ||
205 | ConstantExpression constantExpression = (ConstantExpression) | ||
206 | methodCallExpression.Arguments[1]; | ||
207 | IQueryable inner = (IQueryable) constantExpression.Value; | ||
208 | q.PerformJoin( | ||
209 | this.TableInfo, | ||
210 | typeof(TRecord), | ||
211 | inner, | ||
212 | GetJoinLambda(methodCallExpression.Arguments[2]), | ||
213 | GetJoinLambda(methodCallExpression.Arguments[3]), | ||
214 | GetJoinLambda(methodCallExpression.Arguments[4])); | ||
215 | } | ||
216 | else | ||
217 | { | ||
218 | throw new NotSupportedException( | ||
219 | "Query operation not supported: " + methodName); | ||
220 | } | ||
221 | |||
222 | return q; | ||
223 | } | ||
224 | |||
225 | private static LambdaExpression GetJoinLambda(Expression expresion) | ||
226 | { | ||
227 | UnaryExpression unaryExpression = (UnaryExpression) expresion; | ||
228 | return (LambdaExpression) unaryExpression.Operand; | ||
229 | } | ||
230 | |||
231 | IQueryable IQueryProvider.CreateQuery(Expression expression) | ||
232 | { | ||
233 | return ((IQueryProvider) this).CreateQuery<TRecord>(expression); | ||
234 | } | ||
235 | |||
236 | TResult IQueryProvider.Execute<TResult>(Expression expression) | ||
237 | { | ||
238 | throw new NotSupportedException( | ||
239 | "Direct method calls not supported -- use AsEnumerable() instead."); | ||
240 | } | ||
241 | |||
242 | object IQueryProvider.Execute(Expression expression) | ||
243 | { | ||
244 | throw new NotSupportedException( | ||
245 | "Direct method calls not supported -- use AsEnumerable() instead."); | ||
246 | } | ||
247 | |||
248 | IQueryProvider IQueryable.Provider | ||
249 | { | ||
250 | get | ||
251 | { | ||
252 | return this; | ||
253 | } | ||
254 | } | ||
255 | |||
256 | Type IQueryable.ElementType | ||
257 | { | ||
258 | get | ||
259 | { | ||
260 | return typeof(TRecord); | ||
261 | } | ||
262 | } | ||
263 | |||
264 | Expression IQueryable.Expression | ||
265 | { | ||
266 | get | ||
267 | { | ||
268 | return Expression.Constant(this); | ||
269 | } | ||
270 | } | ||
271 | |||
272 | /// <summary> | ||
273 | /// Creates a new record that can be inserted into this table. | ||
274 | /// </summary> | ||
275 | /// <returns>a record with all fields initialized to null</returns> | ||
276 | /// <remarks> | ||
277 | /// Primary keys and required fields must be filled in with | ||
278 | /// non-null values before the record can be inserted. | ||
279 | /// <para>The record is tied to this table in this database; | ||
280 | /// it cannot be inserted into another table or database.</para> | ||
281 | /// </remarks> | ||
282 | public TRecord NewRecord() | ||
283 | { | ||
284 | TRecord rec = new TRecord(); | ||
285 | rec.Database = this.Database; | ||
286 | rec.TableInfo = this.TableInfo; | ||
287 | IList<string> values = new List<string>(this.TableInfo.Columns.Count); | ||
288 | for (int i = 0; i < this.TableInfo.Columns.Count; i++) | ||
289 | { | ||
290 | values.Add(null); | ||
291 | } | ||
292 | rec.Values = values; | ||
293 | return rec; | ||
294 | } | ||
295 | } | ||
296 | } | ||
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Query.cs b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Query.cs new file mode 100644 index 00000000..ea58757c --- /dev/null +++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/Query.cs | |||
@@ -0,0 +1,992 @@ | |||
1 | // Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. | ||
2 | |||
3 | namespace WixToolset.Dtf.WindowsInstaller.Linq | ||
4 | { | ||
5 | using System; | ||
6 | using System.IO; | ||
7 | using System.Collections; | ||
8 | using System.Collections.Generic; | ||
9 | using System.Linq; | ||
10 | using System.Linq.Expressions; | ||
11 | using System.Text; | ||
12 | using System.Reflection; | ||
13 | using System.Globalization; | ||
14 | using System.Diagnostics.CodeAnalysis; | ||
15 | |||
16 | /// <summary> | ||
17 | /// Implements the LINQ to MSI query functionality. | ||
18 | /// </summary> | ||
19 | /// <typeparam name="T">the result type of the current query -- | ||
20 | /// either some kind of QRecord, or some projection of record data</typeparam> | ||
21 | internal sealed class Query<T> : IOrderedQueryable<T>, IQueryProvider | ||
22 | { | ||
23 | private QDatabase db; | ||
24 | private Expression queryableExpression; | ||
25 | private List<TableInfo> tables; | ||
26 | private List<Type> recordTypes; | ||
27 | private List<string> selectors; | ||
28 | private string where; | ||
29 | private List<object> whereParameters; | ||
30 | private List<TableColumn> orderbyColumns; | ||
31 | private List<TableColumn> selectColumns; | ||
32 | private List<TableColumn> joinColumns; | ||
33 | private List<Delegate> projectionDelegates; | ||
34 | |||
35 | internal Query(QDatabase db, Expression expression) | ||
36 | { | ||
37 | if (expression == null) | ||
38 | { | ||
39 | throw new ArgumentNullException("expression"); | ||
40 | } | ||
41 | |||
42 | this.db = db; | ||
43 | this.queryableExpression = expression; | ||
44 | this.tables = new List<TableInfo>(); | ||
45 | this.recordTypes = new List<Type>(); | ||
46 | this.selectors = new List<string>(); | ||
47 | this.whereParameters = new List<object>(); | ||
48 | this.orderbyColumns = new List<TableColumn>(); | ||
49 | this.selectColumns = new List<TableColumn>(); | ||
50 | this.joinColumns = new List<TableColumn>(); | ||
51 | this.projectionDelegates = new List<Delegate>(); | ||
52 | } | ||
53 | |||
54 | public IEnumerator<T> GetEnumerator() | ||
55 | { | ||
56 | if (this.selectColumns.Count == 0) | ||
57 | { | ||
58 | AddAllColumns(this.tables[0], this.selectColumns); | ||
59 | } | ||
60 | |||
61 | string query = this.CompileQuery(); | ||
62 | return this.InvokeQuery(query); | ||
63 | } | ||
64 | |||
65 | private string CompileQuery() | ||
66 | { | ||
67 | bool explicitTables = this.tables.Count > 1; | ||
68 | |||
69 | StringBuilder queryBuilder = new StringBuilder("SELECT"); | ||
70 | |||
71 | for (int i = 0; i < this.selectColumns.Count; i++) | ||
72 | { | ||
73 | queryBuilder.AppendFormat( | ||
74 | CultureInfo.InvariantCulture, | ||
75 | (explicitTables ? "{0} `{1}`.`{2}`" : "{0} `{2}`"), | ||
76 | (i > 0 ? "," : String.Empty), | ||
77 | this.selectColumns[i].Table.Name, | ||
78 | this.selectColumns[i].Column.Name); | ||
79 | } | ||
80 | |||
81 | for (int i = 0; i < this.tables.Count; i++) | ||
82 | { | ||
83 | queryBuilder.AppendFormat( | ||
84 | CultureInfo.InvariantCulture, | ||
85 | "{0} `{1}`", | ||
86 | (i == 0 ? " FROM" : ","), | ||
87 | this.tables[i].Name); | ||
88 | } | ||
89 | |||
90 | bool startedWhere = false; | ||
91 | for (int i = 0; i < this.joinColumns.Count - 1; i += 2) | ||
92 | { | ||
93 | queryBuilder.AppendFormat( | ||
94 | CultureInfo.InvariantCulture, | ||
95 | "{0} `{1}`.`{2}` = `{3}`.`{4}` ", | ||
96 | (i == 0 ? " WHERE" : "AND"), | ||
97 | this.joinColumns[i].Table, | ||
98 | this.joinColumns[i].Column, | ||
99 | this.joinColumns[i + 1].Table, | ||
100 | this.joinColumns[i + 1].Column); | ||
101 | startedWhere = true; | ||
102 | } | ||
103 | |||
104 | if (this.where != null) | ||
105 | { | ||
106 | queryBuilder.Append(startedWhere ? "AND " : " WHERE"); | ||
107 | queryBuilder.Append(this.where); | ||
108 | } | ||
109 | |||
110 | for (int i = 0; i < this.orderbyColumns.Count; i++) | ||
111 | { | ||
112 | VerifyOrderByColumn(this.orderbyColumns[i]); | ||
113 | |||
114 | queryBuilder.AppendFormat( | ||
115 | CultureInfo.InvariantCulture, | ||
116 | (explicitTables ? "{0} `{1}`.`{2}`" : "{0} `{2}`"), | ||
117 | (i == 0 ? " ORDER BY" : ","), | ||
118 | this.orderbyColumns[i].Table.Name, | ||
119 | this.orderbyColumns[i].Column.Name); | ||
120 | } | ||
121 | |||
122 | return queryBuilder.ToString(); | ||
123 | } | ||
124 | |||
125 | private static void VerifyOrderByColumn(TableColumn tableColumn) | ||
126 | { | ||
127 | if (tableColumn.Column.Type != typeof(int) && | ||
128 | tableColumn.Column.Type != typeof(short)) | ||
129 | { | ||
130 | throw new NotSupportedException( | ||
131 | "Cannot orderby column: " + tableColumn.Column.Name + | ||
132 | "; orderby is only supported on integer fields"); | ||
133 | } | ||
134 | } | ||
135 | |||
136 | private IEnumerator<T> InvokeQuery(string query) | ||
137 | { | ||
138 | TextWriter log = this.db.Log; | ||
139 | if (log != null) | ||
140 | { | ||
141 | log.WriteLine(); | ||
142 | log.WriteLine(query); | ||
143 | } | ||
144 | |||
145 | using (View queryView = this.db.OpenView(query)) | ||
146 | { | ||
147 | if (this.whereParameters != null && this.whereParameters.Count > 0) | ||
148 | { | ||
149 | using (Record paramsRec = this.db.CreateRecord(this.whereParameters.Count)) | ||
150 | { | ||
151 | for (int i = 0; i < this.whereParameters.Count; i++) | ||
152 | { | ||
153 | paramsRec[i + 1] = this.whereParameters[i]; | ||
154 | |||
155 | if (log != null) | ||
156 | { | ||
157 | log.WriteLine(" ? = " + this.whereParameters[i]); | ||
158 | } | ||
159 | } | ||
160 | |||
161 | queryView.Execute(paramsRec); | ||
162 | } | ||
163 | } | ||
164 | else | ||
165 | { | ||
166 | queryView.Execute(); | ||
167 | } | ||
168 | |||
169 | foreach (Record resultRec in queryView) using (resultRec) | ||
170 | { | ||
171 | yield return this.GetResult(resultRec); | ||
172 | } | ||
173 | } | ||
174 | } | ||
175 | |||
176 | private T GetResult(Record resultRec) | ||
177 | { | ||
178 | object[] results = new object[this.tables.Count]; | ||
179 | |||
180 | for (int i = 0; i < this.tables.Count; i++) | ||
181 | { | ||
182 | string[] values = new string[this.tables[i].Columns.Count]; | ||
183 | for (int j = 0; j < this.selectColumns.Count; j++) | ||
184 | { | ||
185 | TableColumn col = this.selectColumns[j]; | ||
186 | if (col.Table.Name == this.tables[i].Name) | ||
187 | { | ||
188 | int index = this.tables[i].Columns.IndexOf( | ||
189 | col.Column.Name); | ||
190 | if (index >= 0) | ||
191 | { | ||
192 | if (col.Column.Type == typeof(Stream)) | ||
193 | { | ||
194 | values[index] = "[Binary Data]"; | ||
195 | } | ||
196 | else | ||
197 | { | ||
198 | values[index] = resultRec.GetString(j + 1); | ||
199 | } | ||
200 | } | ||
201 | } | ||
202 | } | ||
203 | |||
204 | QRecord result = (QRecord) this.recordTypes[i] | ||
205 | .GetConstructor(Type.EmptyTypes).Invoke(null); | ||
206 | result.Database = this.db; | ||
207 | result.TableInfo = this.tables[i]; | ||
208 | result.Values = values; | ||
209 | result.Exists = true; | ||
210 | results[i] = result; | ||
211 | } | ||
212 | |||
213 | if (this.projectionDelegates.Count > 0) | ||
214 | { | ||
215 | object resultsProjection = results[0]; | ||
216 | for (int i = 1; i <= results.Length; i++) | ||
217 | { | ||
218 | if (i < results.Length) | ||
219 | { | ||
220 | resultsProjection = this.projectionDelegates[i - 1] | ||
221 | .DynamicInvoke(new object[] { resultsProjection, results[i] }); | ||
222 | } | ||
223 | else | ||
224 | { | ||
225 | resultsProjection = this.projectionDelegates[i - 1] | ||
226 | .DynamicInvoke(resultsProjection); | ||
227 | } | ||
228 | } | ||
229 | |||
230 | return (T) resultsProjection; | ||
231 | } | ||
232 | else | ||
233 | { | ||
234 | return (T) (object) results[0]; | ||
235 | } | ||
236 | } | ||
237 | |||
238 | IEnumerator IEnumerable.GetEnumerator() | ||
239 | { | ||
240 | return ((IEnumerable<T>) this).GetEnumerator(); | ||
241 | } | ||
242 | |||
243 | public IQueryable<TElement> CreateQuery<TElement>(Expression expression) | ||
244 | { | ||
245 | if (expression == null) | ||
246 | { | ||
247 | throw new ArgumentNullException("expression"); | ||
248 | } | ||
249 | |||
250 | Query<TElement> q = new Query<TElement>(this.db, expression); | ||
251 | q.tables.AddRange(this.tables); | ||
252 | q.recordTypes.AddRange(this.recordTypes); | ||
253 | q.selectors.AddRange(this.selectors); | ||
254 | q.where = this.where; | ||
255 | q.whereParameters.AddRange(this.whereParameters); | ||
256 | q.orderbyColumns.AddRange(this.orderbyColumns); | ||
257 | q.selectColumns.AddRange(this.selectColumns); | ||
258 | q.joinColumns.AddRange(this.joinColumns); | ||
259 | q.projectionDelegates.AddRange(this.projectionDelegates); | ||
260 | |||
261 | MethodCallExpression methodCallExpression = (MethodCallExpression) expression; | ||
262 | string methodName = methodCallExpression.Method.Name; | ||
263 | if (methodName == "Select") | ||
264 | { | ||
265 | LambdaExpression argumentExpression = (LambdaExpression) | ||
266 | ((UnaryExpression) methodCallExpression.Arguments[1]).Operand; | ||
267 | q.BuildProjection(null, argumentExpression); | ||
268 | } | ||
269 | else if (methodName == "Where") | ||
270 | { | ||
271 | LambdaExpression argumentExpression = (LambdaExpression) | ||
272 | ((UnaryExpression) methodCallExpression.Arguments[1]).Operand; | ||
273 | q.BuildQuery(null, argumentExpression); | ||
274 | } | ||
275 | else if (methodName == "ThenBy") | ||
276 | { | ||
277 | LambdaExpression argumentExpression = (LambdaExpression) | ||
278 | ((UnaryExpression) methodCallExpression.Arguments[1]).Operand; | ||
279 | q.BuildSequence(null, argumentExpression); | ||
280 | } | ||
281 | else if (methodName == "Join") | ||
282 | { | ||
283 | ConstantExpression constantExpression = (ConstantExpression) | ||
284 | methodCallExpression.Arguments[1]; | ||
285 | IQueryable inner = (IQueryable) constantExpression.Value; | ||
286 | q.PerformJoin( | ||
287 | null, | ||
288 | null, | ||
289 | inner, | ||
290 | GetJoinLambda(methodCallExpression.Arguments[2]), | ||
291 | GetJoinLambda(methodCallExpression.Arguments[3]), | ||
292 | GetJoinLambda(methodCallExpression.Arguments[4])); | ||
293 | } | ||
294 | else | ||
295 | { | ||
296 | throw new NotSupportedException( | ||
297 | "Query operation not supported: " + methodName); | ||
298 | } | ||
299 | |||
300 | return q; | ||
301 | } | ||
302 | |||
303 | public IQueryable CreateQuery(Expression expression) | ||
304 | { | ||
305 | return this.CreateQuery<T>(expression); | ||
306 | } | ||
307 | |||
308 | private static LambdaExpression GetJoinLambda(Expression expresion) | ||
309 | { | ||
310 | UnaryExpression unaryExpression = (UnaryExpression) expresion; | ||
311 | return (LambdaExpression) unaryExpression.Operand; | ||
312 | } | ||
313 | |||
314 | public TResult Execute<TResult>(Expression expression) | ||
315 | { | ||
316 | throw new NotSupportedException( | ||
317 | "Direct method calls not supported -- use AsEnumerable() instead."); | ||
318 | } | ||
319 | |||
320 | object IQueryProvider.Execute(Expression expression) | ||
321 | { | ||
322 | throw new NotSupportedException( | ||
323 | "Direct method calls not supported -- use AsEnumerable() instead."); | ||
324 | } | ||
325 | |||
326 | public IQueryProvider Provider | ||
327 | { | ||
328 | get | ||
329 | { | ||
330 | return this; | ||
331 | } | ||
332 | } | ||
333 | |||
334 | public Type ElementType | ||
335 | { | ||
336 | get | ||
337 | { | ||
338 | return typeof(T); | ||
339 | } | ||
340 | } | ||
341 | |||
342 | public Expression Expression | ||
343 | { | ||
344 | get | ||
345 | { | ||
346 | return this.queryableExpression; | ||
347 | } | ||
348 | } | ||
349 | |||
350 | internal void BuildQuery(TableInfo tableInfo, LambdaExpression expression) | ||
351 | { | ||
352 | if (tableInfo != null) | ||
353 | { | ||
354 | this.tables.Add(tableInfo); | ||
355 | this.recordTypes.Add(typeof(T)); | ||
356 | this.selectors.Add(expression.Parameters[0].Name); | ||
357 | } | ||
358 | |||
359 | StringBuilder queryBuilder = new StringBuilder(); | ||
360 | |||
361 | this.ParseQuery(expression.Body, queryBuilder); | ||
362 | |||
363 | this.where = queryBuilder.ToString(); | ||
364 | } | ||
365 | |||
366 | internal void BuildNullQuery(TableInfo tableInfo, Type recordType, LambdaExpression expression) | ||
367 | { | ||
368 | this.tables.Add(tableInfo); | ||
369 | this.recordTypes.Add(recordType); | ||
370 | this.selectors.Add(expression.Parameters[0].Name); | ||
371 | } | ||
372 | |||
373 | private void ParseQuery(Expression expression, StringBuilder queryBuilder) | ||
374 | { | ||
375 | queryBuilder.Append("("); | ||
376 | |||
377 | BinaryExpression binaryExpression; | ||
378 | UnaryExpression unaryExpression; | ||
379 | MethodCallExpression methodCallExpression; | ||
380 | |||
381 | if ((binaryExpression = expression as BinaryExpression) != null) | ||
382 | { | ||
383 | switch (binaryExpression.NodeType) | ||
384 | { | ||
385 | case ExpressionType.AndAlso: | ||
386 | this.ParseQuery(binaryExpression.Left, queryBuilder); | ||
387 | queryBuilder.Append(" AND "); | ||
388 | this.ParseQuery(binaryExpression.Right, queryBuilder); | ||
389 | break; | ||
390 | |||
391 | case ExpressionType.OrElse: | ||
392 | this.ParseQuery(binaryExpression.Left, queryBuilder); | ||
393 | queryBuilder.Append(" OR "); | ||
394 | this.ParseQuery(binaryExpression.Right, queryBuilder); | ||
395 | break; | ||
396 | |||
397 | case ExpressionType.Equal: | ||
398 | case ExpressionType.NotEqual: | ||
399 | case ExpressionType.GreaterThan: | ||
400 | case ExpressionType.LessThan: | ||
401 | case ExpressionType.GreaterThanOrEqual: | ||
402 | case ExpressionType.LessThanOrEqual: | ||
403 | this.ParseQueryCondition(binaryExpression, queryBuilder); | ||
404 | break; | ||
405 | |||
406 | default: | ||
407 | throw new NotSupportedException( | ||
408 | "Expression type not supported: " + binaryExpression.NodeType ); | ||
409 | } | ||
410 | } | ||
411 | else if ((unaryExpression = expression as UnaryExpression) != null) | ||
412 | { | ||
413 | throw new NotSupportedException( | ||
414 | "Expression type not supported: " + unaryExpression.NodeType); | ||
415 | } | ||
416 | else if ((methodCallExpression = expression as MethodCallExpression) != null) | ||
417 | { | ||
418 | throw new NotSupportedException( | ||
419 | "Method call not supported: " + methodCallExpression.Method.Name + "()"); | ||
420 | } | ||
421 | else | ||
422 | { | ||
423 | throw new NotSupportedException( | ||
424 | "Query filter expression not supported: " + expression); | ||
425 | } | ||
426 | |||
427 | queryBuilder.Append(")"); | ||
428 | } | ||
429 | |||
430 | private static ExpressionType OppositeExpression(ExpressionType e) | ||
431 | { | ||
432 | switch (e) | ||
433 | { | ||
434 | case ExpressionType.LessThan: | ||
435 | return ExpressionType.GreaterThan; | ||
436 | case ExpressionType.LessThanOrEqual: | ||
437 | return ExpressionType.GreaterThanOrEqual; | ||
438 | case ExpressionType.GreaterThan: | ||
439 | return ExpressionType.LessThan; | ||
440 | case ExpressionType.GreaterThanOrEqual: | ||
441 | return ExpressionType.LessThanOrEqual; | ||
442 | default: | ||
443 | return e; | ||
444 | } | ||
445 | } | ||
446 | |||
447 | private static bool IsIntegerType(Type t) | ||
448 | { | ||
449 | return | ||
450 | t == typeof(sbyte) || | ||
451 | t == typeof(byte) || | ||
452 | t == typeof(short) || | ||
453 | t == typeof(ushort) || | ||
454 | t == typeof(int) || | ||
455 | t == typeof(uint) || | ||
456 | t == typeof(long) || | ||
457 | t == typeof(ulong); | ||
458 | } | ||
459 | |||
460 | private void ParseQueryCondition( | ||
461 | BinaryExpression binaryExpression, StringBuilder queryBuilder) | ||
462 | { | ||
463 | bool swap; | ||
464 | string column = this.GetConditionColumn(binaryExpression, out swap); | ||
465 | queryBuilder.Append(column); | ||
466 | |||
467 | ExpressionType expressionType = binaryExpression.NodeType; | ||
468 | if (swap) | ||
469 | { | ||
470 | expressionType = OppositeExpression(expressionType); | ||
471 | } | ||
472 | |||
473 | LambdaExpression valueExpression = Expression.Lambda( | ||
474 | swap ? binaryExpression.Left : binaryExpression.Right); | ||
475 | object value = valueExpression.Compile().DynamicInvoke(); | ||
476 | |||
477 | bool valueIsInt = false; | ||
478 | if (value != null) | ||
479 | { | ||
480 | if (IsIntegerType(value.GetType())) | ||
481 | { | ||
482 | valueIsInt = true; | ||
483 | } | ||
484 | else | ||
485 | { | ||
486 | value = value.ToString(); | ||
487 | } | ||
488 | } | ||
489 | |||
490 | switch (expressionType) | ||
491 | { | ||
492 | case ExpressionType.Equal: | ||
493 | if (value == null) | ||
494 | { | ||
495 | queryBuilder.Append(" IS NULL"); | ||
496 | } | ||
497 | else if (valueIsInt) | ||
498 | { | ||
499 | queryBuilder.Append(" = "); | ||
500 | queryBuilder.Append(value); | ||
501 | } | ||
502 | else | ||
503 | { | ||
504 | queryBuilder.Append(" = ?"); | ||
505 | this.whereParameters.Add(value); | ||
506 | } | ||
507 | return; | ||
508 | |||
509 | case ExpressionType.NotEqual: | ||
510 | if (value == null) | ||
511 | { | ||
512 | queryBuilder.Append(" IS NOT NULL"); | ||
513 | } | ||
514 | else if (valueIsInt) | ||
515 | { | ||
516 | queryBuilder.Append(" <> "); | ||
517 | queryBuilder.Append(value); | ||
518 | } | ||
519 | else | ||
520 | { | ||
521 | queryBuilder.Append(" <> ?"); | ||
522 | this.whereParameters.Add(value); | ||
523 | } | ||
524 | return; | ||
525 | } | ||
526 | |||
527 | if (value == null) | ||
528 | { | ||
529 | throw new InvalidOperationException( | ||
530 | "A null value was used in a greater-than/less-than operation."); | ||
531 | } | ||
532 | |||
533 | if (!valueIsInt) | ||
534 | { | ||
535 | throw new NotSupportedException( | ||
536 | "Greater-than/less-than operators not supported on strings."); | ||
537 | } | ||
538 | |||
539 | switch (expressionType) | ||
540 | { | ||
541 | case ExpressionType.LessThan: | ||
542 | queryBuilder.Append(" < "); | ||
543 | break; | ||
544 | |||
545 | case ExpressionType.LessThanOrEqual: | ||
546 | queryBuilder.Append(" <= "); | ||
547 | break; | ||
548 | |||
549 | case ExpressionType.GreaterThan: | ||
550 | queryBuilder.Append(" > "); | ||
551 | break; | ||
552 | |||
553 | case ExpressionType.GreaterThanOrEqual: | ||
554 | queryBuilder.Append(" >= "); | ||
555 | break; | ||
556 | |||
557 | default: | ||
558 | throw new NotSupportedException( | ||
559 | "Unsupported query expression type: " + expressionType); | ||
560 | } | ||
561 | |||
562 | queryBuilder.Append(value); | ||
563 | } | ||
564 | |||
565 | private string GetConditionColumn( | ||
566 | BinaryExpression binaryExpression, out bool swap) | ||
567 | { | ||
568 | MemberExpression memberExpression; | ||
569 | MethodCallExpression methodCallExpression; | ||
570 | |||
571 | if (((memberExpression = binaryExpression.Left as MemberExpression) != null) || | ||
572 | ((binaryExpression.Left.NodeType == ExpressionType.Convert || | ||
573 | binaryExpression.Left.NodeType == ExpressionType.ConvertChecked) && | ||
574 | (memberExpression = ((UnaryExpression) binaryExpression.Left).Operand | ||
575 | as MemberExpression) != null)) | ||
576 | { | ||
577 | string column = this.GetConditionColumn(memberExpression); | ||
578 | if (column != null) | ||
579 | { | ||
580 | swap = false; | ||
581 | return column; | ||
582 | } | ||
583 | } | ||
584 | else if (((memberExpression = binaryExpression.Right as MemberExpression) != null) || | ||
585 | ((binaryExpression.Right.NodeType == ExpressionType.Convert || | ||
586 | binaryExpression.Right.NodeType == ExpressionType.ConvertChecked) && | ||
587 | (memberExpression = ((UnaryExpression) binaryExpression.Right).Operand | ||
588 | as MemberExpression) != null)) | ||
589 | { | ||
590 | string column = this.GetConditionColumn(memberExpression); | ||
591 | if (column != null) | ||
592 | { | ||
593 | swap = true; | ||
594 | return column; | ||
595 | } | ||
596 | } | ||
597 | else if ((methodCallExpression = binaryExpression.Left as MethodCallExpression) != null) | ||
598 | { | ||
599 | string column = this.GetConditionColumn(methodCallExpression); | ||
600 | if (column != null) | ||
601 | { | ||
602 | swap = false; | ||
603 | return column; | ||
604 | } | ||
605 | } | ||
606 | else if ((methodCallExpression = binaryExpression.Right as MethodCallExpression) != null) | ||
607 | { | ||
608 | string column = this.GetConditionColumn(methodCallExpression); | ||
609 | if (column != null) | ||
610 | { | ||
611 | swap = true; | ||
612 | return column; | ||
613 | } | ||
614 | } | ||
615 | |||
616 | throw new NotSupportedException( | ||
617 | "Unsupported binary expression: " + binaryExpression); | ||
618 | } | ||
619 | |||
620 | private string GetConditionColumn(MemberExpression memberExpression) | ||
621 | { | ||
622 | string columnName = GetColumnName(memberExpression.Member); | ||
623 | string selectorName = GetConditionSelectorName(memberExpression.Expression); | ||
624 | string tableName = this.GetConditionTable(selectorName, columnName); | ||
625 | return this.FormatColumn(tableName, columnName); | ||
626 | } | ||
627 | |||
628 | private string GetConditionColumn(MethodCallExpression methodCallExpression) | ||
629 | { | ||
630 | LambdaExpression argumentExpression = | ||
631 | Expression.Lambda(methodCallExpression.Arguments[0]); | ||
632 | string columnName = (string) argumentExpression.Compile().DynamicInvoke(); | ||
633 | string selectorName = GetConditionSelectorName(methodCallExpression.Object); | ||
634 | string tableName = this.GetConditionTable(selectorName, columnName); | ||
635 | return this.FormatColumn(tableName, columnName); | ||
636 | } | ||
637 | |||
638 | private static string GetConditionSelectorName(Expression expression) | ||
639 | { | ||
640 | ParameterExpression parameterExpression; | ||
641 | MemberExpression memberExpression; | ||
642 | if ((parameterExpression = expression as ParameterExpression) != null) | ||
643 | { | ||
644 | return parameterExpression.Name; | ||
645 | } | ||
646 | else if ((memberExpression = expression as MemberExpression) != null) | ||
647 | { | ||
648 | return memberExpression.Member.Name; | ||
649 | } | ||
650 | else | ||
651 | { | ||
652 | throw new NotSupportedException( | ||
653 | "Unsupported conditional selector expression: " + expression); | ||
654 | } | ||
655 | } | ||
656 | |||
657 | private string GetConditionTable(string selectorName, string columnName) | ||
658 | { | ||
659 | string tableName = null; | ||
660 | |||
661 | for (int i = 0; i < this.tables.Count; i++) | ||
662 | { | ||
663 | if (this.selectors[i] == selectorName) | ||
664 | { | ||
665 | tableName = this.tables[i].Name; | ||
666 | break; | ||
667 | } | ||
668 | } | ||
669 | |||
670 | if (tableName == null) | ||
671 | { | ||
672 | throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, | ||
673 | "Conditional expression contains column {0}.{1} " + | ||
674 | "from a table that is not in the query.", | ||
675 | selectorName, | ||
676 | columnName)); | ||
677 | } | ||
678 | |||
679 | return tableName; | ||
680 | } | ||
681 | |||
682 | private string FormatColumn(string tableName, string columnName) | ||
683 | { | ||
684 | if (tableName != null && this.tables.Count > 1) | ||
685 | { | ||
686 | return String.Format(CultureInfo.InvariantCulture, "`{0}`.`{1}`", tableName, columnName); | ||
687 | } | ||
688 | else | ||
689 | { | ||
690 | return String.Format(CultureInfo.InvariantCulture, "`{0}`", columnName); | ||
691 | } | ||
692 | } | ||
693 | |||
694 | private static string GetColumnName(MemberInfo memberInfo) | ||
695 | { | ||
696 | foreach (var attr in memberInfo.GetCustomAttributes( | ||
697 | typeof(DatabaseColumnAttribute), false)) | ||
698 | { | ||
699 | return ((DatabaseColumnAttribute) attr).Column; | ||
700 | } | ||
701 | |||
702 | return memberInfo.Name; | ||
703 | } | ||
704 | |||
705 | internal void BuildProjection(TableInfo tableInfo, LambdaExpression expression) | ||
706 | { | ||
707 | if (tableInfo != null) | ||
708 | { | ||
709 | this.tables.Add(tableInfo); | ||
710 | this.recordTypes.Add(typeof(T)); | ||
711 | this.selectors.Add(expression.Parameters[0].Name); | ||
712 | } | ||
713 | |||
714 | this.FindColumns(expression, this.selectColumns); | ||
715 | this.projectionDelegates.Add(expression.Compile()); | ||
716 | } | ||
717 | |||
718 | internal void BuildSequence(TableInfo tableInfo, LambdaExpression expression) | ||
719 | { | ||
720 | if (tableInfo != null) | ||
721 | { | ||
722 | this.tables.Add(tableInfo); | ||
723 | this.recordTypes.Add(typeof(T)); | ||
724 | this.selectors.Add(expression.Parameters[0].Name); | ||
725 | } | ||
726 | |||
727 | this.FindColumns(expression.Body, this.orderbyColumns); | ||
728 | } | ||
729 | |||
730 | private static void AddAllColumns(TableInfo tableInfo, IList<TableColumn> columnList) | ||
731 | { | ||
732 | foreach (ColumnInfo column in tableInfo.Columns) | ||
733 | { | ||
734 | columnList.Add(new TableColumn(tableInfo, column)); | ||
735 | } | ||
736 | } | ||
737 | |||
738 | [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")] | ||
739 | private void FindColumns(Expression expression, IList<TableColumn> columnList) | ||
740 | { | ||
741 | if (expression is ParameterExpression) | ||
742 | { | ||
743 | ParameterExpression e = expression as ParameterExpression; | ||
744 | string selector = e.Name; | ||
745 | for (int i = 0; i < this.tables.Count; i++) | ||
746 | { | ||
747 | if (this.selectors[i] == selector) | ||
748 | { | ||
749 | AddAllColumns(this.tables[i], columnList); | ||
750 | break; | ||
751 | } | ||
752 | } | ||
753 | } | ||
754 | else if (expression.NodeType == ExpressionType.MemberAccess) | ||
755 | { | ||
756 | this.FindColumns(expression as MemberExpression, columnList); | ||
757 | } | ||
758 | else if (expression is MethodCallExpression) | ||
759 | { | ||
760 | this.FindColumns(expression as MethodCallExpression, columnList); | ||
761 | } | ||
762 | else if (expression is BinaryExpression) | ||
763 | { | ||
764 | BinaryExpression e = expression as BinaryExpression; | ||
765 | this.FindColumns(e.Left, columnList); | ||
766 | this.FindColumns(e.Right, columnList); | ||
767 | } | ||
768 | else if (expression is UnaryExpression) | ||
769 | { | ||
770 | UnaryExpression e = expression as UnaryExpression; | ||
771 | this.FindColumns(e.Operand, columnList); | ||
772 | } | ||
773 | else if (expression is ConditionalExpression) | ||
774 | { | ||
775 | ConditionalExpression e = expression as ConditionalExpression; | ||
776 | this.FindColumns(e.Test, columnList); | ||
777 | this.FindColumns(e.IfTrue, columnList); | ||
778 | this.FindColumns(e.IfFalse, columnList); | ||
779 | } | ||
780 | else if (expression is InvocationExpression) | ||
781 | { | ||
782 | InvocationExpression e = expression as InvocationExpression; | ||
783 | this.FindColumns(e.Expression, columnList); | ||
784 | this.FindColumns(e.Arguments, columnList); | ||
785 | } | ||
786 | else if (expression is LambdaExpression) | ||
787 | { | ||
788 | LambdaExpression e = expression as LambdaExpression; | ||
789 | this.FindColumns(e.Body, columnList); | ||
790 | } | ||
791 | else if (expression is ListInitExpression) | ||
792 | { | ||
793 | ListInitExpression e = expression as ListInitExpression; | ||
794 | this.FindColumns(e.NewExpression, columnList); | ||
795 | foreach (ElementInit ei in e.Initializers) | ||
796 | { | ||
797 | this.FindColumns(ei.Arguments, columnList); | ||
798 | } | ||
799 | } | ||
800 | else if (expression is MemberInitExpression) | ||
801 | { | ||
802 | MemberInitExpression e = expression as MemberInitExpression; | ||
803 | this.FindColumns(e.NewExpression, columnList); | ||
804 | foreach (MemberAssignment b in e.Bindings) | ||
805 | { | ||
806 | this.FindColumns(b.Expression, columnList); | ||
807 | } | ||
808 | } | ||
809 | else if (expression is NewExpression) | ||
810 | { | ||
811 | NewExpression e = expression as NewExpression; | ||
812 | this.FindColumns(e.Arguments, columnList); | ||
813 | } | ||
814 | else if (expression is NewArrayExpression) | ||
815 | { | ||
816 | NewArrayExpression e = expression as NewArrayExpression; | ||
817 | this.FindColumns(e.Expressions, columnList); | ||
818 | } | ||
819 | else if (expression is TypeBinaryExpression) | ||
820 | { | ||
821 | TypeBinaryExpression e = expression as TypeBinaryExpression; | ||
822 | this.FindColumns(e.Expression, columnList); | ||
823 | } | ||
824 | } | ||
825 | |||
826 | private void FindColumns(IEnumerable<Expression> expressions, IList<TableColumn> columnList) | ||
827 | { | ||
828 | foreach (Expression expression in expressions) | ||
829 | { | ||
830 | this.FindColumns(expression, columnList); | ||
831 | } | ||
832 | } | ||
833 | |||
834 | private void FindColumns(MemberExpression memberExpression, IList<TableColumn> columnList) | ||
835 | { | ||
836 | string selector = null; | ||
837 | MemberExpression objectMemberExpression; | ||
838 | ParameterExpression objectParameterExpression; | ||
839 | if ((objectParameterExpression = memberExpression.Expression as | ||
840 | ParameterExpression) != null) | ||
841 | { | ||
842 | selector = objectParameterExpression.Name; | ||
843 | } | ||
844 | else if ((objectMemberExpression = memberExpression.Expression as | ||
845 | MemberExpression) != null) | ||
846 | { | ||
847 | selector = objectMemberExpression.Member.Name; | ||
848 | } | ||
849 | |||
850 | if (selector != null) | ||
851 | { | ||
852 | for (int i = 0; i < this.tables.Count; i++) | ||
853 | { | ||
854 | if (this.selectors[i] == selector) | ||
855 | { | ||
856 | string columnName = GetColumnName(memberExpression.Member); | ||
857 | ColumnInfo column = this.tables[i].Columns[columnName]; | ||
858 | columnList.Add(new TableColumn(this.tables[i], column)); | ||
859 | break; | ||
860 | } | ||
861 | } | ||
862 | } | ||
863 | |||
864 | selector = memberExpression.Member.Name; | ||
865 | for (int i = 0; i < this.tables.Count; i++) | ||
866 | { | ||
867 | if (this.selectors[i] == selector) | ||
868 | { | ||
869 | AddAllColumns(this.tables[i], columnList); | ||
870 | break; | ||
871 | } | ||
872 | } | ||
873 | } | ||
874 | |||
875 | private void FindColumns(MethodCallExpression methodCallExpression, IList<TableColumn> columnList) | ||
876 | { | ||
877 | if (methodCallExpression.Method.Name == "get_Item" && | ||
878 | methodCallExpression.Arguments.Count == 1 && | ||
879 | methodCallExpression.Arguments[0].Type == typeof(string)) | ||
880 | { | ||
881 | string selector = null; | ||
882 | MemberExpression objectMemberExpression; | ||
883 | ParameterExpression objectParameterExpression; | ||
884 | if ((objectParameterExpression = methodCallExpression.Object as ParameterExpression) != null) | ||
885 | { | ||
886 | selector = objectParameterExpression.Name; | ||
887 | } | ||
888 | else if ((objectMemberExpression = methodCallExpression.Object as MemberExpression) != null) | ||
889 | { | ||
890 | selector = objectMemberExpression.Member.Name; | ||
891 | } | ||
892 | |||
893 | if (selector != null) | ||
894 | { | ||
895 | for (int i = 0; i < this.tables.Count; i++) | ||
896 | { | ||
897 | if (this.selectors[i] == selector) | ||
898 | { | ||
899 | LambdaExpression argumentExpression = | ||
900 | Expression.Lambda(methodCallExpression.Arguments[0]); | ||
901 | string columnName = (string) | ||
902 | argumentExpression.Compile().DynamicInvoke(); | ||
903 | ColumnInfo column = this.tables[i].Columns[columnName]; | ||
904 | columnList.Add(new TableColumn(this.tables[i], column)); | ||
905 | break; | ||
906 | } | ||
907 | } | ||
908 | } | ||
909 | } | ||
910 | |||
911 | if (methodCallExpression.Object != null && methodCallExpression.Object.NodeType != ExpressionType.Parameter) | ||
912 | { | ||
913 | this.FindColumns(methodCallExpression.Object, columnList); | ||
914 | } | ||
915 | } | ||
916 | |||
917 | internal void PerformJoin( | ||
918 | TableInfo tableInfo, | ||
919 | Type recordType, | ||
920 | IQueryable joinTable, | ||
921 | LambdaExpression outerKeySelector, | ||
922 | LambdaExpression innerKeySelector, | ||
923 | LambdaExpression resultSelector) | ||
924 | { | ||
925 | if (joinTable == null) | ||
926 | { | ||
927 | throw new ArgumentNullException("joinTable"); | ||
928 | } | ||
929 | |||
930 | if (tableInfo != null) | ||
931 | { | ||
932 | this.tables.Add(tableInfo); | ||
933 | this.recordTypes.Add(recordType); | ||
934 | this.selectors.Add(outerKeySelector.Parameters[0].Name); | ||
935 | } | ||
936 | |||
937 | PropertyInfo tableInfoProp = joinTable.GetType().GetProperty("TableInfo"); | ||
938 | if (tableInfoProp == null) | ||
939 | { | ||
940 | throw new NotSupportedException( | ||
941 | "Cannot join with object: " + joinTable.GetType().Name + | ||
942 | "; join is only supported on another QTable."); | ||
943 | } | ||
944 | |||
945 | TableInfo joinTableInfo = (TableInfo) tableInfoProp.GetValue(joinTable, null); | ||
946 | if (joinTableInfo == null) | ||
947 | { | ||
948 | throw new InvalidOperationException("Missing join table info."); | ||
949 | } | ||
950 | |||
951 | this.tables.Add(joinTableInfo); | ||
952 | this.recordTypes.Add(joinTable.ElementType); | ||
953 | this.selectors.Add(innerKeySelector.Parameters[0].Name); | ||
954 | this.projectionDelegates.Add(resultSelector.Compile()); | ||
955 | |||
956 | int joinColumnCount = this.joinColumns.Count; | ||
957 | this.FindColumns(outerKeySelector.Body, this.joinColumns); | ||
958 | if (this.joinColumns.Count > joinColumnCount + 1) | ||
959 | { | ||
960 | throw new NotSupportedException("Join operations involving " + | ||
961 | "multiple columns are not supported."); | ||
962 | } | ||
963 | else if (this.joinColumns.Count != joinColumnCount + 1) | ||
964 | { | ||
965 | throw new InvalidOperationException("Bad outer key selector for join."); | ||
966 | } | ||
967 | |||
968 | this.FindColumns(innerKeySelector.Body, this.joinColumns); | ||
969 | if (this.joinColumns.Count > joinColumnCount + 2) | ||
970 | { | ||
971 | throw new NotSupportedException("Join operations involving " + | ||
972 | "multiple columns not are supported."); | ||
973 | } | ||
974 | if (this.joinColumns.Count != joinColumnCount + 2) | ||
975 | { | ||
976 | throw new InvalidOperationException("Bad inner key selector for join."); | ||
977 | } | ||
978 | } | ||
979 | } | ||
980 | |||
981 | internal class TableColumn | ||
982 | { | ||
983 | public TableColumn(TableInfo table, ColumnInfo column) | ||
984 | { | ||
985 | this.Table = table; | ||
986 | this.Column = column; | ||
987 | } | ||
988 | |||
989 | public TableInfo Table { get; set; } | ||
990 | public ColumnInfo Column { get; set; } | ||
991 | } | ||
992 | } | ||
diff --git a/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/WixToolset.Dtf.WindowsInstaller.Linq.csproj b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/WixToolset.Dtf.WindowsInstaller.Linq.csproj new file mode 100644 index 00000000..b4587071 --- /dev/null +++ b/src/dtf/WixToolset.Dtf.WindowsInstaller.Linq/WixToolset.Dtf.WindowsInstaller.Linq.csproj | |||
@@ -0,0 +1,21 @@ | |||
1 | <?xml version="1.0" encoding="utf-8"?> | ||
2 | <!-- Copyright (c) .NET Foundation and contributors. All rights reserved. Licensed under the Microsoft Reciprocal License. See LICENSE.TXT file in the project root for full license information. --> | ||
3 | |||
4 | <Project Sdk="Microsoft.NET.Sdk"> | ||
5 | <PropertyGroup> | ||
6 | <RootNamespace>WixToolset.Dtf.WindowsInstaller.Linq</RootNamespace> | ||
7 | <AssemblyName>WixToolset.Dtf.WindowsInstaller.Linq</AssemblyName> | ||
8 | <TargetFrameworks>netstandard2.0;net35</TargetFrameworks> | ||
9 | <Description>LINQ extensions for Windows Installer classes</Description> | ||
10 | <CreateDocumentationFile>true</CreateDocumentationFile> | ||
11 | </PropertyGroup> | ||
12 | |||
13 | <ItemGroup> | ||
14 | <ProjectReference Include="..\WixToolset.Dtf.WindowsInstaller\WixToolset.Dtf.WindowsInstaller.csproj" /> | ||
15 | </ItemGroup> | ||
16 | |||
17 | <ItemGroup> | ||
18 | <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> | ||
19 | <PackageReference Include="Nerdbank.GitVersioning" Version="3.3.37" PrivateAssets="All" /> | ||
20 | </ItemGroup> | ||
21 | </Project> | ||