diff --git a/RESTier.sln b/RESTier.sln
index 696fc3f4..6f3fa65e 100644
--- a/RESTier.sln
+++ b/RESTier.sln
@@ -39,6 +39,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Restier.Publisher
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Restier.Providers.EntityFramework", "src\Microsoft.Restier.Providers.EntityFramework\Microsoft.Restier.Providers.EntityFramework.csproj", "{F7EC910E-17CE-4579-84C5-36D3777B3218}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.OData.Service.Library", "test\ODataEndToEnd\Microsoft.OData.Service.Library\Microsoft.OData.Service.Library.csproj", "{1CA9B17F-D3F8-4FC3-A992-5135DCCAB9DE}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -93,6 +95,10 @@ Global
{F7EC910E-17CE-4579-84C5-36D3777B3218}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F7EC910E-17CE-4579-84C5-36D3777B3218}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F7EC910E-17CE-4579-84C5-36D3777B3218}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1CA9B17F-D3F8-4FC3-A992-5135DCCAB9DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1CA9B17F-D3F8-4FC3-A992-5135DCCAB9DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1CA9B17F-D3F8-4FC3-A992-5135DCCAB9DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1CA9B17F-D3F8-4FC3-A992-5135DCCAB9DE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -114,5 +120,6 @@ Global
{31FE1F5B-7CD8-48F1-9CE3-4E57A0066440} = {552DD8A7-2F3A-4D0F-B623-B7D832C6C62B}
{186F667E-54E5-4B57-9998-21D74CB77C24} = {432208D4-54DF-453E-96AE-CB7721461030}
{F7EC910E-17CE-4579-84C5-36D3777B3218} = {0355FEC8-17CF-44B4-9D24-685266A349FB}
+ {1CA9B17F-D3F8-4FC3-A992-5135DCCAB9DE} = {4AC28EC2-FBCF-44CA-A922-0B257F55DE0D}
EndGlobalSection
EndGlobal
diff --git a/test/ODataEndToEnd/Microsoft.OData.Service.Library/DataStoreManager/DefaultDataStoreManager.cs b/test/ODataEndToEnd/Microsoft.OData.Service.Library/DataStoreManager/DefaultDataStoreManager.cs
new file mode 100644
index 00000000..14249bb3
--- /dev/null
+++ b/test/ODataEndToEnd/Microsoft.OData.Service.Library/DataStoreManager/DefaultDataStoreManager.cs
@@ -0,0 +1,157 @@
+// 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.Concurrent;
+using System.Collections.Generic;
+using System.Timers;
+
+namespace Microsoft.OData.Service.Library.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 reasource, 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; } = 1000;
+
+ ///
+ /// 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; } = new TimeSpan(0, 15, 0);
+
+ private Dictionary _dataStoreDict = new Dictionary();
+
+ public TDataStoreType ResetDataStoreInstance(TKey key)
+ {
+ if (_dataStoreDict.ContainsKey(key))
+ {
+ _dataStoreDict[key] = new DataStoreUnit(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 DataStoreUnit(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 DataStoreUnit;
+ if (resouceUnit != null)
+ {
+ System.Diagnostics.Trace.TraceInformation(resouceUnit.DatastoreKey + " timeout occured, now destroy it!");
+ DeleteDataStoreInstance(resouceUnit.DatastoreKey);
+ }
+ }
+
+ private class DataStoreUnit
+ {
+ public TKey DatastoreKey { get; }
+
+ public TDataStoreType DataStore { get; }
+
+ public DateTime DataStoreLastUsedDateTime { get; private set; }
+
+ private Timer DataStoreTimer { get; set; }
+
+ private double _dataStoreLifeTime;
+
+ private EventHandler _timerTimeoutHandler;
+
+ public DataStoreUnit(TKey key, double dataStoreLifeTime, EventHandler dataStoreTimeoutHandler)
+ {
+ DatastoreKey = key;
+ DataStore = new TDataStoreType();
+ DataStoreLastUsedDateTime = DateTime.Now;
+ _dataStoreLifeTime = dataStoreLifeTime;
+ _timerTimeoutHandler += dataStoreTimeoutHandler;
+ InitTimer();
+ }
+
+ public DataStoreUnit UpdateLastUsedDateTime()
+ {
+ UpdateTimer();
+ DataStoreLastUsedDateTime = DateTime.Now;
+ return this;
+ }
+
+ public void StopTimer()
+ {
+ DataStoreTimer.Stop();
+ }
+
+ private Timer InitTimer()
+ {
+ DataStoreTimer = new Timer(_dataStoreLifeTime);
+ DataStoreTimer.Elapsed += (sender, args) => { _timerTimeoutHandler?.Invoke(this, args); };
+ DataStoreTimer.Start();
+ return DataStoreTimer;
+ }
+
+ private void UpdateTimer()
+ {
+ DataStoreTimer.Stop();
+ DataStoreTimer = InitTimer();
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/test/ODataEndToEnd/Microsoft.OData.Service.Library/DataStoreManager/IDataStoreManager.cs b/test/ODataEndToEnd/Microsoft.OData.Service.Library/DataStoreManager/IDataStoreManager.cs
new file mode 100644
index 00000000..6e604cd5
--- /dev/null
+++ b/test/ODataEndToEnd/Microsoft.OData.Service.Library/DataStoreManager/IDataStoreManager.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+namespace Microsoft.OData.Service.Library.DataStoreManager
+{
+ ///
+ /// Resource management interface.
+ ///
+ public interface IDataStoreManager
+ {
+ TDataStoreType GetDataStoreInstance(TKey key);
+ TDataStoreType ResetDataStoreInstance(TKey key);
+ }
+}
\ No newline at end of file
diff --git a/test/ODataEndToEnd/Microsoft.OData.Service.Library/Microsoft.OData.Service.Library.csproj b/test/ODataEndToEnd/Microsoft.OData.Service.Library/Microsoft.OData.Service.Library.csproj
new file mode 100644
index 00000000..768fcd45
--- /dev/null
+++ b/test/ODataEndToEnd/Microsoft.OData.Service.Library/Microsoft.OData.Service.Library.csproj
@@ -0,0 +1,65 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {1CA9B17F-D3F8-4FC3-A992-5135DCCAB9DE}
+ Library
+ Properties
+ Microsoft.OData.Service.Library
+ Microsoft.OData.Service.Library
+ v4.5.2
+ 512
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+ ..\..\..\packages\Newtonsoft.Json.6.0.4\lib\net45\Newtonsoft.Json.dll
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/ODataEndToEnd/Microsoft.OData.Service.Library/Properties/AssemblyInfo.cs b/test/ODataEndToEnd/Microsoft.OData.Service.Library/Properties/AssemblyInfo.cs
new file mode 100644
index 00000000..829ed9c6
--- /dev/null
+++ b/test/ODataEndToEnd/Microsoft.OData.Service.Library/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/test/ODataEndToEnd/Microsoft.OData.Service.Library/Utils/LibraryUtils.cs b/test/ODataEndToEnd/Microsoft.OData.Service.Library/Utils/LibraryUtils.cs
new file mode 100644
index 00000000..1b6f8736
--- /dev/null
+++ b/test/ODataEndToEnd/Microsoft.OData.Service.Library/Utils/LibraryUtils.cs
@@ -0,0 +1,14 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+namespace Microsoft.OData.Service.Library.Utils
+{
+ public static class LibraryUtils
+ {
+ static public string GetSessionId()
+ {
+ var session = System.Web.HttpContext.Current.Session;
+ return session?.SessionID;
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/ODataEndToEnd/Microsoft.OData.Service.Library/Utils/ODataSessionIdManager.cs b/test/ODataEndToEnd/Microsoft.OData.Service.Library/Utils/ODataSessionIdManager.cs
new file mode 100644
index 00000000..8bcfbcb3
--- /dev/null
+++ b/test/ODataEndToEnd/Microsoft.OData.Service.Library/Utils/ODataSessionIdManager.cs
@@ -0,0 +1,75 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License. See License.txt in the project root for license information.
+
+using System.Text.RegularExpressions;
+using System.Web;
+using System.Web.SessionState;
+
+namespace Microsoft.OData.Service.Library.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 = HttpContext.Current.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 = HttpContext.Current.Request.Headers["X-Original-URL"];
+
+ if (!string.IsNullOrEmpty(originalUrl))
+ {
+ var match = Regex.Match(HttpContext.Current.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);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/ODataEndToEnd/Microsoft.OData.Service.Library/packages.config b/test/ODataEndToEnd/Microsoft.OData.Service.Library/packages.config
new file mode 100644
index 00000000..365d26fe
--- /dev/null
+++ b/test/ODataEndToEnd/Microsoft.OData.Service.Library/packages.config
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/test/ODataEndToEnd/Microsoft.OData.Service.Sample.TrippinInMemory/Api/TrippinApi.cs b/test/ODataEndToEnd/Microsoft.OData.Service.Sample.TrippinInMemory/Api/TrippinApi.cs
new file mode 100644
index 00000000..2e80b5b2
--- /dev/null
+++ b/test/ODataEndToEnd/Microsoft.OData.Service.Sample.TrippinInMemory/Api/TrippinApi.cs
@@ -0,0 +1,502 @@
+// // Copyright (c) Microsoft Corporation. All rights reserved.
+// // Licensed under the MIT License. See License.txt in the project root for license information.
+
+#region
+
+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 System.Web.OData.Builder;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.OData.Edm;
+using Microsoft.OData.Service.Library.DataStoreManager;
+using Microsoft.OData.Service.Library.Utils;
+using Microsoft.OData.Service.Sample.TrippinInMemory.Models;
+using Microsoft.Restier.Core;
+using Microsoft.Restier.Core.Model;
+using Microsoft.Restier.Core.Submit;
+using Microsoft.Restier.Publishers.OData.Model;
+using Microsoft.Spatial;
+
+#endregion
+
+namespace Microsoft.OData.Service.Sample.TrippinInMemory.Api
+{
+ public class TrippinApi : ApiBase
+ {
+ private static IDataStoreManager _dataStoreManager
+ = new DefaultDataStoreManager()
+ {
+ MaxDataStoreInstanceCapacity = 1000,
+ MaxDataStoreInstanceLifeTime = new TimeSpan(0, 30, 0)
+ };
+
+ private string Key
+ {
+ get { return LibraryUtils.GetSessionId(); }
+ }
+
+ #region Entity Set
+
+ public IQueryable People
+ {
+ get
+ {
+ var datasource = _dataStoreManager.GetDataStoreInstance(Key);
+ return datasource?.People.AsQueryable();
+ }
+ }
+
+ public Person Me
+ {
+ get
+ {
+ var datasource = _dataStoreManager.GetDataStoreInstance(Key);
+ return datasource?.Me;
+ }
+ }
+
+ public IQueryable Airlines
+ {
+ get
+ {
+ var datasource = _dataStoreManager.GetDataStoreInstance(Key);
+ return datasource?.Airlines.AsQueryable();
+ }
+ }
+
+ public IQueryable Airports
+ {
+ get
+ {
+ var datasource = _dataStoreManager.GetDataStoreInstance(Key);
+ return datasource?.Airports.AsQueryable();
+ }
+ }
+
+ #endregion
+
+ #region function/action
+
+ ///
+ /// Unbound function, Get Person with most friends.
+ ///
+ ///
+ ///
+ ///
+ [Operation(EntitySet = "People")]
+ public Person GetPersonWithMostFriends()
+ {
+ Person result = null;
+ foreach (var person in People)
+ {
+ if (person.Friends == null)
+ {
+ continue;
+ }
+
+ if (result == null)
+ {
+ result = person;
+ }
+
+ if (person.Friends.Count > result.Friends.Count)
+ {
+ result = person;
+ }
+ }
+
+ return result;
+ }
+
+ ///
+ /// Unbound function, get nearest aireport to GeographyPoint(lat, lon).
+ ///
+ /// Latitude
+ /// Longitude
+ ///
+ ///
+ ///
+ [Operation(EntitySet = "Airports")]
+ public Airport GetNearestAirport(double lat, double lon)
+ {
+ var startPoint = GeographyPoint.Create(lat, lon);
+ double minDistance = 2;
+ Airport nearestAirport = null;
+
+ foreach (var airport in Airports)
+ {
+ var distance = CalculateDistance(startPoint, airport.Location.Loc);
+ if (distance < minDistance)
+ {
+ nearestAirport = airport;
+ minDistance = distance;
+ }
+ }
+
+ return nearestAirport;
+ }
+
+ [Operation(IsBound = true)]
+ public Airline GetFavoriteAirline(Person person)
+ {
+ var countDict = new Dictionary();
+ foreach (var a in Airlines)
+ {
+ countDict.Add(a.AirlineCode, 0);
+ }
+
+ foreach (var t in person.Trips)
+ {
+ foreach (var p in t.PlanItems)
+ {
+ var f = p as Flight;
+ if (f != null)
+ {
+ countDict[f.Airline.AirlineCode]++;
+ }
+ }
+ }
+
+ var max = -1;
+ string favoriteAirlineCode = null;
+ foreach (var record in countDict)
+ {
+ if (max < record.Value)
+ {
+ favoriteAirlineCode = record.Key;
+ max = record.Value;
+ }
+ }
+
+ return Airlines.Single(a => a.AirlineCode.Equals(favoriteAirlineCode));
+ }
+
+
+ ///
+ /// Bound Function, get the trips of one friend with userName
+ ///
+ [Operation(IsBound = true)]
+ public ICollection GetFriendsTrips(Person person, string userName)
+ {
+ var friends = person.Friends.Where(p => p.UserName.Equals(userName)).ToArray();
+ if (friends.Count() == 0)
+ {
+ //todo: in this case it should throw a 404 not found error.
+ return new Collection();
+ }
+ else
+ {
+ return friends[0].Trips;
+ }
+ }
+
+ [Operation(IsBound = true)]
+ public ICollection GetInvolvedPeople(Trip trip)
+ {
+ var shareID = trip.ShareId;
+ ICollection sharingPersons = new Collection();
+
+ foreach (var person in People)
+ {
+ if (person.Trips != null)
+ {
+ foreach (var t in person.Trips)
+ {
+ if (shareID.Equals(t.ShareId))
+ {
+ sharingPersons.Add(person);
+ break;
+ }
+ }
+ }
+ }
+
+ return sharingPersons;
+ }
+
+ ///
+ /// Unbound action, reset datasource.
+ ///
+ [Operation(HasSideEffects = true)]
+ public void ResetDataSource()
+ {
+ _dataStoreManager.ResetDataStoreInstance(Key);
+ }
+
+ ///
+ /// Bound Action, update the last name of one person.
+ ///
+ /// The person to be updated.
+ /// The value of last name to be updated.
+ /// True if update successfully.
+ [Operation(IsBound = true)]
+ public bool UpdatePersonLastName(Person person, string lastName)
+ {
+ if (person != null)
+ {
+ person.LastName = lastName;
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ [Operation(IsBound = true, HasSideEffects = true)]
+ public void ShareTrip(Person personInstance, string userName, int tripId)
+ {
+ if (personInstance == null)
+ {
+ throw new ArgumentNullException("personInstance");
+ }
+ if (string.IsNullOrEmpty(userName))
+ {
+ throw new ArgumentNullException("userName");
+ }
+ if (tripId < 0)
+ {
+ throw new ArgumentNullException("tripId");
+ }
+
+ var tripInstance = personInstance.Trips.FirstOrDefault(item => item.TripId == tripId);
+
+ if (tripInstance == null)
+ {
+ throw new Exception(string.Format("Can't get trip with ID '{0}' in person '{1}'", tripId,
+ personInstance.UserName));
+ }
+
+ var friendInstance = personInstance.Friends.FirstOrDefault(item => item.UserName == userName);
+
+ if (friendInstance == null)
+ {
+ throw new Exception(string.Format("Can't get friend with userName '{0}' in person '{1}'", userName,
+ personInstance.UserName));
+ }
+
+ if (friendInstance.Trips != null && friendInstance.Trips.All(item => item.TripId != tripId))
+ {
+ //TODO, should return 201 if we add new entity, those behavior should be update in handler.
+ var newTrip = tripInstance.Clone() as Trip;
+ var maxTripId = friendInstance.Trips.Select(item => item.TripId).Max();
+ newTrip.TripId = maxTripId + 1;
+ friendInstance.Trips.Add(newTrip);
+ }
+ }
+
+ private static double CalculateDistance(GeographyPoint p1, GeographyPoint p2)
+ {
+ // using Haversine formula
+ // refer to http://en.wikipedia.org/wiki/Haversine_formula.
+ var lat1 = Math.PI*p1.Latitude/180;
+ var lat2 = Math.PI*p2.Latitude/180;
+ var lon1 = Math.PI*p1.Longitude/180;
+ var lon2 = Math.PI*p2.Longitude/180;
+ var item1 = Math.Sin((lat1 - lat2)/2)*Math.Sin((lat1 - lat2)/2);
+ var item2 = Math.Cos(lat1)*Math.Cos(lat2)*Math.Sin((lon1 - lon2)/2)*Math.Sin((lon1 - lon2)/2);
+ return Math.Asin(Math.Sqrt(item1 + item2));
+ }
+
+ #endregion
+
+ protected override IServiceCollection ConfigureApi(IServiceCollection services)
+ {
+ services.AddService((sp, next) => new ModelBuilder());
+ services.AddService((sp, next) => new CustomerizedChangeSetInitializer());
+ services.AddService((sp, next) => new CustomerizedSubmitExecutor());
+ return base.ConfigureApi(services);
+ }
+
+ private class ModelBuilder : IModelBuilder
+ {
+ public Task GetModelAsync(ModelContext context, CancellationToken cancellationToken)
+ {
+ var modelBuilder = new ODataConventionModelBuilder();
+ modelBuilder.EntityType();
+ return Task.FromResult(modelBuilder.GetEdmModel());
+ }
+ }
+
+ #region Services
+
+ private class CustomerizedSubmitExecutor : ISubmitExecutor
+ {
+ public Task ExecuteSubmitAsync(SubmitContext context, CancellationToken cancellationToken)
+ {
+ return Task.FromResult(new SubmitResult(context.ChangeSet));
+ }
+ }
+
+ ///
+ /// ChangeSetInitializer class.
+ /// Since our datasource is in memory,
+ /// we just confirm the data change here, not in SubmitExecutor
+ ///
+ private class CustomerizedChangeSetInitializer : IChangeSetInitializer
+ {
+ public async Task InitializeAsync(SubmitContext context, CancellationToken cancellationToken)
+ {
+ var key = LibraryUtils.GetSessionId();
+ var dataSource = _dataStoreManager.GetDataStoreInstance(key);
+ foreach (var dataModificationItem in context.ChangeSet.Entries.OfType())
+ {
+ var expectedEntiType = dataModificationItem.ExpectedResourceType;
+ var operation = dataModificationItem.DataModificationItemAction;
+ object entity;
+ switch (operation)
+ {
+ case DataModificationItemAction.Insert:
+ {
+ // Here we create a instance of entity, parameters are from the request.
+ // Known issues: 1) not support odata.id
+ // 2) not support nested entity.
+ entity = Activator.CreateInstance(expectedEntiType);
+ SetValues(entity, expectedEntiType, dataModificationItem.LocalValues);
+ dataModificationItem.Resource = entity;
+
+ // insert new entity into entity set
+ var entitySetProp = GetEntitySetPropertyInfoFromDataModificationItem(dataSource,
+ dataModificationItem);
+
+ if (entitySetProp != null && entitySetProp.CanWrite)
+ {
+ var originSet = entitySetProp.GetValue(dataSource);
+ entitySetProp.PropertyType.GetMethod("Add").Invoke(originSet, new[] {entity});
+ }
+ }
+ break;
+ case DataModificationItemAction.Update:
+ {
+ entity = FindEntity(dataSource, context, dataModificationItem, cancellationToken);
+ dataModificationItem.Resource = entity;
+
+ // update the entity
+ if (entity != null)
+ {
+ SetValues(entity, expectedEntiType, dataModificationItem.LocalValues);
+ }
+ }
+ break;
+ case DataModificationItemAction.Remove:
+ {
+ entity = FindEntity(dataSource, context, dataModificationItem, cancellationToken);
+ dataModificationItem.Resource = entity;
+
+ // remove the entity
+ if (entity != null)
+ {
+ var entitySetProp = GetEntitySetPropertyInfoFromDataModificationItem(dataSource,
+ dataModificationItem);
+
+ if (entitySetProp != null && entitySetProp.CanWrite)
+ {
+ var originSet = entitySetProp.GetValue(dataSource);
+ entitySetProp.PropertyType.GetMethod("Remove").Invoke(originSet, new[] {entity});
+ }
+ }
+ }
+ break;
+ case DataModificationItemAction.Undefined:
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+ }
+
+ private static void SetValues(object instance, Type type, IReadOnlyDictionary values)
+ {
+ foreach (KeyValuePair propertyPair in values)
+ {
+ object value = propertyPair.Value;
+ PropertyInfo propertyInfo = type.GetProperty(propertyPair.Key);
+ 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)
+ {
+ throw new NotSupportedException(string.Format(
+ CultureInfo.InvariantCulture,
+ propertyPair.Key));
+ }
+
+ value = Activator.CreateInstance(propertyInfo.PropertyType);
+ SetValues(value, propertyInfo.PropertyType, dic);
+ }
+
+ propertyInfo.SetValue(instance, value);
+ }
+ }
+
+ private static object FindEntity(
+ object instance,
+ SubmitContext context,
+ DataModificationItem item,
+ CancellationToken cancellationToken)
+ {
+ var entitySetPropertyInfo = GetEntitySetPropertyInfoFromDataModificationItem(instance, item);
+ var originSet = entitySetPropertyInfo.GetValue(instance);
+
+ object entity = null;
+ var enumerableSet = originSet as IEnumerable