diff --git a/CHANGELOG.md b/CHANGELOG.md index a62c646e..714342e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added 1. `updateViews` parameter of `DefaultDataObjectEdmModelBuilder` class. It allows to change update views for data objects (update view is used for loading a data object during OData update requests). +2. `masterLightLoadTypes` and `masterLightLoadAllTypes` parameters of `DefaultDataObjectEdmModelBuilder` class. They allow to change loading mode of masters during OData update requests. ### Changed 1. Updated Flexberry ORM up to 7.2.0-beta01. diff --git a/NewPlatform.Flexberry.ORM.ODataService/Controllers/DataObjectController.ModifyData.cs b/NewPlatform.Flexberry.ORM.ODataService/Controllers/DataObjectController.ModifyData.cs index e5b682bd..1fd72656 100644 --- a/NewPlatform.Flexberry.ORM.ODataService/Controllers/DataObjectController.ModifyData.cs +++ b/NewPlatform.Flexberry.ORM.ODataService/Controllers/DataObjectController.ModifyData.cs @@ -1,4 +1,4 @@ -namespace NewPlatform.Flexberry.ORM.ODataService.Controllers +namespace NewPlatform.Flexberry.ORM.ODataService.Controllers { using System; using System.Collections.Generic; @@ -23,13 +23,16 @@ #if NETFRAMEWORK using System.Net.Http.Formatting; + using System.Web; using System.Web.Http; using System.Web.Http.Results; using System.Web.Http.Validation; using NewPlatform.Flexberry.ORM.ODataService.Events; using NewPlatform.Flexberry.ORM.ODataService.Handlers; + using Newtonsoft.Json.Linq; #endif #if NETSTANDARD + using System.Data; using Microsoft.AspNet.OData.Formatter; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -676,7 +679,7 @@ private DataObject UpdateObject(EdmEntityObject edmEntity, object key) { // Создадим объект данных по пришедшей сущности. // В переменной objs сформируем список всех объектов для обновления в нужном порядке: сам объект и зависимые всех уровней. - DataObject obj = GetDataObjectByEdmEntity(edmEntity, key, objs, useUpdateView: true); + DataObject obj = GetDataObjectByEdmEntity(edmEntity, key, objs); for (int i = 0; i < objs.Count; i++) { @@ -739,76 +742,113 @@ private DataObject UpdateObject(EdmEntityObject edmEntity, object key) } /// - /// Получить объект данных по ключу: если объект есть в хранилище, то возвращается загруженным по представлению , иначе - создаётся новый. + /// Загрузить существующий объект данных в облегчённом варианте (только __PrimaryKey), используя информацию из EdmEntity. /// - /// Тип объекта, не может быть null. - /// Значение ключа. - /// Представление для загрузки объекта. + /// EdmEntity, который будет использован для получения объекта данных. /// Объект данных. - private DataObject ReturnDataObject(Type objType, object keyValue, View view) + private DataObject LightLoadDataObject(EdmEntityObject edmEntity) { - if (objType == null) + if (edmEntity == null) { - throw new ArgumentNullException(nameof(objType)); + throw new ArgumentNullException(nameof(edmEntity)); } - if (keyValue != null) - { - DataObject dataObjectFromCache = DataObjectCache.GetLivingDataObject(objType, keyValue); + Type masterType = _model.GetDataObjectType(edmEntity); + object masterKey = GetKey(edmEntity); + View view = new View(new ViewAttribute("dynView", new string[] { Information.ExtractPropertyPath(x => x.__PrimaryKey) }), masterType); + + return LoadDataObject(masterType, masterKey, view); + } - if (dataObjectFromCache != null) + /// + /// Загрузить существующий объект данных. + /// + /// Тип загружаемого объекта. + /// Первичный ключ загружаемого объекта. + /// Представление, по которому будет загружен объект. + /// Объект данных. + private DataObject LoadDataObject(Type objType, object keyValue, View view) + { + DataObject dataObjectFromCache = DataObjectCache.GetLivingDataObject(objType, keyValue); + + if (dataObjectFromCache != null) + { + // Если объект не новый и не загружен целиком (начиная с ORM@5.1.0-beta15). + if (dataObjectFromCache.GetStatus(false) == ObjectStatus.UnAltered + && dataObjectFromCache.GetLoadingState() != LoadingState.Loaded) { - // Если объект не новый и не загружен целиком (начиная с ORM@5.1.0-beta15). - if (dataObjectFromCache.GetStatus(false) == ObjectStatus.UnAltered - && dataObjectFromCache.GetLoadingState() != LoadingState.Loaded) + // Для обратной совместимости сравним перечень загруженных свойств и свойств в представлении. + /* Данный код срабатывает, например, если в кэше был объект, который загрузился только на уровне первичного ключа. + * + * Данный код также срабатывает в следующей ситуации: есть класс А, у него детейл Б, у которого есть наследник В. + * При загрузке объекта класса А подгрузятся его детейлы, однако они будут подгружены по представлению, которое соответствует классу Б, даже если детейлы класса В. + * Таким образом, в кэше окажутся объекты класса В, которые загружены только по свойствам Б. Раз не все свойства подгружены, то состояние LightLoaded. + * Догружать необходимо только те свойства, что ещё не загружались (потому что загруженные уже могут быть изменены). + */ + string[] loadedProps = dataObjectFromCache.GetLoadedProperties(); + IEnumerable ownProps = view.Properties.Where(p => !p.Name.Contains('.')); + if (!ownProps.All(p => loadedProps.Contains(p.Name))) { - // Для обратной совместимости сравним перечень загруженных свойств и свойств в представлении. - /* Данный код срабатывает, например, если в кэше был объект, который загрузился только на уровне первичного ключа. - * - * Данный код также срабатывает в следующей ситуации: есть класс А, у него детейл Б, у которого есть наследник В. - * При загрузке объекта класса А подгрузятся его детейлы, однако они будут подгружены по представлению, которое соответствует классу Б, даже если детейлы класса В. - * Таким образом, в кэше окажутся объекты класса В, которые загружены только по свойствам Б. Раз не все свойства подгружены, то состояние LightLoaded. - * Догружать необходимо только те свойства, что ещё не загружались (потому что загруженные уже могут быть изменены). - */ - string[] loadedProps = dataObjectFromCache.GetLoadedProperties(); - IEnumerable ownProps = view.Properties.Where(p => !p.Name.Contains('.')); - if (!ownProps.All(p => loadedProps.Contains(p.Name))) - { - // Вычитывать объект сразу с детейлами нельзя, поскольку в этой же транзакции могут уже оказать отдельные операции с детейлами и перевычитка затрёт эти изменения. - View miniView = view.Clone(); - DetailInView[] miniViewDetails = miniView.Details; - miniView.Details = new DetailInView[0]; - _dataService.SafeLoadWithMasters(miniView, dataObjectFromCache, DataObjectCache); + // Вычитывать объект сразу с детейлами нельзя, поскольку в этой же транзакции могут уже оказаться отдельные операции с детейлами и перевычитка затрёт эти изменения. + View miniView = view.Clone(); + DetailInView[] miniViewDetails = miniView.Details; + miniView.Details = new DetailInView[0]; + _dataService.SafeLoadWithMasters(miniView, dataObjectFromCache, DataObjectCache); - if (miniViewDetails.Length > 0) - { - _dataService.SafeLoadDetails(view, new DataObject[] { dataObjectFromCache }, DataObjectCache); - } + if (miniViewDetails.Length > 0) + { + _dataService.SafeLoadDetails(view, new DataObject[] { dataObjectFromCache }, DataObjectCache); } } - - return dataObjectFromCache; } - // Вычитывать объект сразу с детейлами нельзя, поскольку в этой же транзакции могут уже оказать отдельные операции с детейлами и перевычитка затрёт эти изменения. - View lightView = view.Clone(); - DetailInView[] lightViewDetails = lightView.Details; - lightView.Details = new DetailInView[0]; - - // Проверим существование объекта в базе. - LoadingCustomizationStruct lcs = LoadingCustomizationStruct.GetSimpleStruct(objType, lightView); - lcs.LimitFunction = FunctionBuilder.BuildEquals(keyValue); - lcs.ReturnTop = 2; - DataObject[] dobjs = _dataService.LoadObjects(lcs, DataObjectCache); - if (dobjs.Length == 1) + return dataObjectFromCache; + } + + // Вычитывать объект сразу с детейлами нельзя, поскольку в этой же транзакции могут уже оказаться отдельные операции с детейлами и перевычитка затрёт эти изменения. + View lightView = view.Clone(); + DetailInView[] lightViewDetails = lightView.Details; + lightView.Details = new DetailInView[0]; + + // Проверим существование объекта в базе. + LoadingCustomizationStruct lcs = LoadingCustomizationStruct.GetSimpleStruct(objType, lightView); + lcs.LimitFunction = FunctionBuilder.BuildEquals(keyValue); + lcs.ReturnTop = 2; + DataObject[] dobjs = _dataService.LoadObjects(lcs, DataObjectCache); + if (dobjs.Length == 1) + { + DataObject dataObject = dobjs[0]; + if (lightViewDetails.Any()) { - DataObject dataObject = dobjs[0]; - if (lightViewDetails.Any()) - { - // Дочитаем детейлы, чтобы в бизнес-серверах эти данные уже были. Детейлы с изменёнными состояниями будут пропущены из зачитки. - _dataService.SafeLoadDetails(view, new DataObject[] { dataObject }, DataObjectCache); - } + // Дочитаем детейлы, чтобы в бизнес-серверах эти данные уже были. Детейлы с изменёнными состояниями будут пропущены из зачитки. + _dataService.SafeLoadDetails(view, new DataObject[] { dataObject }, DataObjectCache); + } + + return dataObject; + } + + return null; + } + /// + /// Получить объект данных по ключу: если объект есть в хранилище, то возвращается загруженным по представлению по умолчанию, иначе - создаётся новый. + /// + /// Тип объекта, не может быть null. + /// Значение ключа. + /// Объект данных. + private DataObject ReturnDataObject(Type objType, object keyValue) + { + if (objType == null) + { + throw new ArgumentNullException(nameof(objType)); + } + + if (keyValue != null) + { + View view = _model.GetDataObjectUpdateView(objType) ?? _model.GetDataObjectDefaultView(objType); + DataObject dataObject = LoadDataObject(objType, keyValue, view); + if (dataObject != null) + { return dataObject; } } @@ -848,8 +888,31 @@ private static void AddObjectToUpdate(List objsToUpdate, DataObject objsToUpdate.Insert(0, dataObject); // Добавляем объект в начало списка. } - } } + } + + /// + /// Получить значение ключа у указанной сущности. + /// + /// Сущность. + /// Значение ключа. + private object GetKey(EdmEntityObject edmEntity) + { + if (edmEntity == null) + { + throw new ArgumentNullException(nameof(edmEntity), $"{nameof(edmEntity)} can not be null."); + } + + object key; + + // Получим значение ключа. + IEdmEntityType entityType = (IEdmEntityType)edmEntity.ActualEdmType; + IEnumerable entityProps = entityType.Properties(); + var keyProperty = entityProps.FirstOrDefault(prop => prop.Name == _model.KeyPropertyName); + edmEntity.TryGetPropertyValue(keyProperty.Name, out key); + + return key; + } /// /// Построение объекта данных по сущности OData. @@ -858,45 +921,21 @@ private static void AddObjectToUpdate(List objsToUpdate, DataObject /// Значение ключевого поля сущности. /// Список объектов для обновления. /// Признак, что объект добавляется в конец списка обновления. - /// Использовать представление для обновления (вместо представления по умолчанию). /// Объект данных. - private DataObject GetDataObjectByEdmEntity(EdmEntityObject edmEntity, object key, List dObjs, bool endObject = false, bool useUpdateView = false) + private DataObject GetDataObjectByEdmEntity(EdmEntityObject edmEntity, object key, List dObjs, bool endObject = false) { if (edmEntity == null) { return null; } - // Значение свойства. - object value; - - // Получим значение ключа. - IEdmEntityType entityType = (IEdmEntityType)edmEntity.ActualEdmType; - IEnumerable entityProps = entityType.Properties().ToList(); - var keyProperty = entityProps.FirstOrDefault(prop => prop.Name == _model.KeyPropertyName); - if (key != null) - { - value = key; - } - else - { - edmEntity.TryGetPropertyValue(keyProperty.Name, out value); - } + key = key ?? GetKey(edmEntity); // Загрузим объект из хранилища, если он там есть, или создадим, если нет, но только для POST. // Тем самым гарантируем загруженность свойств при необходимости обновления и установку нужного статуса. Type objType = _model.GetDataObjectType(edmEntity); - View view = null; - if (useUpdateView) - { - view = _model.GetDataObjectUpdateView(objType) ?? _model.GetDataObjectDefaultView(objType); - } else - { - view = _model.GetDataObjectDefaultView(objType); - } - - DataObject obj = ReturnDataObject(objType, value, view); + DataObject obj = ReturnDataObject(objType, key); // Добавляем объект в список для обновления, если там ещё нет объекта с таким ключом. AddObjectToUpdate(dObjs, obj, endObject); @@ -906,6 +945,8 @@ private DataObject GetDataObjectByEdmEntity(EdmEntityObject edmEntity, object ke IEnumerable changedPropNames = edmEntity.GetChangedPropertyNames(); // Обрабатываем агрегатор первым. + IEdmEntityType entityType = (IEdmEntityType)edmEntity.ActualEdmType; + IEnumerable entityProps = entityType.Properties().ToList(); List changedProps = entityProps .Where(ep => changedPropNames.Contains(ep.Name)) .OrderBy(ep => ep.Name != agregatorPropertyName) @@ -931,22 +972,38 @@ private DataObject GetDataObjectByEdmEntity(EdmEntityObject edmEntity, object ke // Обработка мастеров и детейлов. if (prop is EdmNavigationProperty navProp) { - edmEntity.TryGetPropertyValue(prop.Name, out value); - EdmMultiplicity edmMultiplicity = navProp.TargetMultiplicity(); // Обработка мастеров. if (edmMultiplicity == EdmMultiplicity.One || edmMultiplicity == EdmMultiplicity.ZeroOrOne) { + object value; + edmEntity.TryGetPropertyValue(prop.Name, out value); + if (value is EdmEntityObject edmMaster) { // Порядок вставки влияет на порядок отправки объектов в UpdateObjects это в свою очередь влияет на то, как срабатывают бизнес-серверы. Бизнес-сервер мастера должен сработать после, а агрегатора перед этим объектом. bool insertIntoEnd = string.IsNullOrEmpty(agregatorPropertyName); - DataObject master = GetDataObjectByEdmEntity(edmMaster, null, dObjs, insertIntoEnd, useUpdateView); + bool masterOwnPropsUpdated = edmMaster.GetChangedPropertyNames().Any(propName => propName != _model.KeyPropertyName); + bool isAggregator = dataObjectPropName == agregatorPropertyName; + DataObject master = null; + + Type objectType = _model.GetDataObjectType(edmEntity); + bool masterLightLoad = _model.IsMasterLightLoad(objectType); + + if (masterLightLoad && !masterOwnPropsUpdated && !isAggregator) + { + master = LightLoadDataObject(edmMaster); + //AddObjectToUpdate(dObjs, master, insertIntoEnd); + } + else + { + master = GetDataObjectByEdmEntity(edmMaster, null, dObjs, insertIntoEnd); + } Information.SetPropValueByName(obj, dataObjectPropName, master); - if (dataObjectPropName == agregatorPropertyName) + if (isAggregator) { master.AddDetail(obj); @@ -969,6 +1026,9 @@ private DataObject GetDataObjectByEdmEntity(EdmEntityObject edmEntity, object ke { DetailArray detarr = (DetailArray)Information.GetPropValueByName(obj, dataObjectPropName); + object value; + edmEntity.TryGetPropertyValue(prop.Name, out value); + if (value is EdmEntityObjectCollection coll) { if (coll != null && coll.Count > 0) @@ -979,8 +1039,7 @@ private DataObject GetDataObjectByEdmEntity(EdmEntityObject edmEntity, object ke (EdmEntityObject)edmEnt, null, dObjs, - true, - useUpdateView); + true); if (det.__PrimaryKey == null) { @@ -1002,11 +1061,13 @@ private DataObject GetDataObjectByEdmEntity(EdmEntityObject edmEntity, object ke else { // Обработка собственных свойств объекта (неключевых, т.к. ключ устанавливаем при начальной инициализации объекта obj). - if (prop.Name != keyProperty.Name) + if (prop.Name != _model.KeyPropertyName) { - Type dataObjectPropertyType = Information.GetPropertyType(objType, dataObjectPropName); + object value; edmEntity.TryGetPropertyValue(prop.Name, out value); + Type dataObjectPropertyType = Information.GetPropertyType(objType, dataObjectPropName); + // Если тип свойства относится к одному из зарегистрированных провайдеров файловых свойств, // значит свойство файловое, и его нужно обработать особым образом. if (_dataObjectFileAccessor.HasDataObjectFileProvider(dataObjectPropertyType)) diff --git a/NewPlatform.Flexberry.ORM.ODataService/Model/DataObjectEdmModel.cs b/NewPlatform.Flexberry.ORM.ODataService/Model/DataObjectEdmModel.cs index dcfae14f..95586d2a 100644 --- a/NewPlatform.Flexberry.ORM.ODataService/Model/DataObjectEdmModel.cs +++ b/NewPlatform.Flexberry.ORM.ODataService/Model/DataObjectEdmModel.cs @@ -528,6 +528,13 @@ public View GetDataObjectUpdateView(Type dataObjectType) return _metadata[dataObjectType].UpdateView?.Clone(); } + /// + /// Возвращает информацию, должны ли мастера объекта загружаться в экономном режиме (только __PrimaryKey). + /// + /// Тип объекта данных. + /// Мастера должны загружаться экономно. + public bool IsMasterLightLoad(Type dataObjectType) => _metadata == null ? false : _metadata[dataObjectType]?.MasterLightLoad ?? false; + /// /// Получает список зарегистрированных в модели типов по списку имён типов. /// diff --git a/NewPlatform.Flexberry.ORM.ODataService/Model/DataObjectEdmTypeSettings.cs b/NewPlatform.Flexberry.ORM.ODataService/Model/DataObjectEdmTypeSettings.cs index 4e4219a1..e5c03947 100644 --- a/NewPlatform.Flexberry.ORM.ODataService/Model/DataObjectEdmTypeSettings.cs +++ b/NewPlatform.Flexberry.ORM.ODataService/Model/DataObjectEdmTypeSettings.cs @@ -36,6 +36,11 @@ public sealed class DataObjectEdmTypeSettings /// public View UpdateView { get; set; } + /// + /// Whether to load object masters in LightLoaded state (load only primary key). + /// + public bool MasterLightLoad { get; set; } + /// /// The list of exposed details. /// @@ -56,4 +61,4 @@ public sealed class DataObjectEdmTypeSettings /// public IDictionary PseudoDetailProperties { get; } = new Dictionary(); } -} \ No newline at end of file +} diff --git a/NewPlatform.Flexberry.ORM.ODataService/Model/DefaultDataObjectEdmModelBuilder.cs b/NewPlatform.Flexberry.ORM.ODataService/Model/DefaultDataObjectEdmModelBuilder.cs index 3e6ed225..fbe317d4 100644 --- a/NewPlatform.Flexberry.ORM.ODataService/Model/DefaultDataObjectEdmModelBuilder.cs +++ b/NewPlatform.Flexberry.ORM.ODataService/Model/DefaultDataObjectEdmModelBuilder.cs @@ -74,6 +74,16 @@ public class DefaultDataObjectEdmModelBuilder : IDataObjectEdmModelBuilder /// private Dictionary UpdateViews { get; set; } + /// + /// Types for which masters should be light-loaded on updates (load only __PrimaryKey). + /// + private IEnumerable MasterLightLoadTypes { get; set; } + + /// + /// Whether to load all masters in light-loaded mode on updates (load only __PrimaryKey). + /// + private bool MasterLightLoadAllTypes { get; set; } + private readonly PropertyInfo _keyProperty = Information.ExtractPropertyInfo(n => n.__PrimaryKey); /// @@ -88,7 +98,9 @@ public DefaultDataObjectEdmModelBuilder( bool useNamespaceInEntitySetName = true, PseudoDetailDefinitions pseudoDetailDefinitions = null, Dictionary additionalMapping = null, - IEnumerable> updateViews = null) + IEnumerable> updateViews = null, + IEnumerable masterLightLoadTypes = null, + bool masterLightLoadAllTypes = false) { _searchAssemblies = searchAssemblies ?? throw new ArgumentNullException(nameof(searchAssemblies), "Contract assertion not met: searchAssemblies != null"); _useNamespaceInEntitySetName = useNamespaceInEntitySetName; @@ -105,6 +117,18 @@ public DefaultDataObjectEdmModelBuilder( { SetUpdateView(updateViews); } + + if (masterLightLoadTypes != null) + { + SetMasterLightLoadTypes(masterLightLoadTypes); + + if (masterLightLoadAllTypes) + { + throw new ArgumentException("Parameters masterLightLoadAllTypes and masterLightLoadTypes can not be used together in DefaultDataObjectEdmModelBuilder."); + } + } + + this.MasterLightLoadAllTypes = masterLightLoadAllTypes; } /// @@ -215,7 +239,12 @@ private void SetUpdateView(Type dataObjectType, View updateView) { if (!dataObjectType.IsSubclassOf(typeof(DataObject))) { - throw new ArgumentException("Update view can be set only for a DataObject.", nameof(dataObjectType)); + throw new ArgumentException($"Update view can be set only for a DataObject. Current type is {dataObjectType}", nameof(dataObjectType)); + } + + if (dataObjectType is null) + { + throw new ArgumentException("dataObjectType can not be null.", nameof(dataObjectType)); } if (updateView is null) @@ -232,6 +261,26 @@ private void SetUpdateView(Type dataObjectType, View updateView) UpdateViews[dataObjectType] = updateView; } + /// + /// Sets DataObject types for which masters will be light-loaded on updates (load only __PrimaryKey). + /// + /// Types for which masters should be light-loaded. + private void SetMasterLightLoadTypes(IEnumerable masterLightLoadTypes) + { + if (masterLightLoadTypes != null) + { + foreach (Type type in masterLightLoadTypes) + { + if (!type.IsSubclassOf(typeof(DataObject))) + { + throw new ArgumentException("MasterLightLoad option can be set only for a DataObject.", nameof(masterLightLoadTypes)); + } + } + + MasterLightLoadTypes = masterLightLoadTypes; + } + } + /// /// Adds the property for exposing. /// @@ -310,6 +359,7 @@ private void AddDataObjectWithHierarchy(DataObjectEdmMetadata meta, Type dataObj CollectionName = EntitySetNameBuilder(dataObjectType), DefaultView = DynamicView.Create(dataObjectType, null).View, UpdateView = updateView, + MasterLightLoad = MasterLightLoadAllTypes || (MasterLightLoadTypes?.Contains(dataObjectType) ?? false), }; AddProperties(dataObjectType, typeSettings); @@ -458,4 +508,4 @@ private string BuildEntityPropertyName(PropertyInfo propertyDataObject) return propertyDataObject.Name; } } -} \ No newline at end of file +} diff --git a/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/CRUD/Update/MasterLightLoadTest.cs b/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/CRUD/Update/MasterLightLoadTest.cs new file mode 100644 index 00000000..01f70568 --- /dev/null +++ b/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/CRUD/Update/MasterLightLoadTest.cs @@ -0,0 +1,233 @@ +#if NETCOREAPP +namespace NewPlatform.Flexberry.ORM.ODataService.Tests.CRUD.Update +{ + using System; + using System.Data; + using System.Linq; + using System.Net; + using System.Net.Http; + using ICSSoft.STORMNET; + using ICSSoft.STORMNET.Business.LINQProvider; + using ICSSoft.STORMNET.KeyGen; + using NewPlatform.Flexberry.ORM.ODataService.Batch; + using NewPlatform.Flexberry.ORM.ODataService.Tests.Extensions; + using NewPlatform.Flexberry.ORM.ODataService.Tests.Helpers; + + using Xunit; + using Xunit.Abstractions; + + /// + /// Тесты для проверки работы MasterLightLoad. Для запуска OData backend используется модифицированная версия Startup - , + /// которая задаёт флаг экономной загрузки мастеров для . + /// + public class MasterLightLoadTest : BaseODataServiceIntegratedTest + { + /// + /// Конструктор по-умолчанию. + /// + /// Фабрика для приложения. + /// Вывод диагностической информации по тестам. + public MasterLightLoadTest(CustomWebApplicationFactory factory, ITestOutputHelper output) + : base(factory, output) + { + } + + /// + /// Проверка экономной загрузки мастера при активной настройке MasterLightLoad при смене мастера. + /// + [Fact] + public void MasterChangedTest() + { + ActODataService(args => + { + // Создаем объекты данных, которые потом будем обновлять, и добавляем в базу обычным сервисом данных. + Порода порода = new Порода { Название = "Сиамская" }; + Кошка кошка1 = new Кошка { Кличка = "Болтушка", Агрессивная = true, Порода = порода }; + Кошка кошка2 = new Кошка { Кличка = "Петрушка", Агрессивная = false, Порода = порода }; + args.DataService.UpdateObject(порода); + args.DataService.UpdateObject(кошка1); + args.DataService.UpdateObject(кошка2); + + Котенок котенок = new Котенок { Кошка = кошка1, КличкаКотенка = "Котенок Гав", Глупость = 10 }; + args.DataService.UpdateObject(котенок); + + // Обновляем ссылку на мастера + котенок.Кошка = кошка2; + + // Представление, по которому будем обновлять. + string[] котенокPropertiesNames = + { + Information.ExtractPropertyPath<Котенок>(x => x.__PrimaryKey), + }; + var котенокDynamicView = new View(new ViewAttribute("котенокDynamicView", котенокPropertiesNames), typeof(Котенок)); + + // Преобразуем объект в JSON-строку. + string requestJsonData = котенок.ToJson(котенокDynamicView, args.Token.Model); + + // Добавляем в payload информацию о том, что поменяли ссылку на мастера + requestJsonData = ODataTestHelper.AddEntryRelationship(requestJsonData, котенокDynamicView, args.Token.Model, кошка2, nameof(Котенок.Кошка)); + + // Формируем URL запроса к OData-сервису (с идентификатором изменяемой сущности). + var requestUrl = ODataTestHelper.GetRequestUrl(args.Token.Model, котенок); + + using (HttpResponseMessage response = args.HttpClient.PatchAsJsonStringAsync(requestUrl, requestJsonData).Result) + { + // Если приходит код 200, значит, настройка не ломает загрузку. + // Фактическую проверку того, что кошка загрузилась в LightLoaded надо делать через отладчик. + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + // TODO: проверка на экономную загрузку мастера. + } + }); + } + + /// + /// Проверка экономной загрузки мастера при активной настройке MasterLightLoad при смене значения поля у мастера. + /// + [Fact] + public void MasterPropsChangedBatchTest() + { + ActODataService(async args => + { + // Создаем объекты данных, которые потом будем обновлять, и добавляем в базу обычным сервисом данных. + Порода порода = new Порода { Название = "Сиамская" }; + Кошка кошка = new Кошка { Кличка = "Болтушка", Агрессивная = true, Порода = порода }; + args.DataService.UpdateObject(порода); + args.DataService.UpdateObject(кошка); + + Котенок котенок = new Котенок { Кошка = кошка, КличкаКотенка = "Котенок Гав", Глупость = 10 }; + args.DataService.UpdateObject(котенок); + + // Обновляем атрибут объекта + котенок.Глупость = 1; + + // Обновляем атрибут мастера + котенок.Кошка.Кличка = "Петрушка"; + + // Представление, по которому будем обновлять объект + string[] котенокPropertiesNames = + { + Information.ExtractPropertyPath<Котенок>(x => x.__PrimaryKey), + Information.ExtractPropertyPath<Котенок>(x => x.Глупость), + }; + var котенокDynamicView = new View(new ViewAttribute("котенокDynamicView", котенокPropertiesNames), typeof(Котенок)); + + // Представление, по которому будем обновлять мастер + string[] кошкаPropertiesNames = + { + Information.ExtractPropertyPath<Кошка>(x => x.__PrimaryKey), + Information.ExtractPropertyPath<Кошка>(x => x.Кличка), + }; + var кошкаDynamicView = new View(new ViewAttribute("кошкаDynamicView", кошкаPropertiesNames), typeof(Кошка)); + + // Преобразуем объект в JSON-строку. + string котенокJsonData = котенок.ToJson(котенокDynamicView, args.Token.Model); + + // Добавляем в payload информацию о ссылке на мастера + котенокJsonData = ODataTestHelper.AddEntryRelationship(котенокJsonData, котенокDynamicView, args.Token.Model, котенок.Кошка, nameof(Котенок.Кошка)); + + const string baseUrl = "http://localhost/odata"; + string[] changesets = new[] // Важно, чтобы сначала шёл мастер, потом объект, имеющий на него ссылку. + { + CreateChangeset( + $"{baseUrl}/{args.Token.Model.GetEdmEntitySet(typeof(Кошка)).Name}", + кошка.ToJson(кошкаDynamicView, args.Token.Model), + кошка), + CreateChangeset( + $"{baseUrl}/{args.Token.Model.GetEdmEntitySet(typeof(Котенок)).Name}", + котенокJsonData, + котенок), + }; + + // Act. + HttpRequestMessage batchRequest = CreateBatchRequest(baseUrl, changesets); + using (HttpResponseMessage response = args.HttpClient.SendAsync(batchRequest).Result) + { + // Assert. + CheckODataBatchResponseStatusCode(response, new HttpStatusCode[] { HttpStatusCode.OK, HttpStatusCode.OK }); + Котенок котенокLoaded = args.DataService.Query<Котенок>(котенокDynamicView).FirstOrDefault(x => x.__PrimaryKey == котенок.__PrimaryKey); + Кошка кошкаLoaded = args.DataService.Query<Кошка>(кошкаDynamicView).FirstOrDefault(x => x.__PrimaryKey == кошка.__PrimaryKey); + Assert.NotNull(котенокLoaded); + Assert.NotNull(кошкаLoaded); + Assert.Equal(1, котенокLoaded.Глупость); + Assert.Equal("Петрушка", кошкаLoaded.Кличка); + + // TODO: проверка на экономную загрузку мастера. + } + }); + } + + /// + /// Проверка экономной загрузки мастера при активной настройке MasterLightLoad при смене значения поля у мастера. + /// + [Fact] + public void MasterChangedBatchTest() + { + ActODataService(async args => + { + // Создаем объекты данных, которые потом будем обновлять, и добавляем в базу обычным сервисом данных. + Порода порода = new Порода { Название = "Сиамская" }; + Кошка кошка1 = new Кошка { Кличка = "Болтушка", Агрессивная = true, Порода = порода }; + Кошка кошка2 = new Кошка { Кличка = "Петрушка", Агрессивная = false, Порода = порода }; + args.DataService.UpdateObject(порода); + args.DataService.UpdateObject(кошка1); + args.DataService.UpdateObject(кошка2); + + Котенок котенок = new Котенок { Кошка = кошка1, КличкаКотенка = "Котенок Гав", Глупость = 10 }; + args.DataService.UpdateObject(котенок); + + // Обновляем атрибут объекта + котенок.Глупость = 1; + + // Обновляем мастера + котенок.Кошка = кошка2; + + // Представление, по которому будем обновлять объект + string[] котенокPropertiesNames = + { + Information.ExtractPropertyPath<Котенок>(x => x.__PrimaryKey), + Information.ExtractPropertyPath<Котенок>(x => x.Глупость), + }; + var котенокDynamicView = new View(new ViewAttribute("котенокDynamicView", котенокPropertiesNames), typeof(Котенок)); + + // Преобразуем объект в JSON-строку. + string котенокJsonData = котенок.ToJson(котенокDynamicView, args.Token.Model); + + // Добавляем в payload информацию о ссылке на мастера + котенокJsonData = ODataTestHelper.AddEntryRelationship(котенокJsonData, котенокDynamicView, args.Token.Model, котенок.Кошка, nameof(Котенок.Кошка)); + + const string baseUrl = "http://localhost/odata"; + string[] changesets = new[] // Важно, чтобы сначала шёл мастер, потом объект, имеющий на него ссылку. + { + CreateChangeset( + $"{baseUrl}/{args.Token.Model.GetEdmEntitySet(typeof(Котенок)).Name}", + котенокJsonData, + котенок), + }; + + // Act. + HttpRequestMessage batchRequest = CreateBatchRequest(baseUrl, changesets); + using (HttpResponseMessage response = args.HttpClient.SendAsync(batchRequest).Result) + { + // Assert. + CheckODataBatchResponseStatusCode(response, new HttpStatusCode[] { HttpStatusCode.OK }); + + string[] котенокPropertiesNamesMaster = + { + Information.ExtractPropertyPath<Котенок>(x => x.__PrimaryKey), + Information.ExtractPropertyPath<Котенок>(x => x.Глупость), + Information.ExtractPropertyPath<Котенок>(x => x.Кошка), + }; + var котенокDynamicViewMaster = new View(new ViewAttribute("котенокDynamicView", котенокPropertiesNamesMaster), typeof(Котенок)); + Котенок котенокLoaded = args.DataService.Query<Котенок>(котенокDynamicViewMaster).FirstOrDefault(x => x.Кошка.__PrimaryKey == кошка2.__PrimaryKey); + Assert.NotNull(котенокLoaded); + Assert.Equal(кошка2.__PrimaryKey, котенокLoaded.Кошка.__PrimaryKey); + Assert.Equal(1, котенокLoaded.Глупость); + + // TODO: проверка на экономную загрузку мастера. + } + }); + } + } +} +#endif diff --git a/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/CodeGen/NewPlatform.Flexberry.ORM.ODataService.Tests.crp b/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/CodeGen/NewPlatform.Flexberry.ORM.ODataService.Tests.crp index b149d758..b65594b7 100644 --- a/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/CodeGen/NewPlatform.Flexberry.ORM.ODataService.Tests.crp +++ b/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/CodeGen/NewPlatform.Flexberry.ORM.ODataService.Tests.crp @@ -1332,4 +1332,4 @@ - \ No newline at end of file + diff --git a/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/Helpers/ODataTestHelper.cs b/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/Helpers/ODataTestHelper.cs index 4f8bf49c..d4eba42e 100644 --- a/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/Helpers/ODataTestHelper.cs +++ b/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/Helpers/ODataTestHelper.cs @@ -20,16 +20,16 @@ public static class ODataTestHelper /// Новое тело запроса к OData. public static string AddEntryRelationship(string requestJsonData, View view, DataObjectEdmModel model, DataObject dataObject, string relationName) { - DataObjectDictionary objJsonМедв = DataObjectDictionary.Parse(requestJsonData, view, model); + DataObjectDictionary objJson = DataObjectDictionary.Parse(requestJsonData, view, model); - objJsonМедв.Add( + objJson.Add( $"{relationName}@odata.bind", string.Format( "{0}({1})", model.GetEdmEntitySet(dataObject.GetType()).Name, ((KeyGuid)dataObject.__PrimaryKey).Guid.ToString("D"))); - var result = objJsonМедв.Serialize(); + var result = objJson.Serialize(); return result; } diff --git a/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/Startups/MasterLightLoadTestStartup.cs b/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/Startups/MasterLightLoadTestStartup.cs new file mode 100644 index 00000000..f79b7d5f --- /dev/null +++ b/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/Startups/MasterLightLoadTestStartup.cs @@ -0,0 +1,75 @@ +#if NETCOREAPP +namespace NewPlatform.Flexberry.ORM.ODataService.Tests +{ + using System; + using System.Collections.Generic; + using ICSSoft.Services; + using ICSSoft.STORMNET; + using IIS.Caseberry.Logging.Objects; + using Microsoft.AspNetCore.Builder; + using Microsoft.AspNetCore.Hosting; + using Microsoft.AspNetCore.Routing; + using Microsoft.Extensions.Configuration; + using NewPlatform.Flexberry.ORM.ODataService; + using NewPlatform.Flexberry.ORM.ODataService.Extensions; + using NewPlatform.Flexberry.ORM.ODataService.Model; + using NewPlatform.Flexberry.ORM.ODataService.WebApi.Extensions; + using NewPlatform.Flexberry.Services; + using ODataServiceSample.AspNetCore; + using Unity; + + /// + /// Startup for testing MasterLightLoad configuration. + /// Differs from TestStartup that it marks type as data object for which masters should be light-loaded. + /// + public class MasterLightLoadTestStartup : Startup + { + /// + /// Initialize new instance of TestStartup. + /// + /// Configuration for new instance. + public MasterLightLoadTestStartup(IConfiguration configuration) + : base(configuration) + { + } + + /// + public override void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + IUnityContainer unityContainer = UnityFactory.GetContainer(); + unityContainer.RegisterInstance(env); + + app.UseMiddleware(); + + app.UseMvc(builder => + { + builder.MapRoute("Lock", "api/lock/{action}/{dataObjectId}", new { controller = "Lock" }); + builder.MapFileRoute(); + }); + + app.UseODataService(builder => + { + IUnityContainer container = UnityFactory.GetContainer(); + + var assemblies = new[] + { + typeof(Котенок).Assembly, + typeof(ApplicationLog).Assembly, + typeof(UserSetting).Assembly, + typeof(Lock).Assembly, + }; + + PseudoDetailDefinitions pseudoDetailDefinitions = (PseudoDetailDefinitions)container.Resolve(typeof(PseudoDetailDefinitions)); + + // Set MasterLightLoad property for this DataObject + var masterLightLoadTypes = new List { typeof(Котенок) }; + var modelBuilder = new DefaultDataObjectEdmModelBuilder(assemblies, false, pseudoDetailDefinitions, masterLightLoadTypes: masterLightLoadTypes); + + var token = builder.MapDataObjectRoute(modelBuilder); + + container.RegisterInstance(typeof(ManagementToken), token); + }); + } + } +} +#endif