From ea5703968932939d4871286c9fa84bc9c020d907 Mon Sep 17 00:00:00 2001 From: mikepizzo Date: Wed, 13 Sep 2023 07:43:12 -0700 Subject: [PATCH] Add in RESTier-based DynamicService Compiles, but doesn't yet work. OData fails trying to get IEdmModel from DI. --- App.sln | 69 +++- .../DataSourceManager.csproj | 19 ++ .../DefaultDataStoreManager.cs | 168 ++++++++++ .../DataStoreManager/IDataStoreManager.cs | 27 ++ .../SingleDataStoreManager.cs | 28 ++ .../Properties/AssemblyInfo.cs | 36 +++ .../Properties/Resources.Designer.cs | 72 +++++ .../Properties/Resources.resx | 123 +++++++ .../Properties/launchSettings.json | 27 ++ .../Submit/ChangeSetInitializer.cs | 304 ++++++++++++++++++ .../Submit/SubmitExecutor.cs | 17 + .../Utils/InMemoryProviderUtils.cs | 30 ++ .../Utils/ODataSessionIdManager.cs | 74 +++++ ODataService/DynamicService/Api/DynamicApi.cs | 56 ++++ .../DynamicService/App_Data/NWind.csdl | 125 +++++++ .../DynamicService/App_Data/Trippin.csdl | 159 +++++++++ .../DynamicService/App_Start/Startup.cs | 204 ++++++++++++ .../Controllers/DynamicApiController.cs | 29 ++ ODataService/DynamicService/DynamicHelper.cs | 250 ++++++++++++++ .../DynamicService/DynamicODataRoute.cs | 81 +++++ .../DynamicService/DynamicRouteConstraint.cs | 220 +++++++++++++ .../DynamicService/DynamicService.csproj | 29 ++ ODataService/DynamicService/Helpers.cs | 43 +++ ODataService/DynamicService/Program.cs | 33 ++ .../Properties/launchSettings.json | 37 +++ .../Submit/CustomizedSubmitProcessor.cs | 67 ++++ .../appsettings.Development.json | 9 + ODataService/DynamicService/appsettings.json | 9 + ODataServiceTests/DynamicDataSourceTests.cs | 126 ++++++++ 29 files changed, 2470 insertions(+), 1 deletion(-) create mode 100644 ODataService/DataSourceManager/DataSourceManager.csproj create mode 100644 ODataService/DataSourceManager/DataStoreManager/DefaultDataStoreManager.cs create mode 100644 ODataService/DataSourceManager/DataStoreManager/IDataStoreManager.cs create mode 100644 ODataService/DataSourceManager/DataStoreManager/SingleDataStoreManager.cs create mode 100644 ODataService/DataSourceManager/Properties/AssemblyInfo.cs create mode 100644 ODataService/DataSourceManager/Properties/Resources.Designer.cs create mode 100644 ODataService/DataSourceManager/Properties/Resources.resx create mode 100644 ODataService/DataSourceManager/Properties/launchSettings.json create mode 100644 ODataService/DataSourceManager/Submit/ChangeSetInitializer.cs create mode 100644 ODataService/DataSourceManager/Submit/SubmitExecutor.cs create mode 100644 ODataService/DataSourceManager/Utils/InMemoryProviderUtils.cs create mode 100644 ODataService/DataSourceManager/Utils/ODataSessionIdManager.cs create mode 100644 ODataService/DynamicService/Api/DynamicApi.cs create mode 100644 ODataService/DynamicService/App_Data/NWind.csdl create mode 100644 ODataService/DynamicService/App_Data/Trippin.csdl create mode 100644 ODataService/DynamicService/App_Start/Startup.cs create mode 100644 ODataService/DynamicService/Controllers/DynamicApiController.cs create mode 100644 ODataService/DynamicService/DynamicHelper.cs create mode 100644 ODataService/DynamicService/DynamicODataRoute.cs create mode 100644 ODataService/DynamicService/DynamicRouteConstraint.cs create mode 100644 ODataService/DynamicService/DynamicService.csproj create mode 100644 ODataService/DynamicService/Helpers.cs create mode 100644 ODataService/DynamicService/Program.cs create mode 100644 ODataService/DynamicService/Properties/launchSettings.json create mode 100644 ODataService/DynamicService/Submit/CustomizedSubmitProcessor.cs create mode 100644 ODataService/DynamicService/appsettings.Development.json create mode 100644 ODataService/DynamicService/appsettings.json create mode 100644 ODataServiceTests/DynamicDataSourceTests.cs diff --git a/App.sln b/App.sln index 289fb8aeb6..0e7775d7bd 100644 --- a/App.sln +++ b/App.sln @@ -15,36 +15,96 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ODataServiceTests", "ODataS EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataSourceGenerator", "ODataService\DataSourceGenerator\DataSourceGenerator.csproj", "{E6271DA9-A12E-4484-AC50-43F55C1F2DF5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GeneratedDataSourceTests", "GeneratedDataSourceTests\GeneratedDataSourceTests.csproj", "{7DECA341-442D-41D5-89B4-B3BCD3676758}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeneratedDataSourceTests", "GeneratedDataSourceTests\GeneratedDataSourceTests.csproj", "{7DECA341-442D-41D5-89B4-B3BCD3676758}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Project2", "Project2\Project2.csproj", "{7D4313F7-300D-405A-A92B-285EE1045FAC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ApiService", "ODataService\ODataService\ApiService.csproj", "{B379640E-9064-438D-8DA5-6F7B394C2C46}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RESTier", "RESTier", "{0193DBDA-F7D5-4A71-8097-13D74DF3DE58}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Restier.Core", "ODataService\RESTier\src\Microsoft.Restier.Core\Microsoft.Restier.Core.csproj", "{052E17C4-C151-4964-A873-979682F8619B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Restier.AspNet", "ODataService\RESTier\src\Microsoft.Restier.AspNet\Microsoft.Restier.AspNet.csproj", "{573D523D-AF49-460B-98C7-AB9FB930AC60}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNet.OData", "ODataService\WebAPI\src\Microsoft.AspNet.OData\Microsoft.AspNet.OData.csproj", "{A6F9775D-F7E2-424E-8363-79644A73038F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DynamicService", "ODataService\DynamicService\DynamicService.csproj", "{E8072054-3D92-4321-86B1-E038616AAA3E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataSourceManager", "ODataService\DataSourceManager\DataSourceManager.csproj", "{BED3CAAD-C9A8-4F29-B35A-3D73A9DECE87}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + CodeAnalysis|Any CPU = CodeAnalysis|Any CPU Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {413A6974-270D-4F33-BA5D-BDB2080F684C}.CodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU + {413A6974-270D-4F33-BA5D-BDB2080F684C}.CodeAnalysis|Any CPU.Build.0 = Release|Any CPU {413A6974-270D-4F33-BA5D-BDB2080F684C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {413A6974-270D-4F33-BA5D-BDB2080F684C}.Debug|Any CPU.Build.0 = Debug|Any CPU {413A6974-270D-4F33-BA5D-BDB2080F684C}.Release|Any CPU.ActiveCfg = Release|Any CPU {413A6974-270D-4F33-BA5D-BDB2080F684C}.Release|Any CPU.Build.0 = Release|Any CPU + {1978960D-EFF1-4AB1-902B-0DE7BEADA5A1}.CodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU + {1978960D-EFF1-4AB1-902B-0DE7BEADA5A1}.CodeAnalysis|Any CPU.Build.0 = Release|Any CPU {1978960D-EFF1-4AB1-902B-0DE7BEADA5A1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1978960D-EFF1-4AB1-902B-0DE7BEADA5A1}.Debug|Any CPU.Build.0 = Debug|Any CPU {1978960D-EFF1-4AB1-902B-0DE7BEADA5A1}.Release|Any CPU.ActiveCfg = Release|Any CPU {1978960D-EFF1-4AB1-902B-0DE7BEADA5A1}.Release|Any CPU.Build.0 = Release|Any CPU + {E6271DA9-A12E-4484-AC50-43F55C1F2DF5}.CodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU + {E6271DA9-A12E-4484-AC50-43F55C1F2DF5}.CodeAnalysis|Any CPU.Build.0 = Release|Any CPU {E6271DA9-A12E-4484-AC50-43F55C1F2DF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E6271DA9-A12E-4484-AC50-43F55C1F2DF5}.Debug|Any CPU.Build.0 = Debug|Any CPU {E6271DA9-A12E-4484-AC50-43F55C1F2DF5}.Release|Any CPU.ActiveCfg = Release|Any CPU {E6271DA9-A12E-4484-AC50-43F55C1F2DF5}.Release|Any CPU.Build.0 = Release|Any CPU + {7DECA341-442D-41D5-89B4-B3BCD3676758}.CodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU + {7DECA341-442D-41D5-89B4-B3BCD3676758}.CodeAnalysis|Any CPU.Build.0 = Release|Any CPU {7DECA341-442D-41D5-89B4-B3BCD3676758}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7DECA341-442D-41D5-89B4-B3BCD3676758}.Debug|Any CPU.Build.0 = Debug|Any CPU {7DECA341-442D-41D5-89B4-B3BCD3676758}.Release|Any CPU.ActiveCfg = Release|Any CPU {7DECA341-442D-41D5-89B4-B3BCD3676758}.Release|Any CPU.Build.0 = Release|Any CPU + {7D4313F7-300D-405A-A92B-285EE1045FAC}.CodeAnalysis|Any CPU.ActiveCfg = Release|Any CPU {7D4313F7-300D-405A-A92B-285EE1045FAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7D4313F7-300D-405A-A92B-285EE1045FAC}.Debug|Any CPU.Build.0 = Debug|Any CPU {7D4313F7-300D-405A-A92B-285EE1045FAC}.Release|Any CPU.ActiveCfg = Release|Any CPU {7D4313F7-300D-405A-A92B-285EE1045FAC}.Release|Any CPU.Build.0 = Release|Any CPU + {B379640E-9064-438D-8DA5-6F7B394C2C46}.CodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU + {B379640E-9064-438D-8DA5-6F7B394C2C46}.CodeAnalysis|Any CPU.Build.0 = Debug|Any CPU + {B379640E-9064-438D-8DA5-6F7B394C2C46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B379640E-9064-438D-8DA5-6F7B394C2C46}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B379640E-9064-438D-8DA5-6F7B394C2C46}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B379640E-9064-438D-8DA5-6F7B394C2C46}.Release|Any CPU.Build.0 = Release|Any CPU + {052E17C4-C151-4964-A873-979682F8619B}.CodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU + {052E17C4-C151-4964-A873-979682F8619B}.CodeAnalysis|Any CPU.Build.0 = Debug|Any CPU + {052E17C4-C151-4964-A873-979682F8619B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {052E17C4-C151-4964-A873-979682F8619B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {052E17C4-C151-4964-A873-979682F8619B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {052E17C4-C151-4964-A873-979682F8619B}.Release|Any CPU.Build.0 = Release|Any CPU + {573D523D-AF49-460B-98C7-AB9FB930AC60}.CodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU + {573D523D-AF49-460B-98C7-AB9FB930AC60}.CodeAnalysis|Any CPU.Build.0 = Debug|Any CPU + {573D523D-AF49-460B-98C7-AB9FB930AC60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {573D523D-AF49-460B-98C7-AB9FB930AC60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {573D523D-AF49-460B-98C7-AB9FB930AC60}.Release|Any CPU.ActiveCfg = Release|Any CPU + {573D523D-AF49-460B-98C7-AB9FB930AC60}.Release|Any CPU.Build.0 = Release|Any CPU + {A6F9775D-F7E2-424E-8363-79644A73038F}.CodeAnalysis|Any CPU.ActiveCfg = CodeAnalysis|Any CPU + {A6F9775D-F7E2-424E-8363-79644A73038F}.CodeAnalysis|Any CPU.Build.0 = CodeAnalysis|Any CPU + {A6F9775D-F7E2-424E-8363-79644A73038F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6F9775D-F7E2-424E-8363-79644A73038F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6F9775D-F7E2-424E-8363-79644A73038F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6F9775D-F7E2-424E-8363-79644A73038F}.Release|Any CPU.Build.0 = Release|Any CPU + {E8072054-3D92-4321-86B1-E038616AAA3E}.CodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU + {E8072054-3D92-4321-86B1-E038616AAA3E}.CodeAnalysis|Any CPU.Build.0 = Debug|Any CPU + {E8072054-3D92-4321-86B1-E038616AAA3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E8072054-3D92-4321-86B1-E038616AAA3E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E8072054-3D92-4321-86B1-E038616AAA3E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E8072054-3D92-4321-86B1-E038616AAA3E}.Release|Any CPU.Build.0 = Release|Any CPU + {BED3CAAD-C9A8-4F29-B35A-3D73A9DECE87}.CodeAnalysis|Any CPU.ActiveCfg = Debug|Any CPU + {BED3CAAD-C9A8-4F29-B35A-3D73A9DECE87}.CodeAnalysis|Any CPU.Build.0 = Debug|Any CPU + {BED3CAAD-C9A8-4F29-B35A-3D73A9DECE87}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BED3CAAD-C9A8-4F29-B35A-3D73A9DECE87}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BED3CAAD-C9A8-4F29-B35A-3D73A9DECE87}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BED3CAAD-C9A8-4F29-B35A-3D73A9DECE87}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -54,6 +114,13 @@ Global {1978960D-EFF1-4AB1-902B-0DE7BEADA5A1} = {957D9415-CE59-47B5-ABB8-A5EA12863F4E} {E6271DA9-A12E-4484-AC50-43F55C1F2DF5} = {621819DE-DDB7-41FC-A874-757464A90050} {7DECA341-442D-41D5-89B4-B3BCD3676758} = {957D9415-CE59-47B5-ABB8-A5EA12863F4E} + {B379640E-9064-438D-8DA5-6F7B394C2C46} = {621819DE-DDB7-41FC-A874-757464A90050} + {0193DBDA-F7D5-4A71-8097-13D74DF3DE58} = {621819DE-DDB7-41FC-A874-757464A90050} + {052E17C4-C151-4964-A873-979682F8619B} = {0193DBDA-F7D5-4A71-8097-13D74DF3DE58} + {573D523D-AF49-460B-98C7-AB9FB930AC60} = {0193DBDA-F7D5-4A71-8097-13D74DF3DE58} + {A6F9775D-F7E2-424E-8363-79644A73038F} = {0193DBDA-F7D5-4A71-8097-13D74DF3DE58} + {E8072054-3D92-4321-86B1-E038616AAA3E} = {621819DE-DDB7-41FC-A874-757464A90050} + {BED3CAAD-C9A8-4F29-B35A-3D73A9DECE87} = {621819DE-DDB7-41FC-A874-757464A90050} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {736D9CFE-B80E-445C-8328-D46C08F1FC8D} diff --git a/ODataService/DataSourceManager/DataSourceManager.csproj b/ODataService/DataSourceManager/DataSourceManager.csproj new file mode 100644 index 0000000000..8f1668c92f --- /dev/null +++ b/ODataService/DataSourceManager/DataSourceManager.csproj @@ -0,0 +1,19 @@ + + + + net7.0 + false + + Library + + + + + + + + + + + + diff --git a/ODataService/DataSourceManager/DataStoreManager/DefaultDataStoreManager.cs b/ODataService/DataSourceManager/DataStoreManager/DefaultDataStoreManager.cs new file mode 100644 index 0000000000..959c346f7d --- /dev/null +++ b/ODataService/DataSourceManager/DataStoreManager/DefaultDataStoreManager.cs @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Timers; + +namespace DataSourceManager.DataStoreManager +{ + /// + /// Default resource management class to manage resources. + /// Use a dictionary to easily access the resource by and make a constraint on the total number of resources. + /// Use a timer for each resource, when the resource live longer than , it will be destroyed automatically. + /// + public class DefaultDataStoreManager :IDataStoreManager where TDataStoreType : class, new() + { + /// + /// The max capacity of the resource container, this is a constraint for memory cost. + /// + public int MaxDataStoreInstanceCapacity { get; set; } + + /// + /// The max life time of each resource. When the resource lives longer than that, it will be destroyed automatically. + /// Besides, when the resource container is full, the resource live longest will be destroyed. + /// + public TimeSpan MaxDataStoreInstanceLifeTime { get; set; } + + private Dictionary _dataStoreDict = new Dictionary(); + + public DefaultDataStoreManager() + { + MaxDataStoreInstanceCapacity = 1000; + MaxDataStoreInstanceLifeTime = new TimeSpan(0, 15, 0); + } + + public TDataStoreType ResetDataStoreInstance(TKey key) + { + if (_dataStoreDict.ContainsKey(key)) + { + _dataStoreDict[key] = new DataStoreInstanceWrapper(key, MaxDataStoreInstanceLifeTime.TotalMilliseconds, ResouceTimeoutHandler); + } + else + { + AddDataStoreInstance(key); + } + + return _dataStoreDict[key].DataStore; + } + + public TDataStoreType GetDataStoreInstance(TKey key) + { + if (_dataStoreDict.ContainsKey(key)) + { + _dataStoreDict[key].UpdateLastUsedDateTime(); + } + else + { + AddDataStoreInstance(key); + } + + return _dataStoreDict[key].DataStore; + } + + private TDataStoreType AddDataStoreInstance(TKey key) + { + if (_dataStoreDict.Count >= MaxDataStoreInstanceCapacity) + { + // No resource lives longer than maxLifeTime, find the one lives longest and remove it. + var minLastUsedTime = DateTime.Now; + TKey minKey = default(TKey); + + foreach (var val in _dataStoreDict) + { + var resourceLastUsedTime = val.Value.DataStoreLastUsedDateTime; + if (resourceLastUsedTime < minLastUsedTime) + { + minLastUsedTime = resourceLastUsedTime; + minKey = val.Key; + } + } + + DeleteDataStoreInstance(minKey); + } + + System.Diagnostics.Trace.TraceInformation("The resouce dictionary size right now is {0}", _dataStoreDict.Count); + _dataStoreDict.Add(key, new DataStoreInstanceWrapper(key, MaxDataStoreInstanceLifeTime.TotalMilliseconds, ResouceTimeoutHandler)); + return _dataStoreDict[key].DataStore; + } + + private DefaultDataStoreManager DeleteDataStoreInstance(TKey key) + { + if (_dataStoreDict.ContainsKey(key)) + { + _dataStoreDict[key].StopTimer(); + _dataStoreDict.Remove(key); + } + + return this; + } + + private void ResouceTimeoutHandler(object source, EventArgs e) + { + var resouceUnit = source as DataStoreInstanceWrapper; + if (resouceUnit != null) + { + System.Diagnostics.Trace.TraceInformation(resouceUnit.DatastoreKey + " timeout occured, now destroy it!"); + DeleteDataStoreInstance(resouceUnit.DatastoreKey); + } + } + + private class DataStoreInstanceWrapper + { + public TKey DatastoreKey { get; private set; } + + public TDataStoreType DataStore { get; private set; } + + public DateTime DataStoreLastUsedDateTime { get; private set; } + + private Timer DataStoreTimer { get; set; } + + private double _dataStoreLifeTime; + + private EventHandler _timerTimeoutHandler; + + public DataStoreInstanceWrapper(TKey key, double dataStoreLifeTime, EventHandler dataStoreTimeoutHandler) + { + DatastoreKey = key; + DataStore = new TDataStoreType(); + DataStoreLastUsedDateTime = DateTime.Now; + _dataStoreLifeTime = dataStoreLifeTime; + _timerTimeoutHandler += dataStoreTimeoutHandler; + InitTimer(); + } + + public DataStoreInstanceWrapper UpdateLastUsedDateTime() + { + UpdateTimer(); + DataStoreLastUsedDateTime = DateTime.Now; + return this; + } + + public void StopTimer() + { + DataStoreTimer.Stop(); + } + + private Timer InitTimer() + { + DataStoreTimer = new Timer(_dataStoreLifeTime); + DataStoreTimer.Elapsed += (sender, args) => + { + if (_timerTimeoutHandler != null) + { + _timerTimeoutHandler.Invoke(this, args); + } + }; + DataStoreTimer.Start(); + return DataStoreTimer; + } + + private void UpdateTimer() + { + DataStoreTimer.Stop(); + DataStoreTimer = InitTimer(); + } + }; + } +} \ No newline at end of file diff --git a/ODataService/DataSourceManager/DataStoreManager/IDataStoreManager.cs b/ODataService/DataSourceManager/DataStoreManager/IDataStoreManager.cs new file mode 100644 index 0000000000..6923e1011f --- /dev/null +++ b/ODataService/DataSourceManager/DataStoreManager/IDataStoreManager.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace DataSourceManager.DataStoreManager +{ + /// + /// This is a data store management interface to manage the datastore with its key. + /// + /// Type of the key to identify the data store. + /// Type of the real data store. + public interface IDataStoreManager + { + /// + /// Get the data store instance by its key. + /// + /// The key to identify the data store. + /// Return the data store with key. + TDataStoreType GetDataStoreInstance(TKey key); + + /// + /// Find the resource by key and reset it to origin. + /// + /// The key to identify the data store. + /// Return the data store after reseting. + TDataStoreType ResetDataStoreInstance(TKey key); + } +} \ No newline at end of file diff --git a/ODataService/DataSourceManager/DataStoreManager/SingleDataStoreManager.cs b/ODataService/DataSourceManager/DataStoreManager/SingleDataStoreManager.cs new file mode 100644 index 0000000000..90e7cff99f --- /dev/null +++ b/ODataService/DataSourceManager/DataStoreManager/SingleDataStoreManager.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace DataSourceManager.DataStoreManager +{ + /// + /// Data store manager for managing a single (i.e., read-only) datastore + /// + public class SingleDataStoreManager : IDataStoreManager where TDataStoreType : class, new() + { + private TDataStoreType dataStore; + + public SingleDataStoreManager() + { + dataStore = new TDataStoreType(); + } + + public TDataStoreType ResetDataStoreInstance(TKey key) + { + return dataStore = new TDataStoreType(); + } + + public TDataStoreType GetDataStoreInstance(TKey key) + { + return dataStore; + } + } +} \ No newline at end of file diff --git a/ODataService/DataSourceManager/Properties/AssemblyInfo.cs b/ODataService/DataSourceManager/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..829ed9c6a9 --- /dev/null +++ b/ODataService/DataSourceManager/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Microsoft.OData.Service.Library")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Microsoft.OData.Service.Library")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("1ca9b17f-d3f8-4fc3-a992-5135dccab9de")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/ODataService/DataSourceManager/Properties/Resources.Designer.cs b/ODataService/DataSourceManager/Properties/Resources.Designer.cs new file mode 100644 index 0000000000..7c58e9f215 --- /dev/null +++ b/ODataService/DataSourceManager/Properties/Resources.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.Restier.Providers.InMemory.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Restier.Providers.InMemory.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Unable to find DataStoreManager, please add singleton of type {0} to apiBase services collection firstly. . + /// + internal static string DataStoreManagerNotFound { + get { + return ResourceManager.GetString("DataStoreManagerNotFound", resourceCulture); + } + } + } +} diff --git a/ODataService/DataSourceManager/Properties/Resources.resx b/ODataService/DataSourceManager/Properties/Resources.resx new file mode 100644 index 0000000000..02e299135e --- /dev/null +++ b/ODataService/DataSourceManager/Properties/Resources.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Unable to find DataStoreManager, please add singleton of type {0} to apiBase services collection firstly. + + \ No newline at end of file diff --git a/ODataService/DataSourceManager/Properties/launchSettings.json b/ODataService/DataSourceManager/Properties/launchSettings.json new file mode 100644 index 0000000000..20bd6027ab --- /dev/null +++ b/ODataService/DataSourceManager/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:64492/", + "sslPort": 44358 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Microsoft.Restier.Providers.InMemory": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + } + } +} \ No newline at end of file diff --git a/ODataService/DataSourceManager/Submit/ChangeSetInitializer.cs b/ODataService/DataSourceManager/Submit/ChangeSetInitializer.cs new file mode 100644 index 0000000000..e0f5121b91 --- /dev/null +++ b/ODataService/DataSourceManager/Submit/ChangeSetInitializer.cs @@ -0,0 +1,304 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.OData; +using Microsoft.AspNetCore.Http; +using Microsoft.Restier.Core; +using Microsoft.Restier.Core.Submit; +using DataSourceManager.DataStoreManager; +using DataSourceManager.Utils; + +namespace DataSourceManager.Submit +{ + /// + /// ChangeSetInitializer class. + /// Since our datasource is in memory, + /// we just confirm the data change here, not in SubmitExecutor + /// + public class ChangeSetInitializer : IChangeSetInitializer + { + public Task InitializeAsync(SubmitContext context, CancellationToken cancellationToken) + { + var requestScope = context.Api.ServiceProvider.GetService(typeof(HttpRequestScope)) as HttpRequestScope; + var key = InMemoryProviderUtils.GetSessionId(requestScope?.HttpRequest.HttpContext); + var dataStoreManager = context.GetApiService>(); + if (dataStoreManager == null) + { + throw new ArgumentNullException("DataStoreManager Not Found", + typeof(IDataStoreManager).ToString()); + } + + var dataSource = dataStoreManager.GetDataStoreInstance(key); + foreach (var dataModificationItem in context.ChangeSet.Entries.OfType()) + { + var resourceType = dataModificationItem.ExpectedResourceType; + if (dataModificationItem.ActualResourceType != null && + dataModificationItem.ActualResourceType != dataModificationItem.ExpectedResourceType) + { + resourceType = dataModificationItem.ActualResourceType; + } + + var operation = dataModificationItem.EntitySetOperation; + object resource = null; + switch (operation) + { + case RestierEntitySetOperation.Insert: + // Here we create a instance of entity, parameters are from the request. + // Known issues: not support odata.id + resource = Activator.CreateInstance(resourceType); + SetValues(resource, resourceType, dataModificationItem.LocalValues); + dataModificationItem.Resource = resource; + + // insert new entity into entity set + var entitySetPropForInsert = GetEntitySetPropertyInfoFromDataModificationItem(dataSource, + dataModificationItem); + + if (entitySetPropForInsert != null && entitySetPropForInsert.CanWrite) + { + var originSet = entitySetPropForInsert.GetValue(dataSource); + entitySetPropForInsert.PropertyType.GetMethod("Add").Invoke(originSet, new[] {resource}); + } + break; + case RestierEntitySetOperation.Update: + resource = FindResource(dataSource, context, dataModificationItem, cancellationToken); + dataModificationItem.Resource = resource; + + // update the entity + if (resource != null) + { + SetValues(resource, resourceType, dataModificationItem.LocalValues); + } + break; + case RestierEntitySetOperation.Delete: + resource = FindResource(dataSource, context, dataModificationItem, cancellationToken); + dataModificationItem.Resource = resource; + + // remove the entity + if (resource != null) + { + var entitySetPropForRemove = GetEntitySetPropertyInfoFromDataModificationItem(dataSource, + dataModificationItem); + + if (entitySetPropForRemove != null && entitySetPropForRemove.CanWrite) + { + var originSet = entitySetPropForRemove.GetValue(dataSource); + entitySetPropForRemove.PropertyType.GetMethod("Remove").Invoke(originSet, new[] {resource}); + } + } + break; + default: + throw new NotImplementedException(); + } + } + + return Task.WhenAll(); + } + + /// + /// Convert EdmStructuredObject to an object with type. + /// + /// An object with EdmStructuredType. + /// Desired object type. + /// Result object. + private static object ConvertEdmStructuredObjectToTypedObject( + IEdmStructuredObject edmStructuredObject, Type type) + { + if (edmStructuredObject == null) + { + return null; + } + + var obj = Activator.CreateInstance(type); + var propertyInfos = type.GetProperties(); + foreach (var propertyInfo in propertyInfos) + { + if (!propertyInfo.CanWrite) + { + continue; + } + + object value = null; + edmStructuredObject.TryGetPropertyValue(propertyInfo.Name, out value); + if (value == null) + { + propertyInfo.SetValue(obj, value); + continue; + } + + if (!propertyInfo.PropertyType.IsInstanceOfType(value)) + { + var edmObj = value as IEdmStructuredObject; + if (edmObj == null) + { + throw new NotSupportedException(string.Format( + CultureInfo.InvariantCulture, + propertyInfo.PropertyType.ToString())); + } + + value = ConvertEdmStructuredObjectToTypedObject(edmObj, propertyInfo.PropertyType); + } + + propertyInfo.SetValue(obj, value); + } + + return obj; + } + + /// + /// Convert EdmStructuredObjectCollection object to an object with type. + /// + /// EdmStructuredObjectCollection type. + /// An object with EdmStructuredObjectCollection. + /// Desired object type. + /// Result object. + private static object ConvertEdmStructuredObjectCollectionToTypedObject( + TEdmStructuredObjectCollection edmStructuredObjectCollection, Type type) + { + var valueType = typeof(Collection<>).MakeGenericType(type); + var value = Activator.CreateInstance(valueType); + var collection = edmStructuredObjectCollection as System.Collections.IEnumerable; + if (collection == null) + { + return null; + } + + foreach (var c in collection) + { + var obj = ConvertEdmStructuredObjectToTypedObject(c as IEdmStructuredObject, type); + value.GetType().GetMethod("Add").Invoke(value, new[] {obj}); + } + + return value; + } + + private static void SetValues(object instance, Type type, IReadOnlyDictionary values) + { + foreach (var propertyPair in values) + { + var value = propertyPair.Value; + var propertyInfo = type.GetProperty(propertyPair.Key); + if (propertyInfo == null) + { + continue; + } + + if (value == null) + { + // If the property value is null, we set null in the object too. + propertyInfo.SetValue(instance, null); + continue; + } + + if (!propertyInfo.PropertyType.IsInstanceOfType(value)) + { + var dic = value as IReadOnlyDictionary; + + if (dic != null) + { + value = propertyInfo.GetValue(instance); + SetValues(value, propertyInfo.PropertyType, dic); + } + else if (propertyInfo.PropertyType.IsGenericType) + { + // EdmStructuredObjectCollection + var realType = propertyInfo.PropertyType.GenericTypeArguments[0]; + value = ConvertEdmStructuredObjectCollectionToTypedObject(value, realType); + } + else + { + throw new NotSupportedException(string.Format( + CultureInfo.InvariantCulture, + propertyPair.Key)); + } + } + + propertyInfo.SetValue(instance, value); + } + } + + private static object FindResource( + object instance, + SubmitContext context, + DataModificationItem item, + CancellationToken cancellationToken) + { + var entitySetPropertyInfo = GetEntitySetPropertyInfoFromDataModificationItem(instance, item); + var originSet = entitySetPropertyInfo.GetValue(instance); + object resource = null; + Type resourceType = null; + var enumerableSet = originSet as IEnumerable; + if (enumerableSet != null) + { + resourceType = originSet.GetType().GetGenericArguments()[0]; + foreach (var o in enumerableSet) + { + var foundFlag = true; + foreach (var keyVal in item.ResourceKey) + { + var entityProp = o.GetType().GetProperty(keyVal.Key); + if (entityProp != null) + { + foundFlag &= entityProp.GetValue(o).Equals(keyVal.Value); + } + else + { + foundFlag = false; + } + + if (!foundFlag) + { + break; + } + } + + if (foundFlag) + { + resource = Convert.ChangeType(o, resourceType); + break; + } + } + } + + if (resource == null) + { + throw new Exception("Resource Not Found"); + } + + // This means no If-Match or If-None-Match header + if (item.OriginalValues == null || item.OriginalValues.Count == 0) + { + return resource; + } + + // Make a list of resource as IQueryable to valid etag + var listOfItemType = typeof(List<>).MakeGenericType(resourceType); + var list = Activator.CreateInstance(listOfItemType); + listOfItemType.GetMethod("Add").Invoke(list, new[] {resource}); + var method = typeof(Queryable).GetMethod( + "AsQueryable", + BindingFlags.Static | BindingFlags.Public, + null, + new[] {listOfItemType}, + null); + resource = item.ValidateEtag(method.Invoke(list, new[] {list}) as IQueryable); + return resource; + } + + private static PropertyInfo GetEntitySetPropertyInfoFromDataModificationItem(object instance, + DataModificationItem dataModificationItem) + { + var entitySetName = dataModificationItem.ResourceSetName; + var entitySetProp = instance.GetType() + .GetProperty(entitySetName, BindingFlags.Public | BindingFlags.Instance); + return entitySetProp; + } + } +} diff --git a/ODataService/DataSourceManager/Submit/SubmitExecutor.cs b/ODataService/DataSourceManager/Submit/SubmitExecutor.cs new file mode 100644 index 0000000000..8a231c6fd3 --- /dev/null +++ b/ODataService/DataSourceManager/Submit/SubmitExecutor.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Restier.Core.Submit; + +namespace DataSourceManager.Submit +{ + public class SubmitExecutor : ISubmitExecutor + { + public Task ExecuteSubmitAsync(SubmitContext context, CancellationToken cancellationToken) + { + return Task.FromResult(new SubmitResult(context.ChangeSet)); + } + } +} \ No newline at end of file diff --git a/ODataService/DataSourceManager/Utils/InMemoryProviderUtils.cs b/ODataService/DataSourceManager/Utils/InMemoryProviderUtils.cs new file mode 100644 index 0000000000..8afeae31bf --- /dev/null +++ b/ODataService/DataSourceManager/Utils/InMemoryProviderUtils.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; + +namespace DataSourceManager.Utils +{ + public static class InMemoryProviderUtils + { + const string sessionKey = "dataKey"; + static public string GetSessionId(HttpContext context) + { + var session = context.Session; + string id; + if (session != null) + { + id = session.GetString(sessionKey); + if (id == null) + { + id = System.Guid.NewGuid().ToString().Replace("-",""); + session.SetString(sessionKey, id); + } + + return id; + } + + return null; + } + } +} diff --git a/ODataService/DataSourceManager/Utils/ODataSessionIdManager.cs b/ODataService/DataSourceManager/Utils/ODataSessionIdManager.cs new file mode 100644 index 0000000000..d3a76f6fac --- /dev/null +++ b/ODataService/DataSourceManager/Utils/ODataSessionIdManager.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Http; +using System.Text.RegularExpressions; + +namespace DataSourceManager.Utils +{ + ///// + ///// The default SessionIdManager in Azure will cause to loop 302, use custom SessionIdManager to avoid this. + ///// + //public class ODataSessionIdManager : ISessionIDManager + //{ + // private static InternalSessionIdManager _internalManager = new InternalSessionIdManager(); + + // public string CreateSessionID(HttpContext context) + // { + // return _internalManager.CreateSessionID(context); + // } + + // public string GetSessionID(HttpContext context) + // { + // var id = context.Items["AspCookielessSession"] as string; + + // // Azure web site does not support header "AspFilterSessionId", so we cannot get context.Items["AspCookielessSession"] + // // for azure web site use, Headers["X-Original-URL"] format: /(S(xxx))/odata/path. + // var originalUrl = context.Request.Headers["X-Original-URL"]; + + // if (!string.IsNullOrEmpty(originalUrl)) + // { + // var match = Regex.Match(context.Request.Headers["X-Original-URL"], @"/\(S\((\w+)\)\)"); + // if (match.Success) + // { + // id = match.Groups[1].Value; + // } + // } + + // return id; + // } + + // public void Initialize() + // { + // _internalManager.Initialize(); + // } + + // public bool InitializeRequest(HttpContext context, bool suppressAutoDetectRedirect, out bool supportSessionIdReissue) + // { + // return _internalManager.InitializeRequest(context, suppressAutoDetectRedirect, out supportSessionIdReissue); + // } + + // public void RemoveSessionID(HttpContext context) + // { + // _internalManager.RemoveSessionID(context); + // } + + // public void SaveSessionID(HttpContext context, string id, out bool redirected, out bool cookieAdded) + // { + // _internalManager.SaveSessionID(context, id, out redirected, out cookieAdded); + // } + + // public bool Validate(string id) + // { + // return _internalManager.Validate(id); + // } + + // private class InternalSessionIdManager : SessionIDManager + // { + // public override bool Validate(string id) + // { + // return !string.IsNullOrEmpty(id); + // } + // } + //} +} diff --git a/ODataService/DynamicService/Api/DynamicApi.cs b/ODataService/DynamicService/Api/DynamicApi.cs new file mode 100644 index 0000000000..b260fa0273 --- /dev/null +++ b/ODataService/DynamicService/Api/DynamicApi.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.OData.Query; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OData.Edm; +using Microsoft.Restier.Core; +using Microsoft.Restier.Core.Model; +using Microsoft.Restier.Core.Submit; +//using Microsoft.Restier.EntityFramework; +using Microsoft.OData.Edm.Csdl; +using System.Xml; +using Microsoft.OData.Edm.Validation; + + +namespace DynamicService +{ + public class DynamicApi : ApiBase + { + public DynamicApi(IServiceProvider serviceProvider) : base(serviceProvider) { } + } + + public class DynamicModelBuilder : IModelBuilder + { + public IModelBuilder InnerHandler { get; set; } + + public string DataSourceName { get; set; } + public IEdmModel GetModel(ModelContext context) + { + IEdmModel model; + IEnumerable errors; + // todo: improve this logic + DataSourceName = "NWind"; + var path = AppDomain.CurrentDomain.GetData("ContentRootPath") as String; + var file = Path.Combine(path, DataSourceName + ".csdl"); + + XmlReader xmlReader = XmlReader.Create(file); + if (CsdlReader.TryParse(xmlReader, out model, out errors)) + { + return model; + } + + throw new Exception("Couldn't parse xml"); + } + + public async Task GetModelAsync(ModelContext context, CancellationToken cancellationToken) + { + return await Task.FromResult(GetModel(context)); + } + + } +} \ No newline at end of file diff --git a/ODataService/DynamicService/App_Data/NWind.csdl b/ODataService/DynamicService/App_Data/NWind.csdl new file mode 100644 index 0000000000..9723190039 --- /dev/null +++ b/ODataService/DynamicService/App_Data/NWind.csdl @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ODataService/DynamicService/App_Data/Trippin.csdl b/ODataService/DynamicService/App_Data/Trippin.csdl new file mode 100644 index 0000000000..f8a86e0ec3 --- /dev/null +++ b/ODataService/DynamicService/App_Data/Trippin.csdl @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + TimeStampValue + + + + + + + + + + + + + + + + + + + + + + + + LastUpdated + + + + + + + \ No newline at end of file diff --git a/ODataService/DynamicService/App_Start/Startup.cs b/ODataService/DynamicService/App_Start/Startup.cs new file mode 100644 index 0000000000..207d69f359 --- /dev/null +++ b/ODataService/DynamicService/App_Start/Startup.cs @@ -0,0 +1,204 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNet.OData.Extensions; +using Microsoft.AspNet.OData.Query; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Restier.Core; +using Microsoft.OData; +using Microsoft.OData.UriParser; +using Microsoft.Extensions.Configuration; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Restier.AspNetCore; +using Microsoft.Extensions.Hosting; +using Microsoft.Restier.Core.Submit; +using Microsoft.Restier.Core.Model; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Microsoft.OData.Edm; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNet.OData; + +namespace DynamicService +{ + public class Startup + { + private const string routeName = "DynamicApi"; + private const string corsPolicy = "_allowAllCORS"; + internal const string serviceName = "ourService"; + + /// + /// The application configuration + /// + public IConfiguration Configuration { get; } + + /// + /// Initializes a new instance of the class. + /// + /// + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + /// + /// Configures the container. + /// + /// + public void ConfigureServices(IServiceCollection services) + { + //services.AddScoped(sp => + // ((DynamicModelBuilder)sp.GetRequiredService()).GetModel(null) + //); + services.AddRestier((builder) => + { + // This delegate is executed after OData is added to the container. + // Add replacement services here. + builder.AddRestierApi(routeServices => + { + routeServices + .AddSingleton(new ODataValidationSettings + { + MaxAnyAllExpressionDepth = 10, + MaxExpansionDepth = 10, + }) + .AddScoped((sp) => + new ODataMessageWriterSettings + { + Version = ODataVersion.V401, + BaseUri = new Uri("http://" + serviceName, UriKind.Absolute), + }) + + // omit @odata prefixes + .AddSingleton((sp) => + { + ODataSimplifiedOptions simplifiedOptions = new ODataSimplifiedOptions(); + simplifiedOptions.SetOmitODataPrefix(true); + return simplifiedOptions; + }) + .AddScoped((serviceProvider) => + new UnqualifiedODataUriResolver + { + EnableNoDollarQueryOptions = true, + EnableCaseInsensitive = true, + }) + .AddSingleton>() + .AddSingleton() + .AddScoped(sp => + ((DynamicModelBuilder)sp.GetRequiredService()).GetModel(null) + ) + .AddChainedService() + // .AddSingleton>(new DataSourceManager.DataStoreManager.SingleDataStoreManager()) + ; + }); + }); + + services.AddControllers(options => options.EnableEndpointRouting = false); + + services.AddDistributedMemoryCache(); + + services.AddSession(options => + { + options.IdleTimeout = TimeSpan.FromMinutes(30); + options.Cookie.HttpOnly = true; + options.Cookie.IsEssential = false; + }); + + services.AddCors(options => + { + options.AddPolicy(name: corsPolicy, + builder => + { + builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader(); + }); + }); + } + + /// + /// Configures the application and the HTTP Request pipeline. + /// + /// + /// + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseCors(corsPolicy); + + app.UseSession(); + +// app.UseDynamicServiceMiddleware(); + + app.UseMvc(builder => + { + builder.Select().Expand().Filter().OrderBy().MaxTop(100).Count().SetTimeZoneInfo(TimeZoneInfo.Utc); + + builder.MapRestier(builder => + { + builder.MapApiRoute(routeName, "", true); + }); + }); + + //todo: remove when we have a real storage location for csdl + AppDomain.CurrentDomain.SetData("ContentRootPath", env.ContentRootPath + @"\App_Data"); + } + } + + /// + /// DynamicService request handling + /// + public class DynamicServiceMiddleware + { + private RequestDelegate _next; + public DynamicServiceMiddleware(RequestDelegate next) + { + this._next = next; + } + + public async Task Invoke(HttpContext context) + { + // set the host to be Jetsons + context.Request.Host = new HostString(Startup.serviceName); + + // set the default format to be application/json + string accepts = "application/json"; + StringValues acceptValues; + if (context.Request.Headers.TryGetValue("Accept", out acceptValues) && acceptValues.Count > 0) + { + accepts = acceptValues[0].Replace("*/*", "application/json").Replace("application/*", "application/json"); + foreach (string accept in accepts.Split(',')) + { + if (accept.StartsWith("application/json") | accept.StartsWith("application/xml")) + { + break; + } + } + } + + context.Request.Headers["Accept"] = accepts; + + // attempts at geting the service container to inject IEdmModel service +// IPerRouteContainer perRouteContainer = context.RequestServices.GetRequiredService(); +// var sp = perRouteContainer.GetODataRootContainer(routeName); + + await _next.Invoke(context); + } + } + + /// + /// Extension method for registering DynamicServiceMiddleware + /// + public static class DynamicServiceMiddlewareExtensions + { + public static IApplicationBuilder UseDynamicServiceMiddleware(this IApplicationBuilder builder) + { + return builder.UseMiddleware(); + } + } +} diff --git a/ODataService/DynamicService/Controllers/DynamicApiController.cs b/ODataService/DynamicService/Controllers/DynamicApiController.cs new file mode 100644 index 0000000000..d0008dd8b6 --- /dev/null +++ b/ODataService/DynamicService/Controllers/DynamicApiController.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Net.Http; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNet.OData.Extensions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Restier.AspNetCore; +using Microsoft.Restier.Core; + +namespace DynamicService +{ + public class DynamicApiController : RestierController + { + public async Task Get(CancellationToken cancellationToken) + { + IServiceProvider serviceProvider = Request.GetRequestContainer(); + //ApiFactory factory = Request.GetRequestContainer().GetRequiredService(); + var apiProperty = typeof(ApiBase).GetProperty("base", BindingFlags.NonPublic | BindingFlags.Instance); + apiProperty.SetValue(this, typeof(NWind.NWindDataSource)); + + return await base.Get(cancellationToken); + } + } +} \ No newline at end of file diff --git a/ODataService/DynamicService/DynamicHelper.cs b/ODataService/DynamicService/DynamicHelper.cs new file mode 100644 index 0000000000..dcdd938ffd --- /dev/null +++ b/ODataService/DynamicService/DynamicHelper.cs @@ -0,0 +1,250 @@ +using DataSourceGenerator; +using Microsoft.AspNet.OData.Batch; +using Microsoft.AspNet.OData.Extensions; +using Microsoft.AspNet.OData.Routing; +using Microsoft.AspNet.OData.Routing.Conventions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Restier.AspNetCore; +using Microsoft.Restier.AspNetCore.Batch; +using Microsoft.Restier.Core; +using Microsoft.Restier.Core.Model; +using Microsoft.Restier.Core.Query; +using Microsoft.Restier.Core.Submit; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Threading.Tasks; + +namespace DynamicService +{ +#if false + public static class DynamicHelper + { + //private const string MapRestierRouteMethod = "MapRestierRoute"; + //private const string httpConfigurationExtensionsType = "System.Web.Http.HttpConfigurationExtensions"; + private const string DynamicApiType = "Microsoft.OData.Service.ApiAsAService.Api.DynamicApi`1"; + private const string DynamicHelperType = "Microsoft.OData.Service.ApiAsAService.DynamicHelper"; + //private static Type dynamicApiType = Assembly.GetExecutingAssembly().GetType(DynamicApiType); + //private static Type dynamicHelperType = Assembly.GetExecutingAssembly().GetType(DynamicHelperType); + + //public static ApiBase CreateEntifyFrameworkApi(Type tApi, IServiceProvider serviceProvider) + //{ + // return (ApiBase)typeof(EntityFrameworkApi<>).MakeGenericType(tApi).GetConstructor(new Type[] { typeof(IServiceProvider) }).Invoke(new object[] { serviceProvider }); + //} + + /// + /// Maps the API routes to the RestierController. + /// + /// The user API. + /// The instance. + /// The name of the route. + /// The prefix of the route. + /// The handler for batch requests. + /// The task object containing the resulted instance. + public static Task MapRestierRoute( + HttpConfiguration config, + string routeName, + string routePrefix, + RestierBatchHandler batchHandler = null) + where TApi : ApiBase + { + // This will be added a service to callback stored in ApiConfiguration + // Callback is called by ApiBase.AddApiServices method to add real services. + ApiBase.AddPublisherServices(typeof(TApi), services => + { + services.AddScoped(sp => new ApiFactory(sp)); + MapEfServices(services); + services.AddODataServices(); + }); + + IContainerBuilder func() => new RestierContainerBuilder(typeof(TApi)); + config.UseCustomContainerBuilder(func); + + var conventions = CreateRestierRoutingConventions(config, routeName); + if (batchHandler != null) + { + batchHandler.ODataRouteName = routeName; + } + + void configureAction(IContainerBuilder builder) => builder + .AddService>(ServiceLifetime.Singleton, sp => conventions) + .AddService(ServiceLifetime.Singleton, sp => batchHandler); + + var route = GenerateODataServiceRoute(config, routeName, routePrefix, configureAction); + return Task.FromResult(route); + } + + /// + /// Creates the default routing conventions. + /// + /// The instance. + /// The name of the route. + /// The routing conventions created. + internal static IList CreateRestierRoutingConventions( + HttpConfiguration config, string routeName) + { + var conventions = ODataRoutingConventions.CreateDefaultWithAttributeRouting(routeName, config); + var index = 0; + for (; index < conventions.Count; index++) + { + if (conventions[index] is AttributeRoutingConvention attributeRouting) + { + break; + } + } + + conventions.Insert(index + 1, RestierRoutingConventionFactory.Create()); + return conventions; + } + + /// + /// Maps the specified OData route and the OData route attributes. + /// + /// The server configuration. + /// The name of the route to map. + /// The prefix to add to the OData route's path template. + /// The configuring action to add the services to the root container. + /// The added . + public static ODataRoute GenerateODataServiceRoute(HttpConfiguration configuration, string routeName, + string routePrefix, Action configureAction) + { + if (configuration == null) + { + throw new ArgumentNullException("configuration"); + } + + if (routeName == null) + { + throw new ArgumentNullException("routeName"); + } + + // 1) Build and configure the root container. + IServiceProvider rootContainer = configuration.CreateODataRootContainer(routeName, configureAction); + + // 2) Resolve the path handler and set URI resolver to it. + IODataPathHandler pathHandler = rootContainer.GetRequiredService(); + + // if settings is not on local, use the global configuration settings. + if (pathHandler != null && pathHandler.UrlKeyDelimiter == null) + { + ODataUrlKeyDelimiter urlKeyDelimiter = configuration.GetUrlKeyDelimiter(); + pathHandler.UrlKeyDelimiter = urlKeyDelimiter; + } + + // 3) Resolve some required services and create the route constraint. + ODataPathRouteConstraint routeConstraint = new ODataPathRouteConstraint(routeName); + + // Attribute routing must initialized before configuration.EnsureInitialized is called. + rootContainer.GetServices(); + + // 4) Resolve HTTP handler, create the OData route and register it. + ODataRoute route; + HttpRouteCollection routes = configuration.Routes; + routePrefix = RemoveTrailingSlash(routePrefix); + HttpMessageHandler messageHandler = rootContainer.GetService(); + if (messageHandler != null) + { + route = new ODataRoute( + routePrefix, + routeConstraint, + defaults: null, + constraints: null, + dataTokens: null, + handler: messageHandler); + } + else + { + ODataBatchHandler batchHandler = rootContainer.GetService(); + if (batchHandler != null) + { + batchHandler.ODataRouteName = routeName; + string batchTemplate = String.IsNullOrEmpty(routePrefix) ? ODataRouteConstants.Batch + : routePrefix + '/' + ODataRouteConstants.Batch; + routes.MapHttpBatchRoute(routeName + "Batch", batchTemplate, batchHandler); + } + + route = new ODataRoute(routePrefix, routeConstraint); + } + + return route; + } + + private static string RemoveTrailingSlash(string routePrefix) + { + if (!String.IsNullOrEmpty(routePrefix)) + { + int prefixLastIndex = routePrefix.Length - 1; + if (routePrefix[prefixLastIndex] == '/') + { + // Remove the last trailing slash if it has one. + routePrefix = routePrefix.Substring(0, routePrefix.Length - 1); + } + } + return routePrefix; + } + + private static IServiceCollection MapEfServices(IServiceCollection services) + { + services.AddScoped(sp => + { + var modelType = sp.GetRequiredService().ModelType; + DbContext dbContext = Activator.CreateInstance(modelType) as DbContext; +#if EF7 + dbContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; +#else + dbContext.Configuration.ProxyCreationEnabled = false; +#endif + return dbContext; + }); + + return services + .AddService() + .AddService((sp, next) => new ModelMapper(sp.GetRequiredService().ModelType)) + .MakeScoped() + .AddService() + .AddService() + .AddService() + .AddService() + .AddService() + ; + } + + public static Type GetDynamicDbContext(string csdlFile) + { + string name = csdlFile.Split('\\').Last().Replace(".xml", ""); + DbContextGenerator generator = new DbContextGenerator(); + return generator.GenerateDbContext(csdlFile); + } + + public static Type GetDynamicDbContextInADifferentAppDomain(string csdlFile) + { + string name = csdlFile.Split('\\').Last().Replace(".xml", ""); + + // Create a new AppDomain and execute the generation in that AppDomain + AppDomain currentDomain = AppDomain.CurrentDomain; + var newDomain = AppDomain.CreateDomain(name, null, AppDomain.CurrentDomain.SetupInformation); + + // Pass this app domain to the new one + newDomain.SetData("domain", currentDomain); + var program = new DbContextGenerator(); + + // Set the name of the csdl file. Could also be done with newDomain.SetData() + program.csdlFileName = csdlFile; + + // Create the Assembly in the new AppDomain + newDomain.DoCallBack(new CrossAppDomainDelegate(program.GenerateDbContextInANewAppDomain)); + + // Various attempts passing the type back to this app domain; none work + // string assemblies = String.Concat(newDomain.GetAssemblies().Select(a=>a.GetName().Name)); + // Assembly assembly = newDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name == assemblyName.Name); + //Type result = program.DbContextType; + //return Result.DbContextType; + //return program.DbContextType; + object resultType = AppDomain.CurrentDomain.GetData("contextType"); + return resultType as Type; + } + } +#endif +} \ No newline at end of file diff --git a/ODataService/DynamicService/DynamicODataRoute.cs b/ODataService/DynamicService/DynamicODataRoute.cs new file mode 100644 index 0000000000..78df5b1c4f --- /dev/null +++ b/ODataService/DynamicService/DynamicODataRoute.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.OData.Routing; + +namespace DynamicService +{ +#if false + public class DynamicODataRoute : ODataRoute + { + private static readonly string _escapedHashMark = Uri.HexEscape('#'); + private static readonly string _escapedQuestionMark = Uri.HexEscape('?'); + + private bool _canGenerateDirectLink; + + public DynamicODataRoute(string routePrefix, ODataPathRouteConstraint pathConstraint) + : base(routePrefix, pathConstraint) + { + _canGenerateDirectLink = routePrefix != null && RoutePrefix.IndexOf('{') == -1; + } + + public override IHttpVirtualPathData GetVirtualPath( + HttpRequestMessage request, + IDictionary values) + { + if (values == null || !values.Keys.Contains(HttpRoute.HttpRouteKey, StringComparer.OrdinalIgnoreCase)) + { + return null; + } + + object odataPathValue; + if (!values.TryGetValue(ODataRouteConstants.ODataPath, out odataPathValue)) + { + return null; + } + + string odataPath = odataPathValue as string; + if (odataPath != null) + { + return GenerateLinkDirectly(request, odataPath) ?? base.GetVirtualPath(request, values); + } + + return null; + } + + internal HttpVirtualPathData GenerateLinkDirectly(HttpRequestMessage request, string odataPath) + { + HttpConfiguration configuration = request.GetConfiguration(); + if (configuration == null || !_canGenerateDirectLink) + { + return null; + } + + string dataSource = request.Properties[DynamicRouteConstraint.DataSourceNameProperty] as string; + string link = CombinePathSegments(RoutePrefix, dataSource); + link = CombinePathSegments(link, odataPath); + link = UriEncode(link); + + return new HttpVirtualPathData(this, link); + } + + private static string CombinePathSegments(string routePrefix, string odataPath) + { + return string.IsNullOrEmpty(routePrefix) + ? odataPath + : (string.IsNullOrEmpty(odataPath) ? routePrefix : routePrefix + '/' + odataPath); + } + + private static string UriEncode(string str) + { + string escape = Uri.EscapeUriString(str); + escape = escape.Replace("#", _escapedHashMark); + escape = escape.Replace("?", _escapedQuestionMark); + return escape; + } + } +#endif +} \ No newline at end of file diff --git a/ODataService/DynamicService/DynamicRouteConstraint.cs b/ODataService/DynamicService/DynamicRouteConstraint.cs new file mode 100644 index 0000000000..acea71158a --- /dev/null +++ b/ODataService/DynamicService/DynamicRouteConstraint.cs @@ -0,0 +1,220 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using Microsoft.AspNet.OData.Extensions; +using Microsoft.AspNet.OData.Routing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OData; +using Microsoft.Restier.AspNetCore; +using Microsoft.Restier.Core.Model; + +namespace DynamicService +{ + public class DynamicRouteConstraint : ODataPathRouteConstraint + { + // "%2F" + private static readonly string escapedSlash = Uri.HexEscape('/'); + internal const string DataSourceNameProperty = "DynamicService_DataSourceName"; + + public DynamicRouteConstraint(string routeName) + : base(routeName) + { + } + + public override bool Match(HttpContext httpContext, IRouter route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + if (routeDirection == RouteDirection.IncomingRequest) + { + string oDataPathString; + var request = httpContext.Request; + + if (values.TryGetValue(ODataRouteConstants.ODataPath, out object oDataPathValue) && !String.IsNullOrEmpty(oDataPathString = oDataPathValue as string)) + { + ODataPath path; + + try + { + string[] segments = oDataPathString.Split('/'); + string dataSourceName = segments[0]; + + IServiceProvider serviceProvider = request.CreateRequestContainer(this.RouteName); +// var appData = System.Web.HttpContext.Current.Server.MapPath("~/App_Data"); +// var file = System.IO.Path.Combine(appData, dataSourceName + ".xml"); + +// Type dynamicType = DynamicHelper.GetDynamicDbContext(file); + +// ApiFactory factory = serviceProvider.GetRequiredService(); +// factory.ModelType = dynamicType; + + DynamicModelBuilder modelBuilder = serviceProvider.GetRequiredService() as DynamicModelBuilder; + if (modelBuilder != null) + { + modelBuilder.DataSourceName = segments[0]; + } + + // Service root is the current RequestUri, less the query string and the ODataPath (always the + // last portion of the absolute path). ODL expects an escaped service root and other service + // root calculations are calculated using AbsoluteUri (also escaped). But routing exclusively + // uses unescaped strings, determined using + // address.GetComponents(UriComponents.Path, UriFormat.Unescaped) + // + // For example if the AbsoluteUri is + // , the + // oDataPathString will contain "FunctionCall(p0='Chinese西雅图Chars')". + // + // Due to this decoding and the possibility of unecessarily-escaped characters, there's no + // reliable way to determine the original string from which oDataPathString was derived. + // Therefore a straightforward string comparison won't always work. See RemoveODataPath() for + // details of chosen approach. + +// string requestLeftPart = request.RequestUri.GetLeftPart(UriPartial.Path); +// string serviceRoot = requestLeftPart; +// if (!string.IsNullOrEmpty(oDataPathString)) +// { +// serviceRoot = RemoveODataPath(serviceRoot, oDataPathString); +// } + + // As mentioned above, we also need escaped ODataPath. + // The requestLeftPart and request.RequestUri.Query are both escaped. + // The ODataPath for service documents is empty. +// string oDataPathAndQuery = requestLeftPart.Substring(serviceRoot.Length); +// if (!string.IsNullOrEmpty(request.RequestUri.Query)) +// { + // Ensure path handler receives the query string as well as the path. +// oDataPathAndQuery += request.RequestUri.Query; +// } + + // Leave an escaped '/' out of the service route because DefaultODataPathHandler will add a + // literal '/' to the end of this string if not already present. That would double the slash + // in response links and potentially lead to later 404s. +// if (serviceRoot.EndsWith(escapedSlash, StringComparison.OrdinalIgnoreCase)) +// { +// serviceRoot = serviceRoot.Substring(0, serviceRoot.Length - 3); +// } + +// IODataPathHandler pathHandler = serviceProvider.GetRequiredService(); +// int slashIndex = oDataPathAndQuery.IndexOf('/'); +// oDataPathAndQuery = slashIndex < 0 ? "" : oDataPathAndQuery.Substring(slashIndex + 1); +// path = pathHandler.Parse(serviceRoot, oDataPathAndQuery, serviceProvider); + } + catch (ODataException) + { + path = null; + } + + //if (path != null) + //{ + // // Set all the properties we need for routing, querying, formatting + // request.ODataFeature().Path = path; + // request.ODataFeature().RouteName = this.RouteName; + + // if (!values.ContainsKey(ODataRouteConstants.Controller)) + // { + // // Select controller name using the routing conventions + // string controllerName = this.SelectControllerName(path, request); + // if (controllerName != null) + // { + // values[ODataRouteConstants.Controller] = controllerName; + // } + // } + + // return true; + //} + } + + // The request doesn't match this route so dipose the request container. + request.DeleteRequestContainer(true); + return false; + } + else + { + // This constraint only applies to URI resolution + return true; + } + } + + // Find the substring of the given URI string before the given ODataPath. Tests rely on the following: + // 1. ODataPath comes at the end of the processed Path + // 2. Virtual path root, if any, comes at the beginning of the Path and a '/' separates it from the rest + // 3. OData prefix, if any, comes between the virtual path root and the ODataPath and '/' characters separate + // it from the rest + // 4. Even in the case of Unicode character corrections, the only differences between the escaped Path and the + // unescaped string used for routing are %-escape sequences which may be present in the Path + // + // Therefore, look for the '/' character at which to lop off the ODataPath. Can't just unescape the given + // uriString because subsequent comparisons would only help to check wehther a match is _possible_, not where + // to do the lopping. + private static string RemoveODataPath(string uriString, string oDataPathString) + { + // Potential index of oDataPathString within uriString. + int endIndex = uriString.Length - oDataPathString.Length - 1; + if (endIndex <= 0) + { + // Bizarre: oDataPathString is longer than uriString. Likely the values collection passed to Match() + // is corrupt. + throw new InvalidOperationException($"Request Uri Is Too Short For ODataPath. the Uri is {uriString}, and the OData path is {oDataPathString}."); + } + + string startString = uriString.Substring(0, endIndex + 1); // Potential return value. + string endString = uriString.Substring(endIndex + 1); // Potential oDataPathString match. + if (string.Equals(endString, oDataPathString, StringComparison.Ordinal)) + { + // Simple case, no escaping in the ODataPathString portion of the Path. In this case, don't do extra + // work to look for trailing '/' in startString. + return startString; + } + + while (true) + { + // Escaped '/' is a derivative case but certainly possible. + int slashIndex = startString.LastIndexOf('/', endIndex - 1); + int escapedSlashIndex = + startString.LastIndexOf(escapedSlash, endIndex - 1, StringComparison.OrdinalIgnoreCase); + if (slashIndex > escapedSlashIndex) + { + endIndex = slashIndex; + } + else if (escapedSlashIndex >= 0) + { + // Include the escaped '/' (three characters) in the potential return value. + endIndex = escapedSlashIndex + 2; + } + else + { + // Failure, unable to find the expected '/' or escaped '/' separator. + throw new InvalidOperationException($"The OData path is not found. The Uri is {uriString}, and the OData path is {oDataPathString}."); + } + + startString = uriString.Substring(0, endIndex + 1); + endString = uriString.Substring(endIndex + 1); + + // Compare unescaped strings to avoid both arbitrary escaping and use of lowercase 'a' through 'f' in + // %-escape sequences. + endString = Uri.UnescapeDataString(endString); + if (string.Equals(endString, oDataPathString, StringComparison.Ordinal)) + { + return startString; + } + + if (endIndex == 0) + { + // Failure, could not match oDataPathString after an initial '/' or escaped '/'. + throw new InvalidOperationException($"The OData path is not found. The Uri is {uriString}, and the OData path is {oDataPathString}."); + } + } + } + } +} \ No newline at end of file diff --git a/ODataService/DynamicService/DynamicService.csproj b/ODataService/DynamicService/DynamicService.csproj new file mode 100644 index 0000000000..d20453906a --- /dev/null +++ b/ODataService/DynamicService/DynamicService.csproj @@ -0,0 +1,29 @@ + + + + net7.0 + enable + enable + + + + + + + + + + + + + + + ..\..\ODataServiceTests\bin\Debug\net7.0\NWind_Assembly.dll + + + + + + + + diff --git a/ODataService/DynamicService/Helpers.cs b/ODataService/DynamicService/Helpers.cs new file mode 100644 index 0000000000..d5f00557dd --- /dev/null +++ b/ODataService/DynamicService/Helpers.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.OData.Extensions; +using Microsoft.AspNet.OData.Routing; +using Microsoft.OData.UriParser; + +namespace DynamicService +{ + public static class Helpers + { +#if false + public static TKey GetKeyFromUri(HttpRequestMessage request, Uri uri) + { + if (uri == null) + { + throw new ArgumentNullException("uri"); + } + + var urlHelper = request.GetUrlHelper() ?? new UrlHelper(request); + + var pathHandler = (IODataPathHandler)request.GetRequestContainer().GetService(typeof(IODataPathHandler)); + + string serviceRoot = urlHelper.CreateODataLink( + request.ODataProperties().RouteName, + pathHandler, new List()); + var odataPath = pathHandler.Parse(serviceRoot, uri.LocalPath, request.GetRequestContainer()); + + var keySegment = odataPath.Segments.OfType().FirstOrDefault(); + if (keySegment == null) + { + throw new InvalidOperationException("The link does not contain a key."); + } + + var value = keySegment.Keys.FirstOrDefault().Value; + return (TKey)value; + } +#endif + } +} \ No newline at end of file diff --git a/ODataService/DynamicService/Program.cs b/ODataService/DynamicService/Program.cs new file mode 100644 index 0000000000..bde2542f27 --- /dev/null +++ b/ODataService/DynamicService/Program.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + + +namespace DynamicService +{ + /// + /// Main method. + /// + public static class Program + { + /// + /// Main entry point. + /// + /// + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + /// + /// Creates the host builder. + /// + /// + /// + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/ODataService/DynamicService/Properties/launchSettings.json b/ODataService/DynamicService/Properties/launchSettings.json new file mode 100644 index 0000000000..6e11d982c3 --- /dev/null +++ b/ODataService/DynamicService/Properties/launchSettings.json @@ -0,0 +1,37 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:38951", + "sslPort": 44342 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5096", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7115;http://localhost:5096", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/ODataService/DynamicService/Submit/CustomizedSubmitProcessor.cs b/ODataService/DynamicService/Submit/CustomizedSubmitProcessor.cs new file mode 100644 index 0000000000..dae925fe39 --- /dev/null +++ b/ODataService/DynamicService/Submit/CustomizedSubmitProcessor.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Restier.Core.Submit; + +namespace DynamicService.Submit +{ + public class CustomizedSubmitProcessor : IChangeSetItemFilter + { + private IChangeSetItemFilter Inner { get; set; } + + public Task OnChangeSetItemProcessingAsync(SubmitContext context, ChangeSetItem item, CancellationToken cancellationToken) + { + return Inner.OnChangeSetItemProcessingAsync(context, item, cancellationToken); + } + + public Task OnChangeSetItemProcessedAsync(SubmitContext context, ChangeSetItem item, CancellationToken cancellationToken) + { + var dataModificationItem = item as DataModificationItem; + if (dataModificationItem != null) + { + object myEntity = dataModificationItem.Resource; + string entitySetName = dataModificationItem.ResourceSetName; + var operation = dataModificationItem.EntitySetOperation; + + // In case of insert, the request URL has no key, and request body may not have key neither as the key may be generated by database + var keyAttrbiutes = new Dictionary(); + var keyConvention = new Dictionary(); + + var entityTypeName = myEntity.GetType().Name; + PropertyInfo[] properties = myEntity.GetType().GetProperties(); + + foreach (PropertyInfo property in properties) + { + var attribute = Attribute.GetCustomAttribute(property, typeof(KeyAttribute)) + as KeyAttribute; + var propName = property.Name; + // This is getting key with Key attribute defined + if (attribute != null) // This property has a KeyAttribute + { + // Do something, to read from the property: + object val = property.GetValue(myEntity); + keyAttrbiutes.Add(propName, val); + } + // This is getting key based on convention + else if(propName.ToLower().Equals("id") || propName.ToLower().Equals(entityTypeName.ToLower()+"id")) + { + object val = property.GetValue(myEntity); + keyConvention.Add(propName, val); + } + } + if (keyAttrbiutes.Count > 0) + { + // Use property with key attribute as keys + } + else if(keyConvention.Count > 0) + { + // Key is defined based on convention + } + } + return Inner.OnChangeSetItemProcessedAsync(context, item, cancellationToken); + } + } +} \ No newline at end of file diff --git a/ODataService/DynamicService/appsettings.Development.json b/ODataService/DynamicService/appsettings.Development.json new file mode 100644 index 0000000000..770d3e9314 --- /dev/null +++ b/ODataService/DynamicService/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/ODataService/DynamicService/appsettings.json b/ODataService/DynamicService/appsettings.json new file mode 100644 index 0000000000..10f68b8c8b --- /dev/null +++ b/ODataService/DynamicService/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/ODataServiceTests/DynamicDataSourceTests.cs b/ODataServiceTests/DynamicDataSourceTests.cs new file mode 100644 index 0000000000..f58f404f93 --- /dev/null +++ b/ODataServiceTests/DynamicDataSourceTests.cs @@ -0,0 +1,126 @@ +using DataSourceGenerator; +using System.ComponentModel.Design; +using Microsoft.Restier.Core.Submit; +using Microsoft.Restier.Core.Query; +using System.Linq.Expressions; + +namespace ODataServiceTests +{ + [TestClass] + public class DynamicDataSourceTests + { + public class Person + { + public string firstName { get; set; } + public string lastName { get; set; } + public int age { get; set; } + } + + public class Job + { + public string title { get; set; } + public Person employee { get; set; } + } + + public class Company + { + public string name { get; set; } + public List employees { get; set; } + } + + public class NWindTestData : DynamicDataSource + { + public static NWindTestData GetNWindTestData() + { + var sp = new ServiceContainer(); + sp.AddService(typeof(ISubmitExecutor), new MockSubmitExecutor()); + sp.AddService(typeof(IQueryExpressionSourcer), new MockQueryExpressionSourcer()); + sp.AddService(typeof(IChangeSetInitializer), new MockChangeSetInitializer()); + + return new NWindTestData(sp); + } + + public NWindTestData(IServiceProvider sp) : base(sp) + { + people = new List(); + company = new Company(); + } + + public List people { get; set; } + public Company company { get; set; } + } + + public IServiceProvider buildServiceProvider() + { + var sp = new ServiceContainer(); + return sp; + } + + [TestMethod] + public void TestLoad() + { + var dataSource = NWindTestData.GetNWindTestData(); + + var json = @" + { + ""people"": [ + { + ""firstName"":""Joe"", + ""lastName"":""Smith"", + ""age"":23 + }, + { + ""firstName"":""Jane"", + ""lastName"":""Doe"", + ""age"":21 + } + ], + ""company"": + { + ""name"":""widgets"", + ""employees"": [ + { + ""firstName"":""Joe"", + ""lastName"":""Smith"", + ""age"":23 + }, + { + ""firstName"":""Jane"", + ""lastName"":""Doe"", + ""age"":21 + } + ] + } + }"; + + dataSource.Load(json); + Assert.AreEqual(2, dataSource.people.Count); + Assert.AreEqual(2, dataSource.company.employees.Count); + } + } + + public class MockQueryExpressionSourcer : IQueryExpressionSourcer + { + public Expression ReplaceQueryableSource(QueryExpressionContext context, bool embedded) + { + throw new NotImplementedException(); + } + } + + public class MockChangeSetInitializer : IChangeSetInitializer + { + public Task InitializeAsync(SubmitContext context, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } + + public class MockSubmitExecutor : ISubmitExecutor + { + public Task ExecuteSubmitAsync(SubmitContext context, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } + +} \ No newline at end of file