-
Notifications
You must be signed in to change notification settings - Fork 15
/
OData4DynamicDriver.cs
308 lines (253 loc) · 12.7 KB
/
OData4DynamicDriver.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text;
using System.Xml;
using LINQPad.Extensibility.DataContext;
using Microsoft.CSharp;
using Microsoft.OData.Client;
using Microsoft.OData.Edm;
using Microsoft.OData.Edm.Csdl;
using OData4.Builder;
namespace OData4
{
public class OData4DynamicDriver : DynamicDataContextDriver
{
/// <summary> System assemblies, using to compile generated odata client code </summary>
private static readonly string[] SystemAssemblies =
{
"System.dll",
"System.Core.dll",
"System.Xml.dll"
};
/// <summary> Assemblies, using both to compile generated code and to load into LINQPad </summary>
private static readonly string[] Assemblies = {
"Microsoft.OData.Client.dll",
"Microsoft.OData.Core.dll",
"Microsoft.OData.Edm.dll",
"Microsoft.Spatial.dll"
};
public override string Name => "OData 4";
public override string Author => "Dmitrii Smirnov";
public override string GetConnectionDescription(IConnectionInfo cxInfo)
{
return cxInfo.DatabaseInfo.Server;
}
public override Version Version => new Version("1.0.1.0");
public override ParameterDescriptor[] GetContextConstructorParameters(IConnectionInfo cxInfo)
{
// We need to pass the chosen URI into the DataServiceContext's constructor:
return new[] { new ParameterDescriptor("serviceRoot", "System.Uri") };
}
public override object[] GetContextConstructorArguments(IConnectionInfo cxInfo)
{
// We need to pass the chosen URI into the DataServiceContext's constructor:
return new object[] { new Uri(cxInfo.DatabaseInfo.Server) };
}
public override IEnumerable<string> GetAssembliesToAdd(IConnectionInfo cxInfo)
{
// We need the following assembly for compiliation and autocompletion:
return Assemblies;
}
public override IDbConnection GetIDbConnection(IConnectionInfo cxInfo)
{
return null;
}
public override IEnumerable<string> GetNamespacesToAdd(IConnectionInfo cxInfo)
{
return new[] { "Microsoft.OData.Client" };
}
public override bool ShowConnectionDialog(IConnectionInfo cxInfo, bool isNewConnection)
{
// Populate the default URI with a demo value:
if (isNewConnection)
{
cxInfo.DatabaseInfo.Server = "http://services.odata.org/V4/OData/OData.svc/";
}
return new ConnectionDialog(cxInfo).ShowDialog() == true;
}
private static NetworkCredential GetCredentials(IConnectionInfo cxInfo)
{
return (cxInfo.DatabaseInfo.UserName.Length > 0)
? new NetworkCredential(cxInfo.DatabaseInfo.UserName, cxInfo.DatabaseInfo.Password)
: CredentialCache.DefaultNetworkCredentials;
}
public override List<ExplorerItem> GetSchemaAndBuildAssembly(IConnectionInfo cxInfo, AssemblyName assemblyToBuild, ref string nameSpace, ref string typeName)
{
// using code from Microsoft's OData v4 Client Code Generator. see https://visualstudiogallery.msdn.microsoft.com/9b786c0e-79d1-4a50-89a5-125e57475937
var client = new ODataClient(new Configuration(cxInfo.DatabaseInfo.Server, nameSpace));
var code = client.GenerateCode();
// Compile the code into the assembly, using the assembly name provided:
var assembly = BuildAssembly(code, assemblyToBuild);
typeName = GetContainerName(cxInfo);
var containerName = string.Concat(nameSpace, ".", typeName);
var containerType = assembly.GetType(containerName);
// Use the schema to populate the Schema Explorer:
var schema = GetSchema(containerType, assembly);
return schema;
}
private Assembly BuildAssembly(string code, AssemblyName assemblyToBuild)
{
// Use the CSharpCodeProvider to compile the generated code:
CompilerResults results;
using (var codeProvider = new CSharpCodeProvider(new Dictionary<string, string> { { "CompilerVersion", "v4.0" } }))
{
// transform path to assemblies
var assemblies = Assemblies.Select(o => Path.Combine(GetDriverFolder(), o)).Union(SystemAssemblies).ToArray();
var options = new CompilerParameters(assemblies, assemblyToBuild.CodeBase, true);
results = codeProvider.CompileAssemblyFromSource(options, code);
}
if (results.Errors.Count > 0)
{
throw new Exception($"Cannot compile typed context: {results.Errors[0].ErrorText} (line {results.Errors[0].Line})");
}
return results.CompiledAssembly;
}
/// <summary> Get main schema container name for given service uri </summary>
/// <param name="cxInfo">connection info</param>
/// <returns>Container name</returns>
static string GetContainerName(IConnectionInfo cxInfo)
{
// odata v4 client inherites from DataServiceContext, use it for get IEdmModel
IEdmModel model;
var context = new DataServiceContext(new Uri(cxInfo.DatabaseInfo.Server))
{
Credentials = GetCredentials(cxInfo)
};
var metadataUri = context.GetMetadataUri();
using (var reader = XmlReader.Create(metadataUri.ToString()))
{
model = EdmxReader.Parse(reader);
}
var root = model.SchemaElements.OfType<IEdmEntityContainer>().Single();
// FIX ?
var containerName = model.DeclaredNamespaces.Count() > 1 ? root.FullName() : root.Name;
return containerName;
}
// TODO: parse service IEdmModel instead of generated assembly
private static List<ExplorerItem> GetSchema(Type customType, Assembly assembly)
{
var list = customType.GetProperties()
.Where(o => o.DeclaringType == customType &&
typeof(IQueryable).IsAssignableFrom(o.PropertyType))
.OrderBy(o => o.Name)
.Select(o => new ExplorerItem(o.Name, ExplorerItemKind.QueryableObject, ExplorerIcon.Table)
{
IsEnumerable = true,
ToolTipText = o.PropertyType.Name,
// Store the entity type to the Tag property. We'll use it later.
Tag = o.PropertyType.GetGenericArguments()[0]
})
.ToList();
// Create a lookup keying each element type to the properties of that type. This will allow
// us to build hyperlink targets allowing the user to click between associations:
var elementTypeLookup = list.ToLookup(tp => (Type)tp.Tag);
// Populate the columns (properties) of each entity:
foreach (var table in list)
{
var parentType = (Type)table.Tag;
table.Children = parentType.GetProperties()
.Select(childProp => GetChildItem(elementTypeLookup, childProp, parentType, assembly))
.OrderBy(childItem => childItem.Kind)
.ToList();
}
return list;
}
// TODO: simplificate ugly code
private static ExplorerItem GetChildItem(ILookup<Type, ExplorerItem> elementTypeLookup, PropertyInfo childProp, Type parentType, Assembly assembly)
{
ExplorerIcon icon;
// If the property's type is in our list of entities, then it's a Many:1 (or 1:1) reference.
// We'll assume it's a Many:1 (we can't reliably identify 1:1s purely from reflection).
if (elementTypeLookup.Contains(childProp.PropertyType))
{
icon = ExplorerIcon.ManyToOne;
if (childProp.PropertyType.GetProperties().Any(o => o.PropertyType == parentType))
{
icon = ExplorerIcon.OneToOne;
}
return new ExplorerItem(childProp.Name, ExplorerItemKind.ReferenceLink, icon)
{
HyperlinkTarget = elementTypeLookup[childProp.PropertyType].First(),
// FormatTypeName is a helper method that returns a nicely formatted type name.
ToolTipText = childProp.PropertyType.Name
};
}
// Is the property's type a collection of entities?
var ienumerableOfT = childProp.PropertyType.GetInterface("System.Collections.Generic.IEnumerable`1");
if (ienumerableOfT != null)
{
var elementType = ienumerableOfT.GetGenericArguments().First();
if (elementTypeLookup.Contains(elementType))
{
icon = ExplorerIcon.OneToMany;
if (elementType.GetProperties().Any(o => o.PropertyType.IsGenericType && o.PropertyType.GetGenericArguments().First() == parentType))
{
icon = ExplorerIcon.ManyToMany;
}
return new ExplorerItem(childProp.Name, ExplorerItemKind.CollectionLink, icon)
{
HyperlinkTarget = elementTypeLookup[elementType].First(),
ToolTipText = elementType.Name
};
}
}
// Ordinary property:
icon = ExplorerIcon.Column;
var keyAttribute = childProp.DeclaringType.GetCustomAttribute<KeyAttribute>();
if (keyAttribute != null && keyAttribute.KeyNames.Contains(childProp.Name))
{
icon = ExplorerIcon.Key;
}
if (childProp.PropertyType.Assembly == assembly)
{
// type in our dynamic data context assembly
return new ExplorerItem(childProp.Name, ExplorerItemKind.ReferenceLink, ExplorerIcon.OneToOne);
}
// system data type (or not listed in model?)
var propertyTypeStr = GetFullTypeName(childProp.PropertyType);
return new ExplorerItem(childProp.Name + " (" + (propertyTypeStr) + ")", ExplorerItemKind.Property, icon);
}
private static string GetFullTypeName(Type t)
{
if (!t.IsGenericType)
{
return t.Name;
}
if (t.GetGenericTypeDefinition() == typeof(Nullable<>))
{
return t.GetGenericArguments().Single() + "?";
}
var sb = new StringBuilder();
sb.Append(t.Name.Substring(0, t.Name.LastIndexOf("`", StringComparison.Ordinal)));
var i = 0;
t.GetGenericArguments().Aggregate(sb, (a, type) => a.Append((i++ == 0 ? "<" : ",")).Append(GetFullTypeName(type)));
sb.Append(">");
return sb.ToString();
}
public override void InitializeContext(IConnectionInfo cxInfo, object context, QueryExecutionManager executionManager)
{
var dsContext = (DataServiceContext)context;
// The next step is to feed any supplied credentials into the Astoria service.
// (This could be enhanced to support other authentication modes, too).
dsContext.Credentials = GetCredentials(cxInfo);
dsContext.Configurations.RequestPipeline.OnMessageCreating += args =>
{
var message = new CustomizedRequestMessage(args);
return message;
};
// Finally, we handle the SendingRequest event so that it writes the request text to the SQL translation window:
dsContext.SendingRequest2 += (s, e) => executionManager.SqlTranslationWriter.WriteLine(e.RequestMessage.Url);
}
public override bool AreRepositoriesEquivalent(IConnectionInfo r1, IConnectionInfo r2)
{
// Two repositories point to the same endpoint if their URIs are the same.
return Equals(r1.DatabaseInfo.Server, r2.DatabaseInfo.Server);
}
}
}