diff --git a/Microsoft.Azure.Cosmos/src/CosmosIndexJsonConverter.cs b/Microsoft.Azure.Cosmos/src/CosmosIndexJsonConverter.cs new file mode 100644 index 0000000000..466782e768 --- /dev/null +++ b/Microsoft.Azure.Cosmos/src/CosmosIndexJsonConverter.cs @@ -0,0 +1,89 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos +{ + using System; + using System.Globalization; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + + internal sealed class CosmosIndexJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return typeof(Index).IsAssignableFrom(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (objectType != typeof(Index)) + { + return null; + } + + JToken indexToken = JToken.Load(reader); + + if (indexToken.Type == JTokenType.Null) + { + return null; + } + + if (indexToken.Type != JTokenType.Object) + { + throw new JsonSerializationException( + string.Format(CultureInfo.CurrentCulture, Documents.RMResources.InvalidIndexSpecFormat)); + } + + JToken indexKindToken = indexToken[Documents.Constants.Properties.IndexKind]; + if (indexKindToken == null || indexKindToken.Type != JTokenType.String) + { + throw new JsonSerializationException( + string.Format(CultureInfo.CurrentCulture, Documents.RMResources.InvalidIndexSpecFormat)); + } + + IndexKind indexKind = IndexKind.Hash; + if (Enum.TryParse(indexKindToken.Value(), out indexKind)) + { + object index = null; + switch (indexKind) + { + case IndexKind.Hash: + index = new HashIndex(); + break; + case IndexKind.Range: + index = new RangeIndex(); + break; + case IndexKind.Spatial: + index = new SpatialIndex(); + break; + default: + throw new JsonSerializationException( + string.Format(CultureInfo.CurrentCulture, Documents.RMResources.InvalidIndexKindValue, indexKind)); + } + + serializer.Populate(indexToken.CreateReader(), index); + return index; + } + else + { + throw new JsonSerializationException( + string.Format(CultureInfo.CurrentCulture, Documents.RMResources.InvalidIndexKindValue, indexKindToken.Value())); + } + } + + public override bool CanWrite + { + get + { + return false; + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/src/Resource/Settings/Index.cs b/Microsoft.Azure.Cosmos/src/Resource/Settings/Index.cs index aed00e3cc2..4141a0e1fa 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/Settings/Index.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/Settings/Index.cs @@ -11,7 +11,7 @@ namespace Microsoft.Azure.Cosmos /// /// Base class for IndexingPolicy Indexes in the Azure Cosmos DB service, you should use a concrete Index like HashIndex or RangeIndex. /// - [JsonConverter(typeof(IndexJsonConverter))] + [JsonConverter(typeof(CosmosIndexJsonConverter))] internal abstract class Index { /// diff --git a/Microsoft.Azure.Cosmos/src/Resource/Settings/SpatialPath.cs b/Microsoft.Azure.Cosmos/src/Resource/Settings/SpatialPath.cs index 723a4c489f..879ffbdc79 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/Settings/SpatialPath.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/Settings/SpatialPath.cs @@ -15,7 +15,6 @@ namespace Microsoft.Azure.Cosmos /// public sealed class SpatialPath { - [JsonProperty(PropertyName = Constants.Properties.Types, ItemConverterType = typeof(StringEnumConverter))] private Collection spatialTypesInternal; /// @@ -27,6 +26,7 @@ public sealed class SpatialPath /// /// Path's spatial type /// + [JsonProperty(PropertyName = Constants.Properties.Types, ItemConverterType = typeof(StringEnumConverter))] public Collection SpatialTypes { get diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ChangeFeed/DynamicTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ChangeFeed/DynamicTests.cs index 757a9dcf16..14a076dfe3 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ChangeFeed/DynamicTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ChangeFeed/DynamicTests.cs @@ -81,8 +81,8 @@ public async Task TestWithRunningProcessor() [TestMethod] public async Task TestWithFixedLeaseContainer() { - await CosmosItemTests.CreateNonPartitionedContainer( - this.database.Id, + await NonPartitionedContainerHelper.CreateNonPartitionedContainer( + this.database, "fixedLeases"); Container fixedLeasesContainer = this.cosmosClient.GetContainer(this.database.Id, "fixedLeases"); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs index afdd89bb49..89db954e14 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosItemTests.cs @@ -34,11 +34,7 @@ public class CosmosItemTests : BaseCosmosClientHelper private Container Container = null; private ContainerProperties containerSettings = null; - private static readonly string utc_date = DateTime.UtcNow.ToString("r"); - - private static readonly string PreNonPartitionedMigrationApiVersion = "2018-09-17"; private static readonly string nonPartitionItemId = "fixed-Container-Item"; - private static readonly string undefinedPartitionItemId = "undefined-partition-Item"; [TestInitialize] @@ -1263,9 +1259,12 @@ public async Task ReadNonPartitionItemAsync() ContainerCore fixedContainer = null; try { - fixedContainer = await this.CreateNonPartitionedContainer("ReadNonPartition" + Guid.NewGuid()); - await this.CreateItemInNonPartitionedContainer(fixedContainer, nonPartitionItemId); - await this.CreateUndefinedPartitionItem((ContainerCore)this.Container); + fixedContainer = await NonPartitionedContainerHelper.CreateNonPartitionedContainer( + this.database, + "ReadNonPartition" + Guid.NewGuid()); + + await NonPartitionedContainerHelper.CreateItemInNonPartitionedContainer(fixedContainer, nonPartitionItemId); + await NonPartitionedContainerHelper.CreateUndefinedPartitionItem((ContainerCore)this.Container, undefinedPartitionItemId); ContainerResponse containerResponse = await fixedContainer.ReadContainerAsync(); Assert.IsTrue(containerResponse.Resource.PartitionKey.Paths.Count > 0); @@ -1390,13 +1389,15 @@ public async Task MigrateDataInNonPartitionContainer() ContainerCore fixedContainer = null; try { - fixedContainer = await this.CreateNonPartitionedContainer("ItemTestMigrateData" + Guid.NewGuid().ToString()); + fixedContainer = await NonPartitionedContainerHelper.CreateNonPartitionedContainer( + this.database, + "ItemTestMigrateData" + Guid.NewGuid().ToString()); const int ItemsToCreate = 4; // Insert a few items with no Partition Key for (int i = 0; i < ItemsToCreate; i++) { - await this.CreateItemInNonPartitionedContainer(fixedContainer, Guid.NewGuid().ToString()); + await NonPartitionedContainerHelper.CreateItemInNonPartitionedContainer(fixedContainer, Guid.NewGuid().ToString()); } // Read the container metadata @@ -1645,124 +1646,6 @@ private static async Task ExecuteReadFeedAsync(Container container, HttpStatusCo } } - private async Task CreateNonPartitionedContainer(string id) - { - await CosmosItemTests.CreateNonPartitionedContainer( - this.database.Id, - id); - - return (ContainerCore)this.cosmosClient.GetContainer(this.database.Id, id); - } - - internal static async Task CreateNonPartitionedContainer( - string dbName, - string containerName, - string indexingPolicyString = null) - { - string authKey = ConfigurationManager.AppSettings["MasterKey"]; - string endpoint = ConfigurationManager.AppSettings["GatewayEndpoint"]; - //Creating non partition Container, rest api used instead of .NET SDK api as it is not supported anymore. - HttpClient client = new System.Net.Http.HttpClient(); - Uri baseUri = new Uri(endpoint); - string verb = "POST"; - string resourceType = "colls"; - string resourceId = string.Format("dbs/{0}", dbName); - string resourceLink = string.Format("dbs/{0}/colls", dbName); - client.DefaultRequestHeaders.Add("x-ms-date", utc_date); - client.DefaultRequestHeaders.Add("x-ms-version", CosmosItemTests.PreNonPartitionedMigrationApiVersion); - - string authHeader = CosmosItemTests.GenerateMasterKeyAuthorizationSignature(verb, resourceId, resourceType, authKey, "master", "1.0"); - - client.DefaultRequestHeaders.Add("authorization", authHeader); - DocumentCollection documentCollection = new DocumentCollection() - { - Id = containerName - }; - if (indexingPolicyString != null) - { - documentCollection.IndexingPolicy = JsonConvert.DeserializeObject(indexingPolicyString); - } - string containerDefinition = documentCollection.ToString(); - StringContent containerContent = new StringContent(containerDefinition); - Uri requestUri = new Uri(baseUri, resourceLink); - HttpResponseMessage response = await client.PostAsync(requestUri.ToString(), containerContent); - Assert.AreEqual(HttpStatusCode.Created, response.StatusCode, response.ToString()); - } - - private async Task CreateItemInNonPartitionedContainer(ContainerCore container, string itemId) - { - string authKey = ConfigurationManager.AppSettings["MasterKey"]; - string endpoint = ConfigurationManager.AppSettings["GatewayEndpoint"]; - //Creating non partition Container item. - HttpClient client = new System.Net.Http.HttpClient(); - Uri baseUri = new Uri(endpoint); - string verb = "POST"; - string resourceType = "docs"; - string resourceLink = string.Format("dbs/{0}/colls/{1}/docs", this.database.Id, container.Id); - string authHeader = CosmosItemTests.GenerateMasterKeyAuthorizationSignature(verb, container.LinkUri.OriginalString, resourceType, authKey, "master", "1.0"); - - client.DefaultRequestHeaders.Add("x-ms-date", utc_date); - client.DefaultRequestHeaders.Add("x-ms-version", CosmosItemTests.PreNonPartitionedMigrationApiVersion); - client.DefaultRequestHeaders.Add("authorization", authHeader); - - string itemDefinition = JsonConvert.SerializeObject(ToDoActivity.CreateRandomToDoActivity(id: itemId)); - { - StringContent itemContent = new StringContent(itemDefinition); - Uri requestUri = new Uri(baseUri, resourceLink); - HttpResponseMessage response = await client.PostAsync(requestUri.ToString(), itemContent); - Assert.AreEqual(HttpStatusCode.Created, response.StatusCode, response.ToString()); - } - } - - private async Task CreateUndefinedPartitionItem(ContainerCore container) - { - string authKey = ConfigurationManager.AppSettings["MasterKey"]; - string endpoint = ConfigurationManager.AppSettings["GatewayEndpoint"]; - //Creating undefined partition key item, rest api used instead of .NET SDK api as it is not supported anymore. - HttpClient client = new System.Net.Http.HttpClient(); - Uri baseUri = new Uri(endpoint); - client.DefaultRequestHeaders.Add("x-ms-date", utc_date); - client.DefaultRequestHeaders.Add("x-ms-version", CosmosItemTests.PreNonPartitionedMigrationApiVersion); - client.DefaultRequestHeaders.Add("x-ms-documentdb-partitionkey", "[{}]"); - - //Creating undefined partition Container item. - string verb = "POST"; - string resourceType = "docs"; - string resourceId = container.LinkUri.OriginalString; - string resourceLink = string.Format("dbs/{0}/colls/{1}/docs", this.database.Id, container.Id); - string authHeader = CosmosItemTests.GenerateMasterKeyAuthorizationSignature(verb, resourceId, resourceType, authKey, "master", "1.0"); - - client.DefaultRequestHeaders.Remove("authorization"); - client.DefaultRequestHeaders.Add("authorization", authHeader); - - var payload = new { id = undefinedPartitionItemId, user = undefinedPartitionItemId }; - string itemDefinition = JsonConvert.SerializeObject(payload); - StringContent itemContent = new StringContent(itemDefinition); - Uri requestUri = new Uri(baseUri, resourceLink); - await client.PostAsync(requestUri.ToString(), itemContent); - } - - private static string GenerateMasterKeyAuthorizationSignature(string verb, string resourceId, string resourceType, string key, string keyType, string tokenVersion) - { - System.Security.Cryptography.HMACSHA256 hmacSha256 = new System.Security.Cryptography.HMACSHA256 { Key = Convert.FromBase64String(key) }; - - string payLoad = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}\n{1}\n{2}\n{3}\n{4}\n", - verb.ToLowerInvariant(), - resourceType.ToLowerInvariant(), - resourceId, - utc_date.ToLowerInvariant(), - "" - ); - - byte[] hashPayLoad = hmacSha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(payLoad)); - string signature = Convert.ToBase64String(hashPayLoad); - - return System.Web.HttpUtility.UrlEncode(string.Format(System.Globalization.CultureInfo.InvariantCulture, "type={0}&ver={1}&sig={2}", - keyType, - tokenVersion, - signature)); - } - public class ToDoActivityAfterMigration { public string id { get; set; } diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CrossPartitionQueryTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CrossPartitionQueryTests.cs index 0f645a170f..f178eecfa9 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CrossPartitionQueryTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CrossPartitionQueryTests.cs @@ -152,8 +152,8 @@ private async Task CreateNonPartitionedContainer( Microsoft.Azure.Cosmos.IndexingPolicy indexingPolicy = null) { string containerName = Guid.NewGuid().ToString() + "container"; - await CosmosItemTests.CreateNonPartitionedContainer( - this.database.Id, + await NonPartitionedContainerHelper.CreateNonPartitionedContainer( + this.database, containerName, indexingPolicy == null ? null : JsonConvert.SerializeObject(indexingPolicy)); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Fluent/ContainerSettingsTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Fluent/ContainerSettingsTests.cs index 0d30f2420d..33063927f6 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Fluent/ContainerSettingsTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Fluent/ContainerSettingsTests.cs @@ -4,13 +4,14 @@ namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests { - using System; - using System.Threading.Tasks; using Microsoft.VisualStudio.TestTools.UnitTesting; - using Microsoft.Azure.Cosmos.Fluent; - using System.Net; - using System.Linq; using Newtonsoft.Json.Linq; + using System; + using System.Collections.ObjectModel; + using System.IO; + using System.Linq; + using System.Net; + using System.Threading.Tasks; // Similar tests to CosmosContainerTests but with Fluent syntax [TestClass] @@ -33,21 +34,192 @@ public async Task Cleanup() [TestMethod] public async Task ContainerContractTest() { - ContainerResponse response = - await this.database.DefineContainer(new Guid().ToString(), "/id") - .CreateAsync(); + ContainerProperties containerProperties = new ContainerProperties(Guid.NewGuid().ToString(), "/users") + { + IndexingPolicy = new IndexingPolicy() + { + Automatic = true, + IndexingMode = IndexingMode.Consistent, + IncludedPaths = new Collection() + { + new IncludedPath() + { + Path = "/*" + } + }, + ExcludedPaths = new Collection() + { + new ExcludedPath() + { + Path = "/test/*" + } + }, + CompositeIndexes = new Collection>() + { + new Collection() + { + new CompositePath() + { + Path = "/address/city", + Order = CompositePathSortOrder.Ascending + }, + new CompositePath() + { + Path = "/address/zipcode", + Order = CompositePathSortOrder.Descending + } + } + }, + SpatialIndexes = new Collection() + { + new SpatialPath() + { + Path = "/address/spatial/*", + SpatialTypes = new Collection() + { + SpatialType.LineString + } + } + } + } + }; + + var serializer = new CosmosJsonDotNetSerializer(); + Stream stream = serializer.ToStream(containerProperties); + ContainerProperties deserialziedTest = serializer.FromStream(stream); + + ContainerResponse response = await this.database.CreateContainerAsync(containerProperties); Assert.IsNotNull(response); Assert.IsTrue(response.RequestCharge > 0); Assert.IsNotNull(response.Headers); Assert.IsNotNull(response.Headers.ActivityId); - ContainerProperties containerSettings = response.Resource; - Assert.IsNotNull(containerSettings.Id); - Assert.IsNotNull(containerSettings.ResourceId); - Assert.IsNotNull(containerSettings.ETag); - Assert.IsTrue(containerSettings.LastModified.HasValue); + ContainerProperties responseProperties = response.Resource; + Assert.IsNotNull(responseProperties.Id); + Assert.IsNotNull(responseProperties.ResourceId); + Assert.IsNotNull(responseProperties.ETag); + Assert.IsTrue(responseProperties.LastModified.HasValue); + + Assert.IsTrue(responseProperties.LastModified.Value > new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), responseProperties.LastModified.Value.ToString()); + + Assert.AreEqual(1, responseProperties.IndexingPolicy.IncludedPaths.Count); + IncludedPath includedPath = responseProperties.IndexingPolicy.IncludedPaths.First(); + Assert.AreEqual("/*", includedPath.Path); + + Assert.AreEqual("/test/*", responseProperties.IndexingPolicy.ExcludedPaths.First().Path); + + Assert.AreEqual(1, responseProperties.IndexingPolicy.CompositeIndexes.Count); + Assert.AreEqual(2, responseProperties.IndexingPolicy.CompositeIndexes.First().Count); + CompositePath compositePath = responseProperties.IndexingPolicy.CompositeIndexes.First().First(); + Assert.AreEqual("/address/city", compositePath.Path); + Assert.AreEqual(CompositePathSortOrder.Ascending, compositePath.Order); + + Assert.AreEqual(1, responseProperties.IndexingPolicy.SpatialIndexes.Count); + SpatialPath spatialPath = responseProperties.IndexingPolicy.SpatialIndexes.First(); + Assert.AreEqual("/address/spatial/*", spatialPath.Path); + Assert.AreEqual(1, spatialPath.SpatialTypes.Count); + Assert.AreEqual(SpatialType.LineString, spatialPath.SpatialTypes.First()); + } + + [TestMethod] + public async Task ContainerNegativeSpatialIndexTest() + { + ContainerProperties containerProperties = new ContainerProperties(Guid.NewGuid().ToString(), "/users") + { + IndexingPolicy = new IndexingPolicy() + { + SpatialIndexes = new Collection() + { + new SpatialPath() + { + Path = "/address/spatial/*" + } + } + } + }; + + try + { + ContainerResponse response = await this.database.CreateContainerAsync(containerProperties); + Assert.Fail("Should require spatial type"); + } + catch (CosmosException ce) when (ce.StatusCode == HttpStatusCode.BadRequest) + { + Assert.IsTrue(ce.Message.Contains("The spatial data types array cannot be empty. Assign at least one spatial type for the 'types' array for the path")); + } + } - Assert.IsTrue(containerSettings.LastModified.Value > new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc), containerSettings.LastModified.Value.ToString()); + [TestMethod] + public async Task ContainerMigrationTest() + { + string containerName = "MigrationIndexTest"; + Documents.Index index1 = new Documents.RangeIndex(Documents.DataType.String, -1); + Documents.Index index2 = new Documents.RangeIndex(Documents.DataType.Number, -1); + Documents.DocumentCollection documentCollection = new Microsoft.Azure.Documents.DocumentCollection() + { + Id = containerName, + IndexingPolicy = new Documents.IndexingPolicy() + { + IncludedPaths = new Collection() + { + new Documents.IncludedPath() + { + Path = "/*", + Indexes = new Collection() + { + index1, + index2 + } + } + } + } + }; + + Documents.DocumentCollection createResponse = await NonPartitionedContainerHelper.CreateNonPartitionedContainer(this.database, documentCollection); + + // Verify the collection was created with deprecated Index objects + Assert.AreEqual(2, createResponse.IndexingPolicy.IncludedPaths.First().Indexes.Count); + Documents.Index createIndex = createResponse.IndexingPolicy.IncludedPaths.First().Indexes.First(); + Assert.AreEqual(index1.Kind, createIndex.Kind); + + // Verify v3 can add composite indexes and update the container + Container container = this.database.GetContainer(containerName); + ContainerProperties containerProperties = await container.ReadContainerAsync(); + string cPath0 = "/address/city"; + string cPath1 = "/address/state"; + containerProperties.IndexingPolicy.CompositeIndexes.Add(new Collection() + { + new CompositePath() + { + Path= cPath0, + Order = CompositePathSortOrder.Descending + }, + new CompositePath() + { + Path= cPath1, + Order = CompositePathSortOrder.Ascending + } + }); + + containerProperties.IndexingPolicy.SpatialIndexes.Add( + new SpatialPath() + { + Path = "/address/test/*", + SpatialTypes = new Collection() { SpatialType.Point } + }); + + ContainerProperties propertiesAfterReplace = await container.ReplaceContainerAsync(containerProperties); + Assert.AreEqual(0, propertiesAfterReplace.IndexingPolicy.IncludedPaths.First().Indexes.Count); + Assert.AreEqual(1, propertiesAfterReplace.IndexingPolicy.CompositeIndexes.Count); + Collection compositePaths = propertiesAfterReplace.IndexingPolicy.CompositeIndexes.First(); + Assert.AreEqual(2, compositePaths.Count); + CompositePath compositePath0 = compositePaths.ElementAt(0); + CompositePath compositePath1 = compositePaths.ElementAt(1); + Assert.IsTrue(string.Equals(cPath0, compositePath0.Path) || string.Equals(cPath1, compositePath0.Path)); + Assert.IsTrue(string.Equals(cPath0, compositePath1.Path) || string.Equals(cPath1, compositePath1.Path)); + + Assert.AreEqual(1, propertiesAfterReplace.IndexingPolicy.SpatialIndexes.Count); + Assert.AreEqual("/address/test/*", propertiesAfterReplace.IndexingPolicy.SpatialIndexes.First().Path); } [TestMethod] @@ -59,7 +231,7 @@ public async Task PartitionedCRUDTest() ContainerResponse containerResponse = await this.database.DefineContainer(containerName, partitionKeyPath) .WithIndexingPolicy() - .WithIndexingMode(Cosmos.IndexingMode.None) + .WithIndexingMode(IndexingMode.None) .WithAutomaticIndexing(false) .Attach() .CreateAsync(); @@ -68,14 +240,14 @@ await this.database.DefineContainer(containerName, partitionKeyPath) Assert.AreEqual(containerName, containerResponse.Resource.Id); Assert.AreEqual(partitionKeyPath, containerResponse.Resource.PartitionKey.Paths.First()); Container container = containerResponse; - Assert.AreEqual(Cosmos.IndexingMode.None, containerResponse.Resource.IndexingPolicy.IndexingMode); + Assert.AreEqual(IndexingMode.None, containerResponse.Resource.IndexingPolicy.IndexingMode); Assert.IsFalse(containerResponse.Resource.IndexingPolicy.Automatic); containerResponse = await container.ReadContainerAsync(); Assert.AreEqual(HttpStatusCode.OK, containerResponse.StatusCode); Assert.AreEqual(containerName, containerResponse.Resource.Id); Assert.AreEqual(partitionKeyPath, containerResponse.Resource.PartitionKey.Paths.First()); - Assert.AreEqual(Cosmos.IndexingMode.None, containerResponse.Resource.IndexingPolicy.IndexingMode); + Assert.AreEqual(IndexingMode.None, containerResponse.Resource.IndexingPolicy.IndexingMode); Assert.IsFalse(containerResponse.Resource.IndexingPolicy.Automatic); containerResponse = await containerResponse.Container.DeleteContainerAsync(); @@ -358,7 +530,7 @@ public async Task TimeToLivePropertyPath() ItemResponse createItemResponse = await container.CreateItemAsync(payload); Assert.IsNotNull(createItemResponse.Resource); Assert.AreEqual(createItemResponse.StatusCode, HttpStatusCode.Created); - ItemResponse readItemResponse = await container.ReadItemAsync(payload.id, new Cosmos.PartitionKey(payload.user)); + ItemResponse readItemResponse = await container.ReadItemAsync(payload.id, new PartitionKey(payload.user)); Assert.IsNotNull(readItemResponse.Resource); Assert.AreEqual(readItemResponse.StatusCode, HttpStatusCode.OK); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/NonPartitionedContainerHelper.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/NonPartitionedContainerHelper.cs new file mode 100644 index 0000000000..a13c088fd1 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/NonPartitionedContainerHelper.cs @@ -0,0 +1,175 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ +namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests +{ + using System; + using System.Net; + using System.Net.Http; + using System.Threading.Tasks; + using Microsoft.Azure.Documents; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Newtonsoft.Json; + + internal static class NonPartitionedContainerHelper + { + private static readonly string PreNonPartitionedMigrationApiVersion = "2018-08-31"; + private static readonly string utc_date = DateTime.UtcNow.ToString("r"); + + internal static async Task CreateNonPartitionedContainer( + Cosmos.Database database, + string containerId, + string indexingPolicy = null) + { + DocumentCollection documentCollection = new DocumentCollection() + { + Id = containerId + }; + + if (indexingPolicy != null) + { + documentCollection.IndexingPolicy = JsonConvert.DeserializeObject(indexingPolicy); + } + + await NonPartitionedContainerHelper.CreateNonPartitionedContainer( + database, + documentCollection); + + return (ContainerCore)database.GetContainer(containerId); + } + + internal static async Task CreateNonPartitionedContainer( + Cosmos.Database database, + DocumentCollection documentCollection) + { + (string endpoint, string authKey) accountInfo = TestCommon.GetAccountInfo(); + + //Creating non partition Container, rest api used instead of .NET SDK api as it is not supported anymore. + HttpClient client = new System.Net.Http.HttpClient(); + Uri baseUri = new Uri(accountInfo.endpoint); + string verb = "POST"; + string resourceType = "colls"; + string resourceId = string.Format("dbs/{0}", database.Id); + string resourceLink = string.Format("dbs/{0}/colls", database.Id); + client.DefaultRequestHeaders.Add("x-ms-date", utc_date); + client.DefaultRequestHeaders.Add("x-ms-version", NonPartitionedContainerHelper.PreNonPartitionedMigrationApiVersion); + + string authHeader = NonPartitionedContainerHelper.GenerateMasterKeyAuthorizationSignature( + verb, + resourceId, + resourceType, + accountInfo.authKey, + "master", + "1.0"); + + client.DefaultRequestHeaders.Add("authorization", authHeader); + string containerDefinition = documentCollection.ToString(); + StringContent containerContent = new StringContent(containerDefinition); + Uri requestUri = new Uri(baseUri, resourceLink); + + DocumentCollection responseCollection = null; + using (HttpResponseMessage response = await client.PostAsync(requestUri.ToString(), containerContent)) + { + response.EnsureSuccessStatusCode(); + Assert.AreEqual(HttpStatusCode.Created, response.StatusCode, response.ToString()); + responseCollection = await response.Content.ToResourceAsync(); + } + + return responseCollection; + } + + internal static async Task CreateUndefinedPartitionItem( + ContainerCore container, + string itemId) + { + (string endpoint, string authKey) accountInfo = TestCommon.GetAccountInfo(); + //Creating undefined partition key item, rest api used instead of .NET SDK api as it is not supported anymore. + HttpClient client = new System.Net.Http.HttpClient(); + Uri baseUri = new Uri(accountInfo.endpoint); + client.DefaultRequestHeaders.Add("x-ms-date", utc_date); + client.DefaultRequestHeaders.Add("x-ms-version", NonPartitionedContainerHelper.PreNonPartitionedMigrationApiVersion); + client.DefaultRequestHeaders.Add("x-ms-documentdb-partitionkey", "[{}]"); + + //Creating undefined partition Container item. + string verb = "POST"; + string resourceType = "docs"; + string resourceId = container.LinkUri.OriginalString; + string resourceLink = string.Format("dbs/{0}/colls/{1}/docs", container.Database.Id, container.Id); + string authHeader = NonPartitionedContainerHelper.GenerateMasterKeyAuthorizationSignature( + verb, + resourceId, + resourceType, + accountInfo.authKey, + "master", + "1.0"); + + client.DefaultRequestHeaders.Remove("authorization"); + client.DefaultRequestHeaders.Add("authorization", authHeader); + + var payload = new { id = itemId, user = itemId }; + string itemDefinition = JsonConvert.SerializeObject(payload); + StringContent itemContent = new StringContent(itemDefinition); + Uri requestUri = new Uri(baseUri, resourceLink); + await client.PostAsync(requestUri.ToString(), itemContent); + } + + internal static async Task CreateItemInNonPartitionedContainer( + ContainerCore container, + string itemId) + { + (string endpoint, string authKey) accountInfo = TestCommon.GetAccountInfo(); + //Creating non partition Container item. + HttpClient client = new System.Net.Http.HttpClient(); + Uri baseUri = new Uri(accountInfo.endpoint); + string verb = "POST"; + string resourceType = "docs"; + string resourceLink = string.Format("dbs/{0}/colls/{1}/docs", container.Database.Id, container.Id); + string authHeader = NonPartitionedContainerHelper.GenerateMasterKeyAuthorizationSignature( + verb, container.LinkUri.OriginalString, + resourceType, + accountInfo.authKey, + "master", + "1.0"); + + client.DefaultRequestHeaders.Add("x-ms-date", utc_date); + client.DefaultRequestHeaders.Add("x-ms-version", NonPartitionedContainerHelper.PreNonPartitionedMigrationApiVersion); + client.DefaultRequestHeaders.Add("authorization", authHeader); + + string itemDefinition = JsonConvert.SerializeObject(ToDoActivity.CreateRandomToDoActivity(id: itemId)); + { + StringContent itemContent = new StringContent(itemDefinition); + Uri requestUri = new Uri(baseUri, resourceLink); + HttpResponseMessage response = await client.PostAsync(requestUri.ToString(), itemContent); + Assert.AreEqual(HttpStatusCode.Created, response.StatusCode, response.ToString()); + } + } + + private static string GenerateMasterKeyAuthorizationSignature( + string verb, + string resourceId, + string resourceType, + string key, + string keyType, + string tokenVersion) + { + System.Security.Cryptography.HMACSHA256 hmacSha256 = new System.Security.Cryptography.HMACSHA256 { Key = Convert.FromBase64String(key) }; + + string payLoad = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}\n{1}\n{2}\n{3}\n{4}\n", + verb.ToLowerInvariant(), + resourceType.ToLowerInvariant(), + resourceId, + utc_date.ToLowerInvariant(), + "" + ); + + byte[] hashPayLoad = hmacSha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(payLoad)); + string signature = Convert.ToBase64String(hashPayLoad); + + return System.Web.HttpUtility.UrlEncode(string.Format(System.Globalization.CultureInfo.InvariantCulture, "type={0}&ver={1}&sig={2}", + keyType, + tokenVersion, + signature)); + } + + } +} diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/TestCommon.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/TestCommon.cs index f769770c34..54001402d6 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/TestCommon.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/TestCommon.cs @@ -51,12 +51,19 @@ static TestCommon() TestCommon.masterStalenessIntervalInSeconds = int.Parse(ConfigurationManager.AppSettings["MasterStalenessIntervalInSeconds"], CultureInfo.InvariantCulture); } - internal static CosmosClientBuilder GetDefaultConfiguration() + internal static (string endpoint, string authKey) GetAccountInfo() { string authKey = ConfigurationManager.AppSettings["MasterKey"]; string endpoint = ConfigurationManager.AppSettings["GatewayEndpoint"]; - return new CosmosClientBuilder(accountEndpoint: endpoint, accountKey: authKey); + return (endpoint, authKey); + } + + internal static CosmosClientBuilder GetDefaultConfiguration() + { + (string endpoint, string authKey) accountInfo = TestCommon.GetAccountInfo(); + + return new CosmosClientBuilder(accountEndpoint: accountInfo.endpoint, accountKey: accountInfo.authKey); } internal static CosmosClient CreateCosmosClient(Action customizeClientBuilder = null) diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/DotNetSDKAPI.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/DotNetSDKAPI.json index fc02eee502..6a04fb081c 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/DotNetSDKAPI.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/DotNetSDKAPI.json @@ -6104,9 +6104,11 @@ "Attributes": [], "MethodInfo": "System.Collections.ObjectModel.Collection`1[Microsoft.Azure.Cosmos.SpatialType] get_SpatialTypes()" }, - "System.Collections.ObjectModel.Collection`1[Microsoft.Azure.Cosmos.SpatialType] SpatialTypes": { + "System.Collections.ObjectModel.Collection`1[Microsoft.Azure.Cosmos.SpatialType] SpatialTypes[Newtonsoft.Json.JsonPropertyAttribute(ItemConverterType = typeof(Newtonsoft.Json.Converters.StringEnumConverter), PropertyName = \"types\")]": { "Type": "Property", - "Attributes": [], + "Attributes": [ + "JsonPropertyAttribute" + ], "MethodInfo": null }, "System.String get_Path()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/SettingsContractTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/SettingsContractTests.cs index ce85911c54..d984062f96 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/SettingsContractTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/SettingsContractTests.cs @@ -4,6 +4,12 @@ namespace Microsoft.Azure.Cosmos.Tests { + using Microsoft.Azure.Cosmos.Linq; + using Microsoft.Azure.Cosmos.Scripts; + using Microsoft.Azure.Documents; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; using System; using System.Collections.ObjectModel; using System.IO; @@ -11,10 +17,6 @@ namespace Microsoft.Azure.Cosmos.Tests using System.Reflection; using System.Text; using System.Threading.Tasks; - using Microsoft.Azure.Cosmos.Scripts; - using Microsoft.Azure.Documents; - using Microsoft.VisualStudio.TestTools.UnitTesting; - using Newtonsoft.Json; [TestClass] public class SettingsContractTests @@ -87,7 +89,7 @@ public void DatabaseStreamDeserialzieTest() + "\",\"_etag\":\"" + etag + "\",\"_colls\":\"colls\\/\",\"_users\":\"users\\/\",\"_ts\":" + ts + "}"; - DatabaseProperties deserializedPayload = + DatabaseProperties deserializedPayload = JsonConvert.DeserializeObject(testPyaload); Assert.IsTrue(deserializedPayload.LastModified.HasValue); @@ -348,11 +350,11 @@ public void ContainerSettingsDefaults() string id = Guid.NewGuid().ToString(); string pkPath = "/partitionKey"; - SettingsContractTests.TypeAccessorGuard(typeof(ContainerProperties), - "Id", - "UniqueKeyPolicy", - "DefaultTimeToLive", - "IndexingPolicy", + SettingsContractTests.TypeAccessorGuard(typeof(ContainerProperties), + "Id", + "UniqueKeyPolicy", + "DefaultTimeToLive", + "IndexingPolicy", "TimeToLivePropertyPath", "PartitionKeyPath", "PartitionKeyDefinitionVersion", @@ -387,6 +389,126 @@ public void ContainerSettingsDefaults() Assert.IsNotNull(uk.Paths); } + [TestMethod] + public async Task ContainerSettingsIndexTest() + { + string containerJsonString = "{\"indexingPolicy\":{\"automatic\":true,\"indexingMode\":\"Consistent\",\"includedPaths\":[{\"path\":\"/*\",\"indexes\":[{\"dataType\":\"Number\",\"precision\":-1,\"kind\":\"Range\"},{\"dataType\":\"String\",\"precision\":-1,\"kind\":\"Range\"}]}],\"excludedPaths\":[{\"path\":\"/\\\"_etag\\\"/?\"}],\"compositeIndexes\":[],\"spatialIndexes\":[]},\"id\":\"MigrationTest\",\"partitionKey\":{\"paths\":[\"/id\"],\"kind\":\"Hash\"}}"; + + CosmosJsonDotNetSerializer cosmosSerializer = new CosmosJsonDotNetSerializer(); + ContainerProperties containerProperties = null; + using (MemoryStream memory = new MemoryStream(Encoding.UTF8.GetBytes(containerJsonString))) + { + containerProperties = cosmosSerializer.FromStream(memory); + } + + Assert.IsNotNull(containerProperties); + Assert.AreEqual("MigrationTest", containerProperties.Id); + + string containerJsonAfterConversion = null; + using (Stream stream = cosmosSerializer.ToStream(containerProperties)) + { + using (StreamReader sr = new StreamReader(stream)) + { + containerJsonAfterConversion = await sr.ReadToEndAsync(); + } + } + + Assert.AreEqual(containerJsonString, containerJsonAfterConversion); + } + + [TestMethod] + public async Task ContainerV2CompatTest() + { + string containerId = "SerializeContainerTest"; + DocumentCollection documentCollection = new DocumentCollection() + { + Id = containerId, + PartitionKey = new PartitionKeyDefinition() + { + Paths = new Collection() + { + "/pkPath" + } + }, + IndexingPolicy = new IndexingPolicy() + { + IncludedPaths = new Collection() + { + new IncludedPath() + { + Path = "/*" + } + }, + CompositeIndexes = new Collection>() + { + new Collection() + { + new CompositePath() + { + Path = "/address/test/*", + Order = CompositePathSortOrder.Ascending + }, + new CompositePath() + { + Path = "/address/test2/*", + Order = CompositePathSortOrder.Ascending + } + } + }, + SpatialIndexes = new Collection() + { + new SpatialSpec() + { + Path = "/name/first/*", + SpatialTypes = new Collection() + { + SpatialType.LineString + } + } + } + }, + }; + + + string documentJsonString = null; + using (MemoryStream memoryStream = new MemoryStream()) + { + documentCollection.SaveTo(memoryStream); + memoryStream.Position = 0; + using (StreamReader sr = new StreamReader(memoryStream)) + { + documentJsonString = await sr.ReadToEndAsync(); + } + } + + Assert.IsNotNull(documentJsonString); + + string cosmosJsonString = null; + using (MemoryStream memoryStream = new MemoryStream()) + { + documentCollection.SaveTo(memoryStream); + memoryStream.Position = 0; + + CosmosJsonDotNetSerializer cosmosSerializer = new CosmosJsonDotNetSerializer(); + ContainerProperties containerProperties = cosmosSerializer.FromStream(memoryStream); + + Assert.IsNotNull(containerProperties); + Assert.AreEqual(containerId, containerProperties.Id); + + using (Stream stream = cosmosSerializer.ToStream(containerProperties)) + { + using (StreamReader sr = new StreamReader(stream)) + { + cosmosJsonString = await sr.ReadToEndAsync(); + } + } + } + + JObject jObjectDocumentCollection = JObject.Parse(documentJsonString); + JObject jObjectContainer = JObject.Parse(cosmosJsonString); + Assert.IsTrue(JToken.DeepEquals(jObjectDocumentCollection, jObjectContainer)); + } + [TestMethod] public void CosmosAccountSettingsSerializationTest() { @@ -395,7 +517,7 @@ public void CosmosAccountSettingsSerializationTest() cosmosAccountSettings.EnableMultipleWriteLocations = true; cosmosAccountSettings.ResourceId = "/uri"; cosmosAccountSettings.ETag = "etag"; - cosmosAccountSettings.WriteLocationsInternal = new Collection() { new AccountRegion() { Name="region1", Endpoint = "endpoint1" } }; + cosmosAccountSettings.WriteLocationsInternal = new Collection() { new AccountRegion() { Name = "region1", Endpoint = "endpoint1" } }; cosmosAccountSettings.ReadLocationsInternal = new Collection() { new AccountRegion() { Name = "region2", Endpoint = "endpoint2" } }; cosmosAccountSettings.AddressesLink = "link"; cosmosAccountSettings.Consistency = new AccountConsistency() { DefaultConsistencyLevel = Cosmos.ConsistencyLevel.BoundedStaleness }; @@ -486,7 +608,7 @@ private static T CosmosDeserialize(string payload) } } - private static T DirectDeSerialize(string payload) where T: JsonSerializable, new() + private static T DirectDeSerialize(string payload) where T : JsonSerializable, new() { using (MemoryStream ms = new MemoryStream()) { @@ -510,7 +632,7 @@ private static string CosmosSerialize(object input) } } - private static string DirectSerialize(T input) where T: JsonSerializable + private static string DirectSerialize(T input) where T : JsonSerializable { using (MemoryStream ms = new MemoryStream()) { @@ -527,7 +649,7 @@ private static string DirectSerialize(T input) where T: JsonSerializable private static void TypeAccessorGuard(Type input, params string[] publicSettable) { // All properties are public readable only by-default - PropertyInfo[] allProperties = input.GetProperties(BindingFlags.Instance|BindingFlags.Public); + PropertyInfo[] allProperties = input.GetProperties(BindingFlags.Instance | BindingFlags.Public); foreach (PropertyInfo pInfo in allProperties) { MethodInfo[] accessors = pInfo.GetAccessors(); @@ -550,7 +672,7 @@ private static void TypeAccessorGuard(Type input, params string[] publicSettable } } - private void AssertEnums() where TFirstEnum : struct, IConvertible where TSecondEnum : struct, IConvertible + private void AssertEnums() where TFirstEnum : struct, IConvertible where TSecondEnum : struct, IConvertible { string[] allCosmosEntries = Enum.GetNames(typeof(TFirstEnum)); string[] allDocumentsEntries = Enum.GetNames(typeof(TSecondEnum)); diff --git a/changelog.md b/changelog.md index b0ae1e71e9..c6451dd26a 100644 --- a/changelog.md +++ b/changelog.md @@ -6,7 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Fixed -- [#612] (https://github.com/Azure/azure-cosmos-dotnet-v3/pull/612) Bug fix for ReadFeed with partition-key + +- [#612](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/612) Bug fix for ReadFeed with partition-key +- [#614](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/614) Fixed SpatialPath serialization and compatibility with older index versions ## [3.1.0](https://www.nuget.org/packages/Microsoft.Azure.Cosmos/3.1.0) - 2019-07-26 @@ -17,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#557](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/557) Added trigger options to item request options - [#571](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/571) Added a default JSON.net serializer with optional settings - [#572](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/572) Added partition key validation on CreateContainerIfNotExistsAsync -- [#581](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/581) Adding LINQ to QueryDefinition API +- [#581](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/581) Added LINQ to QueryDefinition API - [#592](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/592) Added CreateIfNotExistsAsync to container builder - [#597](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/597) Added continuation token property to ResponseMessage - [#604](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/604) Added LINQ ToStreamIterator extension method