From 173dc503411fd080286345ca6cd71fe810f2ccab Mon Sep 17 00:00:00 2001 From: inaidanov Date: Wed, 17 Jan 2024 16:23:24 +0500 Subject: [PATCH] =?UTF-8?q?=D0=92=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=82=D1=8C?= =?UTF-8?q?=20UpdateView=20+=20=D1=82=D0=B5=D1=81=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- NewPlatform.Flexberry.ORM.ODataService.nuspec | 230 ++--- .../DataObjectController.ModifyData.cs | 93 +- .../Extensions/DataServiceExtensions.cs | 838 +++++++-------- .../Model/DataObjectEdmModel.cs | 7 + .../Model/DataObjectEdmTypeSettings.cs | 121 +-- .../Model/DefaultDataObjectEdmModelBuilder.cs | 972 +++++++++--------- .../CRUD/Update/MasterLightLoadTest.cs | 77 ++ .../CRUD/Update/ModifyDataTest.cs | 321 +++--- .../CRUD/Update/UpdateViewsTest.cs | 156 +-- ...tform.Flexberry.ORM.ODataService.Tests.crp | 2 +- .../Startups/MasterLightLoadTestStartup.cs | 74 ++ .../Startups/UpdateViewsTestStartup.cs | 154 +-- 12 files changed, 1654 insertions(+), 1391 deletions(-) create mode 100644 Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/CRUD/Update/MasterLightLoadTest.cs create mode 100644 Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/Startups/MasterLightLoadTestStartup.cs diff --git a/NewPlatform.Flexberry.ORM.ODataService.nuspec b/NewPlatform.Flexberry.ORM.ODataService.nuspec index 3fcf28b3..40f52d72 100644 --- a/NewPlatform.Flexberry.ORM.ODataService.nuspec +++ b/NewPlatform.Flexberry.ORM.ODataService.nuspec @@ -1,115 +1,115 @@ - - - - NewPlatform.Flexberry.ORM.ODataService - 7.2.0-beta02 - Flexberry ORM ODataService - New Platform Ltd. - New Platform Ltd. - http://flexberry.ru/License-FlexberryOrm-Runtime - https://flexberry.net/ru/developers-flexberry-orm.html - https://flexberry.net/img/logo-color.png - true - Flexberry ORM OData Service Package. - - 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). - - Changed - 1. Updated Flexberry ORM up to 7.2.0-beta01. - - Fixed - 1. Fixed loading of object with crushing of already loaded masters. - 2. Fixed loading of details. - - Copyright New Platform Ltd 2023 - Flexberry ORM OData ODataService - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + NewPlatform.Flexberry.ORM.ODataService + 7.2.0-beta02 + Flexberry ORM ODataService + New Platform Ltd. + New Platform Ltd. + http://flexberry.ru/License-FlexberryOrm-Runtime + https://flexberry.net/ru/developers-flexberry-orm.html + https://flexberry.net/img/logo-color.png + true + Flexberry ORM OData Service Package. + + 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). + + Changed + 1. Updated Flexberry ORM up to 7.2.0-beta01. + + Fixed + 1. Fixed loading of object with crushing of already loaded masters. + 2. Fixed loading of details. + + Copyright New Platform Ltd 2023 + Flexberry ORM OData ODataService + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/NewPlatform.Flexberry.ORM.ODataService/Controllers/DataObjectController.ModifyData.cs b/NewPlatform.Flexberry.ORM.ODataService/Controllers/DataObjectController.ModifyData.cs index e5b682bd..b9bfc550 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; @@ -28,6 +28,8 @@ using System.Web.Http.Validation; using NewPlatform.Flexberry.ORM.ODataService.Events; using NewPlatform.Flexberry.ORM.ODataService.Handlers; + using Newtonsoft.Json.Linq; + using System.Web; #endif #if NETSTANDARD using Microsoft.AspNet.OData.Formatter; @@ -38,10 +40,10 @@ using NewPlatform.Flexberry.ORM.ODataService.Middleware; #endif - /// - /// Определяет класс контроллера OData, который поддерживает запись и чтение данных с использованием OData формата. - /// - public partial class DataObjectController + /// + /// Определяет класс контроллера OData, который поддерживает запись и чтение данных с использованием OData формата. + /// + public partial class DataObjectController { /// /// Метаданные файлов, временно загруженных в каталог файлового хранилища и привязанных к свойствам обрабатываемых объектов данных. @@ -848,8 +850,41 @@ private static void AddObjectToUpdate(List objsToUpdate, DataObject objsToUpdate.Insert(0, dataObject); // Добавляем объект в начало списка. } - } } + } + + /// + /// Получить значение ключа у указанной сущности. + /// + /// Сущность. + /// Значение ключа. + private object GetKey(EdmEntityObject edmEntity) + { + 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 без загрузки свойств и мастеров/детейлов. Загружен только первичный ключ. + /// + /// Сущность OData. + /// Объект данных. + private DataObject GetDataObjectByEdmEntityLight(EdmEntityObject edmEntity) + { + var masterType = _model.GetDataObjectType(edmEntity); + var masterKey = GetKey(edmEntity); + var dataObject = (DataObject)Activator.CreateInstance(masterType); + dataObject.SetExistObjectPrimaryKey(masterKey); + + return dataObject; + } /// /// Построение объекта данных по сущности OData. @@ -867,21 +902,7 @@ private DataObject GetDataObjectByEdmEntity(EdmEntityObject edmEntity, object ke 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. // Тем самым гарантируем загруженность свойств при необходимости обновления и установку нужного статуса. @@ -896,7 +917,7 @@ private DataObject GetDataObjectByEdmEntity(EdmEntityObject edmEntity, object ke view = _model.GetDataObjectDefaultView(objType); } - DataObject obj = ReturnDataObject(objType, value, view); + DataObject obj = ReturnDataObject(objType, key, view); // Добавляем объект в список для обновления, если там ещё нет объекта с таким ключом. AddObjectToUpdate(dObjs, obj, endObject); @@ -906,12 +927,17 @@ 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) .ToList(); foreach (var prop in changedProps) { + object value; + edmEntity.TryGetPropertyValue(prop.Name, out value); + string dataObjectPropName; try { @@ -931,8 +957,6 @@ private DataObject GetDataObjectByEdmEntity(EdmEntityObject edmEntity, object ke // Обработка мастеров и детейлов. if (prop is EdmNavigationProperty navProp) { - edmEntity.TryGetPropertyValue(prop.Name, out value); - EdmMultiplicity edmMultiplicity = navProp.TargetMultiplicity(); // Обработка мастеров. @@ -942,11 +966,25 @@ private DataObject GetDataObjectByEdmEntity(EdmEntityObject edmEntity, object ke { // Порядок вставки влияет на порядок отправки объектов в 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 masterType = _model.GetDataObjectType(edmEntity); + bool masterLightLoad = _model.IsMasterLightLoad(masterType); + + if (masterLightLoad && !masterOwnPropsUpdated && !isAggregator) + { + master = GetDataObjectByEdmEntityLight(edmMaster); // здесь мастер не добавляется в dObjs (объекты на обновление) т.к. мы точно знаем что он будет в состоянии UnAltered + } + else + { + master = GetDataObjectByEdmEntity(edmMaster, null, dObjs, insertIntoEnd, useUpdateView); + } Information.SetPropValueByName(obj, dataObjectPropName, master); - if (dataObjectPropName == agregatorPropertyName) + if (isAggregator) { master.AddDetail(obj); @@ -1002,10 +1040,9 @@ 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); - edmEntity.TryGetPropertyValue(prop.Name, out value); // Если тип свойства относится к одному из зарегистрированных провайдеров файловых свойств, // значит свойство файловое, и его нужно обработать особым образом. diff --git a/NewPlatform.Flexberry.ORM.ODataService/Extensions/DataServiceExtensions.cs b/NewPlatform.Flexberry.ORM.ODataService/Extensions/DataServiceExtensions.cs index b7639b38..427dbf86 100644 --- a/NewPlatform.Flexberry.ORM.ODataService/Extensions/DataServiceExtensions.cs +++ b/NewPlatform.Flexberry.ORM.ODataService/Extensions/DataServiceExtensions.cs @@ -1,419 +1,419 @@ -namespace NewPlatform.Flexberry.ORM.ODataService.Extensions -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Linq.Expressions; - - using ICSSoft.STORMNET; - using ICSSoft.STORMNET.Business; - using ICSSoft.STORMNET.Business.LINQProvider; - using ICSSoft.STORMNET.FunctionalLanguage; - using ICSSoft.STORMNET.FunctionalLanguage.SQLWhere; - - /// - /// Класс, содержащий расширения для сервиса данных. - /// - public static class DataServiceExtensions - { - /// - /// Осуществляет вычитку объектов данных заданного типа по заданному представлению и LINQ-выражению. - /// - /// Экземпляр сервиса данных. - /// Тип вычитываемых объектов данных, наследуемый от . - /// Представления для вычитки объектов данных. - /// LINQ-выражение, ограничивающее набор вычитываемых объектов. - /// Коллекция вычитанных объектов. - public static object Execute(this SQLDataService dataService, Type dataObjectType, View dataObjectView, Expression expression) - { - IQueryProvider queryProvider = typeof(LcsQueryProvider<,>) - .MakeGenericType(dataObjectType, typeof(LcsGeneratorQueryModelVisitor)) - .GetConstructor(new Type[3] { typeof(SQLDataService), typeof(View), typeof(IEnumerable) }) - .Invoke(new object[3] { dataService, dataObjectView, null }) as IQueryProvider; - - return queryProvider.Execute(expression); - } - - /// - /// Загрузка объекта с его мастерами (объект должен быть не изменнённый и не до конца загруженный). - /// С мастерами необходимо обращаться аккуратно: если в кэше уже есть мастер, то нужно эту ситуацию разрешить, - /// поскольку иначе стандартная загрузка перетрёт данные мастера в кэше (и если он там изменён, то все изменения будут потеряны). - /// - /// Экземпляр сервиса данных. - /// Представление объекта с мастерами. - /// Объект данных, в который будет производиться загрузка. - /// Текущий кэш объектов данных (в данном кэше ранее существующие там объекты не должны быть перетёрты). - public static void SafeLoadWithMasters( - this IDataService dataService, View view, ICSSoft.STORMNET.DataObject dobjectFromCache, DataObjectCache dataObjectCache) - { - if (dataService == null) - { - throw new ArgumentNullException(nameof(dataService)); - } - - if (view == null) - { - throw new ArgumentNullException(nameof(view)); - } - - if (dobjectFromCache == null) - { - throw new ArgumentNullException(nameof(dobjectFromCache)); - } - - if (dataObjectCache == null) - { - throw new ArgumentNullException(nameof(dataObjectCache)); - } - - // Прогружается пустой объект, чтобы избежать риска перетирания основного. - DataObject createdObject = (DataObject)Activator.CreateInstance(dobjectFromCache.GetType()); - createdObject.SetExistObjectPrimaryKey(dobjectFromCache.__PrimaryKey); - - // Используется отдельный кэш, чтобы не перетереть данные в основном кэше. - DataObjectCache specialCache = new DataObjectCache(); - specialCache.StartCaching(false); - specialCache.AddDataObject(createdObject); - dataService.LoadObject(view, createdObject, false, true, specialCache); - specialCache.StopCaching(); - - // Перенос данных из одного объекта в другой. - ProperUpdateOfObject(dobjectFromCache, createdObject, dataObjectCache, specialCache); - } - - /// - /// Загрузка детейлов с сохранением состояния изменения. - /// - /// Экземпляр сервиса данных. - /// Представление агрегатора с детейлами. Чтение будет идти по массиву Details представлений детейлов, включая детейлы второго и последующих уровней. - /// Агрегаторы, для которых дочитываются детейлы. - /// Кеш объектов данных, в нём хранятся состояния детейлов, которые не надо затирать. В него же будут добавлены новые зачитанные детейлы. - public static void SafeLoadDetails(this IDataService dataService, View view, IList agregators, DataObjectCache dataObjectCache) - { - if (dataService == null) - { - throw new ArgumentNullException(nameof(dataService)); - } - - if (view == null) - { - throw new ArgumentNullException(nameof(view)); - } - - if (agregators == null) - { - throw new ArgumentNullException(nameof(agregators)); - } - - if (dataObjectCache == null) - { - throw new ArgumentNullException(nameof(dataObjectCache)); - } - - if (agregators.Count == 0) - { - return; - } - - // Обрабатываем все детейлы, которые указаны в представлении. - DetailInView[] detailsInView = view.Details; - foreach (DetailInView detailInView in detailsInView) - { - // Оригинальное представление будет использоваться в рекурсии. - View detailView = detailInView.View.Clone(); - DetailInView[] secondDetailsInView = detailView.Details; - - // Удалим из представления детейлы второго уровня, поскольку их обработка будет рекурсивной с аналогичной логикой. - foreach (DetailInView secondDetailInView in secondDetailsInView) - { - detailView.RemoveDetail(secondDetailInView.Name); - } - - Type detailType = detailView.DefineClassType; - string agregatorPropertyName = Information.GetAgregatePropertyName(detailType); - - // Нужно гарантировать, что у загруженных детейлов будет проставлена ссылка на агрегатора, поэтому добавим свойство в представление (если уже есть, то второй раз не добавится). - detailView.AddProperty(agregatorPropertyName); - LoadingCustomizationStruct lcs = LoadingCustomizationStruct.GetSimpleStruct(detailView.DefineClassType, detailView); - Type[] usageTypes = TypeUsageProvider.TypeUsage.GetUsageTypes(view.DefineClassType, detailInView.Name); - List storedTypes = new List(); - foreach (var usageType in usageTypes) - { - if (Information.IsStoredType(usageType)) - { - storedTypes.Add(usageType); - } - } - - if (!storedTypes.Any()) - { - foreach (DataObject agregator in agregators) - { - Type agregatorType = agregator.GetType(); - Type[] usageTypesFromAgregator = TypeUsageProvider.TypeUsage.GetUsageTypes(agregatorType, detailInView.Name); - foreach (Type usageTypeFromAgregator in usageTypesFromAgregator) - { - if (Information.IsStoredType(usageTypeFromAgregator) && !storedTypes.Contains(usageTypeFromAgregator)) - { - storedTypes.Add(usageTypeFromAgregator); - } - } - } - } - - lcs.LoadingTypes = storedTypes.ToArray(); - Type agregatorKeyType = agregators[0].__PrimaryKey.GetType(); - - // Производим обработку только тех агрегаторов, для которых ранее не был загружен детейл. - object[] keys = agregators.Where(a => !a.CheckLoadedProperty(detailInView.Name)).Select(d => d.__PrimaryKey).ToArray(); - lcs.LimitFunction = FunctionBuilder.BuildIn(agregatorPropertyName, SQLWhereLanguageDef.LanguageDef.GetObjectTypeForNetType(agregatorKeyType), keys); - - // Нужно соблюсти единственность инстанций агрегаторов при вычитке, поэтому реализуем отдельный кеш. Смешивать с кешем dataObjectCache нельзя, поскольку в предстоящей выборке будут те же самые детейлы (значения в кеше затрутся). - // Агрегаторы в кэш не помещаем. От помещения агрегаторов в кэш возникают неконтролируемые сбои основного кэша. - DataObjectCache agregatorCache = new DataObjectCache(); - agregatorCache.StartCaching(false); - - // Вычитываются детейлы одного типа, но для нескольких инстанций агрегаторов (оптимизируем количество SQL-запросов). - DataObject[] loadedDetails = dataService.LoadObjects(lcs, agregatorCache); - agregatorCache.StopCaching(); - - Dictionary extraCacheForAgregators = new Dictionary(); - foreach (DataObject agregator in agregators) - { - agregator.AddLoadedProperties(detailInView.Name); - extraCacheForAgregators.Add(agregator.__PrimaryKey, agregator); - } - - // Ввиду того, что агрегаторы нам пришли готовые с пустыми коллекциями детейлов, заполняем детейлы по агрегаторам значениями из кеша или из базы. - // На загрузку детейлов второго уровня передаем только детейлы отсутствующие в кэше. - List toLoadSecondDetails = new List(); - foreach (DataObject loadedDetail in loadedDetails) - { - DataObject agregatorTemp = (DataObject)Information.GetPropValueByName(loadedDetail, agregatorPropertyName); - DataObject agregator = extraCacheForAgregators[agregatorTemp.__PrimaryKey]; - object detailPrimaryKey = loadedDetail.__PrimaryKey; - - DataObject detailFromCache = dataObjectCache.GetLivingDataObject(loadedDetail.GetType(), detailPrimaryKey); - - // Необходимо игнорировать объекты-пустышки, которые проинициализированы только первичным ключом. - bool detailFromCacheIsEmpty = detailFromCache == null || !detailFromCache.GetLoadedProperties().Any(); - DataObject detailForAdd = detailFromCacheIsEmpty - ? loadedDetail - : detailFromCache; - - agregator.AddDetail(detailForAdd); - - DataObject agregatorDataCopy = agregator.GetDataCopy(); - DataObject detailDataCopy = detailForAdd.GetDataCopy(); - - if (agregatorDataCopy != null && detailDataCopy != null) - { - agregatorDataCopy.AddDetail(detailDataCopy); - } - - if (detailFromCacheIsEmpty) - { - dataObjectCache.AddDataObject(loadedDetail); - toLoadSecondDetails.Add(loadedDetail); - } - } - - // Детейлы второго и последующих уровней нужно обработать аналогичным образом. - if (toLoadSecondDetails.Any() && secondDetailsInView.Any()) - { - dataService.SafeLoadDetails(detailInView.View, toLoadSecondDetails, dataObjectCache); - } - } - } - - /// - /// Догрузка объекта по указанному представлению, с загрузкой детейлов с сохранением состояния изменения. - /// - /// Экземпляр сервиса данных. - /// Объект данных, который нужно догрузить. - /// Представление, которое используется для догрузки. - /// Кеш объектов данных. - public static void SafeLoadObject(this IDataService dataService, DataObject dataObject, View view, DataObjectCache dataObjectCache) - { - if (dataService == null) - { - throw new ArgumentNullException(nameof(dataService)); - } - - if (view == null) - { - throw new ArgumentNullException(nameof(view)); - } - - // Вычитывать объект сразу с детейлами нельзя, поскольку в этой же транзакции могут уже оказаться отдельные операции с детейлами и перевычитка затрёт эти изменения. - View miniView = view.Clone(); - DetailInView[] miniViewDetails = miniView.Details; - miniView.Details = new DetailInView[0]; - dataService.LoadObject(miniView, dataObject, false, true, dataObjectCache); - - if (miniViewDetails.Length > 0) - { - dataService.SafeLoadDetails(view, new DataObject[] { dataObject }, dataObjectCache); - } - } - - /// - /// Add detail object to agregator according detail type. - /// - /// Agregator object. - /// Detail object. - public static void AddDetail(this DataObject agregator, DataObject detail) - { - if (agregator == null) - { - throw new ArgumentNullException(nameof(agregator)); - } - - if (detail == null) - { - throw new ArgumentNullException(nameof(detail)); - } - - Type agregatorType = agregator.GetType(); - Type detailType = detail.GetType(); - string detailPropertyName = Information.GetDetailArrayPropertyName(agregatorType, detailType); - - Type parentType = detailType.BaseType; - while (detailPropertyName == null && parentType != typeof(DataObject) && parentType != typeof(object) && parentType != null) - { - detailPropertyName = Information.GetDetailArrayPropertyName(agregatorType, parentType); - parentType = parentType.BaseType; - } - - if (detailPropertyName != null) - { - DetailArray details = (DetailArray)Information.GetPropValueByName(agregator, detailPropertyName); - - if (details != null) - { - DataObject existDetail = details.GetByKey(detail.__PrimaryKey); - - if (existDetail == null) - { - details.AddObject(detail); - } - } - } - else - { - LogService.LogWarn($"Detail type {detailType.AssemblyQualifiedName} not found in agregator of type {agregatorType.AssemblyQualifiedName}."); - } - } - - - /// - /// Перенос означенных свойств из свежезагруженного объекта в основной, расположенный в основном кэше. - /// - /// Основной объект, куда необходимо копировать значения свойств. - /// Свежезагруженный объект. - /// Основной кэш. - /// Локальный кэш, куда была выполнена свежая прогрузка. - private static void ProperUpdateOfObject(DataObject currentObject, DataObject loadedObjectLocal, DataObjectCache dataObjectCache, DataObjectCache dataObjectCacheLocal) - { - if (currentObject == null) - { - throw new ArgumentNullException(nameof(currentObject)); - } - - if (loadedObjectLocal == null) - { - throw new ArgumentNullException(nameof(loadedObjectLocal)); - } - - if (dataObjectCache == null) - { - throw new ArgumentNullException(nameof(dataObjectCache)); - } - - if (dataObjectCacheLocal == null) - { - throw new ArgumentNullException(nameof(dataObjectCacheLocal)); - } - - // Перенос значений свойств объекта (в том числе могут быть мастера). Если мастера означены, то перенос свойств мастера производится далее. - List localObjectLoadedProps = loadedObjectLocal.GetLoadedPropertiesList(); - List currentObjectLoadedProps = currentObject.GetLoadedPropertiesList(); - List notLoadedForActual = localObjectLoadedProps.Except(currentObjectLoadedProps).ToList(); - DataObject currentDataCopy = currentObject.GetDataCopy(); - foreach (string notLoadedPropName in notLoadedForActual) - { - object propValue = Information.GetPropValueByName(loadedObjectLocal, notLoadedPropName); - Information.SetPropValueByName(currentObject, notLoadedPropName, propValue); - currentObject.AddLoadedProperties(notLoadedPropName); - Information.SetPropValueByName(currentDataCopy, notLoadedPropName, propValue); - } - - // Ещё могут быть частично загруженные мастера. - ProperCacheUpdateForOneObject(dataObjectCache, dataObjectCacheLocal, loadedObjectLocal, true); - } - - /// - /// Обновление кэша по свежезагруженному объекту. - /// - /// Текущий основной кэш объектов. - /// Вспомогательный кэш, куда загружался объект. - /// Свежезагруженный объект, по которому обновляется основной кэш. - /// Флаг, определяющий, что в кэш уже добавлен свежезагруженный объект. - private static void ProperCacheUpdateForOneObject(DataObjectCache dataObjectCacheActual, DataObjectCache dataObjectCacheWithMasters, DataObject loadedDataObject, bool loadedObjectsAdded) - { - if (dataObjectCacheActual == null) - { - throw new ArgumentNullException(nameof(dataObjectCacheActual)); - } - - if (dataObjectCacheWithMasters == null) - { - throw new ArgumentNullException(nameof(dataObjectCacheWithMasters)); - } - - if (loadedDataObject == null) - { - return; - } - - if (!loadedObjectsAdded) - { - dataObjectCacheActual.AddDataObject(loadedDataObject); - } - - Type dobjType = typeof(DataObject); - Type currentType = loadedDataObject.GetType(); - List loadedProperties = loadedDataObject.GetLoadedPropertiesList(); - foreach (string currentPropertyName in loadedProperties) - { - Type currentPropertyType = Information.GetPropertyType(currentType, currentPropertyName); - if (currentPropertyType.IsSubclassOf(dobjType)) // Выбираем у текущего объекта ссылки на мастеров. - { - DataObject currentMaster = (DataObject)Information.GetPropValueByName(loadedDataObject, currentPropertyName); - if (currentMaster != null) - { - // Типы currentPropertyType и currentMaster.GetType() могут различаться из-за наследования. - DataObject masterFromActualCache = dataObjectCacheActual.GetLivingDataObject(currentMaster.GetType(), currentMaster.__PrimaryKey); - - if (masterFromActualCache == null) - { - // Если мастера ранее не было в кэше, то просто его туда переносим. - dataObjectCacheActual.AddDataObject(currentMaster); - - // Но в добавленном мастере могут быть мастера 2 и далее уровней. - ProperCacheUpdateForOneObject(dataObjectCacheActual, dataObjectCacheWithMasters, currentMaster, true); - } - else - { // Если мастер был в кэше, то аккуратно нужно перенести только незагруженные ранее свойства. - if (masterFromActualCache.GetStatus(false) == ObjectStatus.UnAltered && masterFromActualCache.GetLoadingState() != LoadingState.Loaded) - { - ProperUpdateOfObject(masterFromActualCache, currentMaster, dataObjectCacheActual, dataObjectCacheWithMasters); - } - } - } - } - } - } - - } -} +namespace NewPlatform.Flexberry.ORM.ODataService.Extensions +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + + using ICSSoft.STORMNET; + using ICSSoft.STORMNET.Business; + using ICSSoft.STORMNET.Business.LINQProvider; + using ICSSoft.STORMNET.FunctionalLanguage; + using ICSSoft.STORMNET.FunctionalLanguage.SQLWhere; + + /// + /// Класс, содержащий расширения для сервиса данных. + /// + public static class DataServiceExtensions + { + /// + /// Осуществляет вычитку объектов данных заданного типа по заданному представлению и LINQ-выражению. + /// + /// Экземпляр сервиса данных. + /// Тип вычитываемых объектов данных, наследуемый от . + /// Представления для вычитки объектов данных. + /// LINQ-выражение, ограничивающее набор вычитываемых объектов. + /// Коллекция вычитанных объектов. + public static object Execute(this SQLDataService dataService, Type dataObjectType, View dataObjectView, Expression expression) + { + IQueryProvider queryProvider = typeof(LcsQueryProvider<,>) + .MakeGenericType(dataObjectType, typeof(LcsGeneratorQueryModelVisitor)) + .GetConstructor(new Type[3] { typeof(SQLDataService), typeof(View), typeof(IEnumerable) }) + .Invoke(new object[3] { dataService, dataObjectView, null }) as IQueryProvider; + + return queryProvider.Execute(expression); + } + + /// + /// Загрузка объекта с его мастерами (объект должен быть не изменнённый и не до конца загруженный). + /// С мастерами необходимо обращаться аккуратно: если в кэше уже есть мастер, то нужно эту ситуацию разрешить, + /// поскольку иначе стандартная загрузка перетрёт данные мастера в кэше (и если он там изменён, то все изменения будут потеряны). + /// + /// Экземпляр сервиса данных. + /// Представление объекта с мастерами. + /// Объект данных, в который будет производиться загрузка. + /// Текущий кэш объектов данных (в данном кэше ранее существующие там объекты не должны быть перетёрты). + public static void SafeLoadWithMasters( + this IDataService dataService, View view, ICSSoft.STORMNET.DataObject dobjectFromCache, DataObjectCache dataObjectCache) + { + if (dataService == null) + { + throw new ArgumentNullException(nameof(dataService)); + } + + if (view == null) + { + throw new ArgumentNullException(nameof(view)); + } + + if (dobjectFromCache == null) + { + throw new ArgumentNullException(nameof(dobjectFromCache)); + } + + if (dataObjectCache == null) + { + throw new ArgumentNullException(nameof(dataObjectCache)); + } + + // Прогружается пустой объект, чтобы избежать риска перетирания основного. + DataObject createdObject = (DataObject)Activator.CreateInstance(dobjectFromCache.GetType()); + createdObject.SetExistObjectPrimaryKey(dobjectFromCache.__PrimaryKey); + + // Используется отдельный кэш, чтобы не перетереть данные в основном кэше. + DataObjectCache specialCache = new DataObjectCache(); + specialCache.StartCaching(false); + specialCache.AddDataObject(createdObject); + dataService.LoadObject(view, createdObject, false, true, specialCache); + specialCache.StopCaching(); + + // Перенос данных из одного объекта в другой. + ProperUpdateOfObject(dobjectFromCache, createdObject, dataObjectCache, specialCache); + } + + /// + /// Загрузка детейлов с сохранением состояния изменения. + /// + /// Экземпляр сервиса данных. + /// Представление агрегатора с детейлами. Чтение будет идти по массиву Details представлений детейлов, включая детейлы второго и последующих уровней. + /// Агрегаторы, для которых дочитываются детейлы. + /// Кеш объектов данных, в нём хранятся состояния детейлов, которые не надо затирать. В него же будут добавлены новые зачитанные детейлы. + public static void SafeLoadDetails(this IDataService dataService, View view, IList agregators, DataObjectCache dataObjectCache) + { + if (dataService == null) + { + throw new ArgumentNullException(nameof(dataService)); + } + + if (view == null) + { + throw new ArgumentNullException(nameof(view)); + } + + if (agregators == null) + { + throw new ArgumentNullException(nameof(agregators)); + } + + if (dataObjectCache == null) + { + throw new ArgumentNullException(nameof(dataObjectCache)); + } + + if (agregators.Count == 0) + { + return; + } + + // Обрабатываем все детейлы, которые указаны в представлении. + DetailInView[] detailsInView = view.Details; + foreach (DetailInView detailInView in detailsInView) + { + // Оригинальное представление будет использоваться в рекурсии. + View detailView = detailInView.View.Clone(); + DetailInView[] secondDetailsInView = detailView.Details; + + // Удалим из представления детейлы второго уровня, поскольку их обработка будет рекурсивной с аналогичной логикой. + foreach (DetailInView secondDetailInView in secondDetailsInView) + { + detailView.RemoveDetail(secondDetailInView.Name); + } + + Type detailType = detailView.DefineClassType; + string agregatorPropertyName = Information.GetAgregatePropertyName(detailType); + + // Нужно гарантировать, что у загруженных детейлов будет проставлена ссылка на агрегатора, поэтому добавим свойство в представление (если уже есть, то второй раз не добавится). + detailView.AddProperty(agregatorPropertyName); + LoadingCustomizationStruct lcs = LoadingCustomizationStruct.GetSimpleStruct(detailView.DefineClassType, detailView); + Type[] usageTypes = TypeUsageProvider.TypeUsage.GetUsageTypes(view.DefineClassType, detailInView.Name); + List storedTypes = new List(); + foreach (var usageType in usageTypes) + { + if (Information.IsStoredType(usageType)) + { + storedTypes.Add(usageType); + } + } + + if (!storedTypes.Any()) + { + foreach (DataObject agregator in agregators) + { + Type agregatorType = agregator.GetType(); + Type[] usageTypesFromAgregator = TypeUsageProvider.TypeUsage.GetUsageTypes(agregatorType, detailInView.Name); + foreach (Type usageTypeFromAgregator in usageTypesFromAgregator) + { + if (Information.IsStoredType(usageTypeFromAgregator) && !storedTypes.Contains(usageTypeFromAgregator)) + { + storedTypes.Add(usageTypeFromAgregator); + } + } + } + } + + lcs.LoadingTypes = storedTypes.ToArray(); + Type agregatorKeyType = agregators[0].__PrimaryKey.GetType(); + + // Производим обработку только тех агрегаторов, для которых ранее не был загружен детейл. + object[] keys = agregators.Where(a => !a.CheckLoadedProperty(detailInView.Name)).Select(d => d.__PrimaryKey).ToArray(); + lcs.LimitFunction = FunctionBuilder.BuildIn(agregatorPropertyName, SQLWhereLanguageDef.LanguageDef.GetObjectTypeForNetType(agregatorKeyType), keys); + + // Нужно соблюсти единственность инстанций агрегаторов при вычитке, поэтому реализуем отдельный кеш. Смешивать с кешем dataObjectCache нельзя, поскольку в предстоящей выборке будут те же самые детейлы (значения в кеше затрутся). + // Агрегаторы в кэш не помещаем. От помещения агрегаторов в кэш возникают неконтролируемые сбои основного кэша. + DataObjectCache agregatorCache = new DataObjectCache(); + agregatorCache.StartCaching(false); + + // Вычитываются детейлы одного типа, но для нескольких инстанций агрегаторов (оптимизируем количество SQL-запросов). + DataObject[] loadedDetails = dataService.LoadObjects(lcs, agregatorCache); + agregatorCache.StopCaching(); + + Dictionary extraCacheForAgregators = new Dictionary(); + foreach (DataObject agregator in agregators) + { + agregator.AddLoadedProperties(detailInView.Name); + extraCacheForAgregators.Add(agregator.__PrimaryKey, agregator); + } + + // Ввиду того, что агрегаторы нам пришли готовые с пустыми коллекциями детейлов, заполняем детейлы по агрегаторам значениями из кеша или из базы. + // На загрузку детейлов второго уровня передаем только детейлы отсутствующие в кэше. + List toLoadSecondDetails = new List(); + foreach (DataObject loadedDetail in loadedDetails) + { + DataObject agregatorTemp = (DataObject)Information.GetPropValueByName(loadedDetail, agregatorPropertyName); + DataObject agregator = extraCacheForAgregators[agregatorTemp.__PrimaryKey]; + object detailPrimaryKey = loadedDetail.__PrimaryKey; + + DataObject detailFromCache = dataObjectCache.GetLivingDataObject(loadedDetail.GetType(), detailPrimaryKey); + + // Необходимо игнорировать объекты-пустышки, которые проинициализированы только первичным ключом. + bool detailFromCacheIsEmpty = detailFromCache == null || !detailFromCache.GetLoadedProperties().Any(); + DataObject detailForAdd = detailFromCacheIsEmpty + ? loadedDetail + : detailFromCache; + + agregator.AddDetail(detailForAdd); + + DataObject agregatorDataCopy = agregator.GetDataCopy(); + DataObject detailDataCopy = detailForAdd.GetDataCopy(); + + if (agregatorDataCopy != null && detailDataCopy != null) + { + agregatorDataCopy.AddDetail(detailDataCopy); + } + + if (detailFromCacheIsEmpty) + { + dataObjectCache.AddDataObject(loadedDetail); + toLoadSecondDetails.Add(loadedDetail); + } + } + + // Детейлы второго и последующих уровней нужно обработать аналогичным образом. + if (toLoadSecondDetails.Any() && secondDetailsInView.Any()) + { + dataService.SafeLoadDetails(detailInView.View, toLoadSecondDetails, dataObjectCache); + } + } + } + + /// + /// Догрузка объекта по указанному представлению, с загрузкой детейлов с сохранением состояния изменения. + /// + /// Экземпляр сервиса данных. + /// Объект данных, который нужно догрузить. + /// Представление, которое используется для догрузки. + /// Кеш объектов данных. + public static void SafeLoadObject(this IDataService dataService, DataObject dataObject, View view, DataObjectCache dataObjectCache) + { + if (dataService == null) + { + throw new ArgumentNullException(nameof(dataService)); + } + + if (view == null) + { + throw new ArgumentNullException(nameof(view)); + } + + // Вычитывать объект сразу с детейлами нельзя, поскольку в этой же транзакции могут уже оказать отдельные операции с детейлами и перевычитка затрёт эти изменения. + View miniView = view.Clone(); + DetailInView[] miniViewDetails = miniView.Details; + miniView.Details = new DetailInView[0]; + dataService.LoadObject(miniView, dataObject, false, true, dataObjectCache); + + if (miniViewDetails.Length > 0) + { + dataService.SafeLoadDetails(view, new DataObject[] { dataObject }, dataObjectCache); + } + } + + /// + /// Add detail object to agregator according detail type. + /// + /// Agregator object. + /// Detail object. + public static void AddDetail(this DataObject agregator, DataObject detail) + { + if (agregator == null) + { + throw new ArgumentNullException(nameof(agregator)); + } + + if (detail == null) + { + throw new ArgumentNullException(nameof(detail)); + } + + Type agregatorType = agregator.GetType(); + Type detailType = detail.GetType(); + string detailPropertyName = Information.GetDetailArrayPropertyName(agregatorType, detailType); + + Type parentType = detailType.BaseType; + while (detailPropertyName == null && parentType != typeof(DataObject) && parentType != typeof(object) && parentType != null) + { + detailPropertyName = Information.GetDetailArrayPropertyName(agregatorType, parentType); + parentType = parentType.BaseType; + } + + if (detailPropertyName != null) + { + DetailArray details = (DetailArray)Information.GetPropValueByName(agregator, detailPropertyName); + + if (details != null) + { + DataObject existDetail = details.GetByKey(detail.__PrimaryKey); + + if (existDetail == null) + { + details.AddObject(detail); + } + } + } + else + { + LogService.LogWarn($"Detail type {detailType.AssemblyQualifiedName} not found in agregator of type {agregatorType.AssemblyQualifiedName}."); + } + } + + + /// + /// Перенос означенных свойств из свежезагруженного объекта в основной, расположенный в основном кэше. + /// + /// Основной объект, куда необходимо копировать значения свойств. + /// Свежезагруженный объект. + /// Основной кэш. + /// Локальный кэш, куда была выполнена свежая прогрузка. + private static void ProperUpdateOfObject(DataObject currentObject, DataObject loadedObjectLocal, DataObjectCache dataObjectCache, DataObjectCache dataObjectCacheLocal) + { + if (currentObject == null) + { + throw new ArgumentNullException(nameof(currentObject)); + } + + if (loadedObjectLocal == null) + { + throw new ArgumentNullException(nameof(loadedObjectLocal)); + } + + if (dataObjectCache == null) + { + throw new ArgumentNullException(nameof(dataObjectCache)); + } + + if (dataObjectCacheLocal == null) + { + throw new ArgumentNullException(nameof(dataObjectCacheLocal)); + } + + // Перенос значений свойств объекта (в том числе могут быть мастера). Если мастера означены, то перенос свойств мастера производится далее. + List localObjectLoadedProps = loadedObjectLocal.GetLoadedPropertiesList(); + List currentObjectLoadedProps = currentObject.GetLoadedPropertiesList(); + List notLoadedForActual = localObjectLoadedProps.Except(currentObjectLoadedProps).ToList(); + DataObject currentDataCopy = currentObject.GetDataCopy(); + foreach (string notLoadedPropName in notLoadedForActual) + { + object propValue = Information.GetPropValueByName(loadedObjectLocal, notLoadedPropName); + Information.SetPropValueByName(currentObject, notLoadedPropName, propValue); + currentObject.AddLoadedProperties(notLoadedPropName); + Information.SetPropValueByName(currentDataCopy, notLoadedPropName, propValue); + } + + // Ещё могут быть частично загруженные мастера. + ProperCacheUpdateForOneObject(dataObjectCache, dataObjectCacheLocal, loadedObjectLocal, true); + } + + /// + /// Обновление кэша по свежезагруженному объекту. + /// + /// Текущий основной кэш объектов. + /// Вспомогательный кэш, куда загружался объект. + /// Свежезагруженный объект, по которому обновляется основной кэш. + /// Флаг, определяющий, что в кэш уже добавлен свежезагруженный объект. + private static void ProperCacheUpdateForOneObject(DataObjectCache dataObjectCacheActual, DataObjectCache dataObjectCacheWithMasters, DataObject loadedDataObject, bool loadedObjectsAdded) + { + if (dataObjectCacheActual == null) + { + throw new ArgumentNullException(nameof(dataObjectCacheActual)); + } + + if (dataObjectCacheWithMasters == null) + { + throw new ArgumentNullException(nameof(dataObjectCacheWithMasters)); + } + + if (loadedDataObject == null) + { + return; + } + + if (!loadedObjectsAdded) + { + dataObjectCacheActual.AddDataObject(loadedDataObject); + } + + Type dobjType = typeof(DataObject); + Type currentType = loadedDataObject.GetType(); + List loadedProperties = loadedDataObject.GetLoadedPropertiesList(); + foreach (string currentPropertyName in loadedProperties) + { + Type currentPropertyType = Information.GetPropertyType(currentType, currentPropertyName); + if (currentPropertyType.IsSubclassOf(dobjType)) // Выбираем у текущего объекта ссылки на мастеров. + { + DataObject currentMaster = (DataObject)Information.GetPropValueByName(loadedDataObject, currentPropertyName); + if (currentMaster != null) + { + // Типы currentPropertyType и currentMaster.GetType() могут различаться из-за наследования. + DataObject masterFromActualCache = dataObjectCacheActual.GetLivingDataObject(currentMaster.GetType(), currentMaster.__PrimaryKey); + + if (masterFromActualCache == null) + { + // Если мастера ранее не было в кэше, то просто его туда переносим. + dataObjectCacheActual.AddDataObject(currentMaster); + + // Но в добавленном мастере могут быть мастера 2 и далее уровней. + ProperCacheUpdateForOneObject(dataObjectCacheActual, dataObjectCacheWithMasters, currentMaster, true); + } + else + { // Если мастер был в кэше, то аккуратно нужно перенести только незагруженные ранее свойства. + if (masterFromActualCache.GetStatus(false) == ObjectStatus.UnAltered && masterFromActualCache.GetLoadingState() != LoadingState.Loaded) + { + ProperUpdateOfObject(masterFromActualCache, currentMaster, dataObjectCacheActual, dataObjectCacheWithMasters); + } + } + } + } + } + } + + } +} diff --git a/NewPlatform.Flexberry.ORM.ODataService/Model/DataObjectEdmModel.cs b/NewPlatform.Flexberry.ORM.ODataService/Model/DataObjectEdmModel.cs index dcfae14f..88f9efea 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[dataObjectType].MasterLightLoad; + /// /// Получает список зарегистрированных в модели типов по списку имён типов. /// diff --git a/NewPlatform.Flexberry.ORM.ODataService/Model/DataObjectEdmTypeSettings.cs b/NewPlatform.Flexberry.ORM.ODataService/Model/DataObjectEdmTypeSettings.cs index 4e4219a1..82db8ccb 100644 --- a/NewPlatform.Flexberry.ORM.ODataService/Model/DataObjectEdmTypeSettings.cs +++ b/NewPlatform.Flexberry.ORM.ODataService/Model/DataObjectEdmTypeSettings.cs @@ -1,59 +1,64 @@ -namespace NewPlatform.Flexberry.ORM.ODataService.Model -{ - using System.Collections.Generic; - using System.Reflection; - - using ICSSoft.STORMNET; - using System; - - /// - /// Metadata class for types. - /// - public sealed class DataObjectEdmTypeSettings - { - /// - /// Type of key, if null then used from based type. - /// - public Type KeyType { get; set; } - - /// - /// The name of appropriate EDM entity set. - /// - public string CollectionName { get; set; } - - /// - /// Is EDM entity set is required. - /// - public bool EnableCollection { get; set; } - - /// - /// Default view for queries without $select parameter. - /// - public View DefaultView { get; set; } - - /// - /// View to be used instead of DefaultView on updates (Patch/Batch). - /// - public View UpdateView { get; set; } - - /// - /// The list of exposed details. - /// - public IDictionary DetailProperties { get; } = new Dictionary(); - - /// - /// The list of exposed masters. - /// - public IDictionary MasterProperties { get; } = new Dictionary(); - - /// - /// The list of exposed properties. - /// - public IList OwnProperties { get; } = new List(); - - /// - /// The list of exposed links from master to pseudodetail (pseudoproperties) as properties. - /// - public IDictionary PseudoDetailProperties { get; } = new Dictionary(); - } +namespace NewPlatform.Flexberry.ORM.ODataService.Model +{ + using System.Collections.Generic; + using System.Reflection; + + using ICSSoft.STORMNET; + using System; + + /// + /// Metadata class for types. + /// + public sealed class DataObjectEdmTypeSettings + { + /// + /// Type of key, if null then used from based type. + /// + public Type KeyType { get; set; } + + /// + /// The name of appropriate EDM entity set. + /// + public string CollectionName { get; set; } + + /// + /// Is EDM entity set is required. + /// + public bool EnableCollection { get; set; } + + /// + /// Default view for queries without $select parameter. + /// + public View DefaultView { get; set; } + + /// + /// View to be used instead of DefaultView on updates (Patch/Batch). + /// + 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. + /// + public IDictionary DetailProperties { get; } = new Dictionary(); + + /// + /// The list of exposed masters. + /// + public IDictionary MasterProperties { get; } = new Dictionary(); + + /// + /// The list of exposed properties. + /// + public IList OwnProperties { get; } = new List(); + + /// + /// The list of exposed links from master to pseudodetail (pseudoproperties) as properties. + /// + 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..1e19d071 100644 --- a/NewPlatform.Flexberry.ORM.ODataService/Model/DefaultDataObjectEdmModelBuilder.cs +++ b/NewPlatform.Flexberry.ORM.ODataService/Model/DefaultDataObjectEdmModelBuilder.cs @@ -1,461 +1,511 @@ -namespace NewPlatform.Flexberry.ORM.ODataService.Model -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Reflection; - - using ICSSoft.STORMNET; - using ICSSoft.STORMNET.KeyGen; - - using Microsoft.OData.Edm; - - /// - /// Default implementation of . - /// Builds EDM-model by list of assemblies. - /// - /// - public class DefaultDataObjectEdmModelBuilder : IDataObjectEdmModelBuilder - { - /// - /// The list of assemblies for searching types to expose. - /// - private readonly IEnumerable _searchAssemblies; - - /// - /// Is need to add the whole type namespace for EDM entity set. - /// - private readonly bool _useNamespaceInEntitySetName; - - /// - /// The list of links from master to pseudodetail (pseudoproperty) definitions. - /// - private readonly PseudoDetailDefinitions _pseudoDetailDefinitions; - - /// - /// Additional mapping of CLR type to edm primitive type. When it's required on the application side. - /// - public Dictionary AdditionalMapping { get; } - - /// - /// Delegate for additional filtering exposed types. - /// At the result EDM-model will be added only those types, for that the delegate returned true. - /// - public Func TypeFilter { get; set; } - - /// - /// Delegate for additional filtering exposed properties. - /// At the result EDM-model will be added only those properties, for that the delegate returned true. - /// - public Func PropertyFilter { get; set; } - - /// - /// Delegate for building names for EDM entity sets. - /// - public Func EntitySetNameBuilder { get; set; } - - /// - /// Delegate for building namespaces for EDM entity types. - /// - public Func EntityTypeNamespaceBuilder { get; set; } - - /// - /// Delegate for building names for EDM entity type. - /// - public Func EntityTypeNameBuilder { get; set; } - - /// - /// Delegate for building names for EDM entity property. - /// - public Func EntityPropertyNameBuilder { get; set; } - - /// - /// Dictionary of views to be used to load objects on updates. Used to restrict properties for performance (all props are loaded if view is not specified). - /// - private Dictionary UpdateViews { get; set; } - - private readonly PropertyInfo _keyProperty = Information.ExtractPropertyInfo(n => n.__PrimaryKey); - - /// - /// Initializes a new instance of the class. - /// - /// The list of assemblies for searching types to expose. - /// Is need to add the whole type namespace for EDM entity set. - /// A collection of pseudodetail links. - /// Additional mapping of CLR type to edm primitive type. - public DefaultDataObjectEdmModelBuilder( - IEnumerable searchAssemblies, - bool useNamespaceInEntitySetName = true, - PseudoDetailDefinitions pseudoDetailDefinitions = null, - Dictionary additionalMapping = null, - IEnumerable> updateViews = null) - { - _searchAssemblies = searchAssemblies ?? throw new ArgumentNullException(nameof(searchAssemblies), "Contract assertion not met: searchAssemblies != null"); - _useNamespaceInEntitySetName = useNamespaceInEntitySetName; - _pseudoDetailDefinitions = pseudoDetailDefinitions ?? new PseudoDetailDefinitions(); - - AdditionalMapping = additionalMapping; - - EntitySetNameBuilder = BuildEntitySetName; - EntityPropertyNameBuilder = BuildEntityPropertyName; - EntityTypeNameBuilder = BuildEntityTypeName; - EntityTypeNamespaceBuilder = BuildEntityTypeNamespace; - - if (updateViews != null) - { - SetUpdateView(updateViews); - } - } - - /// - /// Builds instance using specified assemblies. - /// - /// An instance. - public DataObjectEdmModel Build() - { - var meta = new DataObjectEdmMetadata - { - KeyPropertyName = _keyProperty.Name, - KeyProperty = _keyProperty - }; - - var typeFilter = TypeFilter ?? (t => true); - - foreach (Assembly assembly in _searchAssemblies) - { - IEnumerable dataObjectTypes = assembly - .GetTypes() - .Where(t => t.IsSubclassOf(typeof(DataObject))) - .Where(typeFilter); - - foreach (Type dataObjectType in dataObjectTypes) - { - if (!meta.Contains(dataObjectType)) - AddDataObjectWithHierarchy(meta, dataObjectType); - } - } - - if (meta[typeof(DataObject)].KeyType == null) - { - var key = meta[typeof(DataObject)].OwnProperties.FirstOrDefault(p => p.Name == _keyProperty.Name); - if (key == null) - { - throw new ArgumentException("Contract assertion not met: key != null", "value"); - } - - meta[typeof(DataObject)].OwnProperties.Clear(); - foreach (var type in meta.Types) - { - if (type.BaseType == typeof(DataObject) && meta[type].KeyType == null) - { - meta[type].OwnProperties.Add(key); - meta[type].KeyType = typeof(Guid); - } - } - } - - return new DataObjectEdmModel(meta, this); - } - - /// - /// Returns as object. - /// - /// The type of master. - /// The name of the link from master to pseudodetail (pseudoproperty). - /// An instance as object. - public object GetPseudoDetail(Type masterType, string masterToDetailPseudoProperty) - { - return _pseudoDetailDefinitions - .Where(x => x.MasterType == masterType) - .Where(x => x.MasterToDetailPseudoProperty == masterToDetailPseudoProperty) - .FirstOrDefault() - ?.PseudoDetail; - } - - /// - /// Returns instance. - /// - /// instance as object. - /// An instance. - public IPseudoDetailDefinition GetPseudoDetailDefinition(object pseudoDetail) - { - return _pseudoDetailDefinitions - .Where(x => x.PseudoDetail == pseudoDetail) - .FirstOrDefault(); - } - - /// - /// Change default views that would be used to load objects on updates. - /// - /// Should be called before MapDataObjectRoute. - /// Key - DataObject type, value - view to be used for objects of that type on updates. - private void SetUpdateView(IEnumerable> updateViews) - { - if (this.UpdateViews is null) - { - this.UpdateViews = new Dictionary(); - } - - if (updateViews != null) - { - foreach (KeyValuePair kvp in updateViews) - { - SetUpdateView(kvp.Key, kvp.Value); - } - } - } - - /// - /// Change for a specific . Update view would be used to load these objects on updates. - /// - /// Should be called before MapDataObjectRoute. - /// DataObject type for which update view would be set. - /// Update view to be used for objects of type . Setting removes update view for the type. - 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)); - } - - if (updateView is null) - { - UpdateViews.Remove(dataObjectType); - return; - } - - if (!Information.CheckViewForClasses(updateView.Name, dataObjectType)) - { - throw new ArgumentException($"View from DataObject {updateView.DefineClassType} can not be set for a DataObject of type {dataObjectType}.", nameof(updateView)); - } - - UpdateViews[dataObjectType] = updateView; - } - - /// - /// Adds the property for exposing. - /// - /// The type settings. - /// The data object property. - private static void AddProperty(DataObjectEdmTypeSettings typeSettings, PropertyInfo dataObjectProperty) - { - // Master property. - if (dataObjectProperty.PropertyType.IsSubclassOf(typeof(DataObject))) - { - var masterMetadata = new DataObjectEdmMasterSettings(dataObjectProperty.PropertyType) - { - AllowNull = dataObjectProperty.GetCustomAttribute(typeof(NotNullAttribute)) == null - }; - - typeSettings.MasterProperties.Add(dataObjectProperty, masterMetadata); - return; - } - - // Detail property. - if (dataObjectProperty.PropertyType.IsSubclassOf(typeof(DetailArray))) - { - var detailType = dataObjectProperty.PropertyType.GetProperty("Item", new[] { typeof(int) }).PropertyType; - typeSettings.DetailProperties.Add(dataObjectProperty, new DataObjectEdmDetailSettings(detailType)); - return; - } - - // Link from master to pseudodetail (pseudoproperty). - if (dataObjectProperty is PseudoDetailPropertyInfo) - { - var detailType = dataObjectProperty.PropertyType.GenericTypeArguments[0]; - typeSettings.PseudoDetailProperties.Add(dataObjectProperty, new DataObjectEdmDetailSettings(detailType)); - return; - } - - // Own property. - typeSettings.OwnProperties.Add(dataObjectProperty); - } - - /// - /// Adds the data object for exposing with its whole hierarchy. - /// - /// The metadata object. - /// The type of the data object. - private void AddDataObjectWithHierarchy(DataObjectEdmMetadata meta, Type dataObjectType) - { - // Some base class can be already added. - if (meta.Contains(dataObjectType)) - return; - - if (dataObjectType == typeof(DataObject)) - { - var dataObjectTypeSettings = meta[dataObjectType] = new DataObjectEdmTypeSettings() { KeyType = typeof(Guid), EnableCollection = true, CollectionName = EntitySetNameBuilder(dataObjectType) }; // TODO - AddProperty(dataObjectTypeSettings, typeof(DataObject).GetProperty(_keyProperty.Name)); - return; - } - - Type baseType = dataObjectType.BaseType; - if (baseType == null) - { - throw new ArgumentException("Contract assertion not met: baseType != null", nameof(dataObjectType)); - } - - AddDataObjectWithHierarchy(meta, baseType); - - // Extract user-defined update view: - View updateView = null; - if (UpdateViews != null) - { - UpdateViews.TryGetValue(dataObjectType, out updateView); - } - - var typeSettings = meta[dataObjectType] = new DataObjectEdmTypeSettings - { - EnableCollection = true, - CollectionName = EntitySetNameBuilder(dataObjectType), - DefaultView = DynamicView.Create(dataObjectType, null).View, - UpdateView = updateView, - }; - - AddProperties(dataObjectType, typeSettings); - if (typeSettings.KeyType != null) - meta[baseType].KeyType = null; - } - - private Type GetKeyType(Type dataObjectType) - { - BaseKeyGenerator generator = Activator.CreateInstance(Information.GetKeyGeneratorType(dataObjectType)) as BaseKeyGenerator; - return generator.KeyType; - } - - /// - /// Adds the properties for exposing. - /// - /// The type of the data object. - /// The type settings. - private void AddProperties(Type dataObjectType, DataObjectEdmTypeSettings typeSettings) - { - Func propertyFilter = PropertyFilter ?? (p => true); - - var keyType = GetKeyType(dataObjectType); - if (keyType == GetKeyType(dataObjectType.BaseType)) - { - typeSettings.KeyType = null; - } - else - { - if (dataObjectType.BaseType != typeof(DataObject)) - { - throw new ArgumentException($"Запрещено переопределение ключа в типе {dataObjectType.FullName}, т.к он не наследуется непосредственно от DataObject.", nameof(dataObjectType)); - } - - typeSettings.KeyType = keyType; - } - - IEnumerable properties = dataObjectType - .GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public) - .Where(propertyFilter); - - foreach (var property in properties) - { - bool overridden = false; - foreach (var method in property.GetAccessors(true)) - { - if (!method.Equals(method.GetBaseDefinition())) - { - overridden = true; - break; - } - } - - if (overridden && property.Name != _keyProperty.Name) - continue; - AddProperty(typeSettings, property); - } - - // Add the defined links from master to pseudodetail (pseudoproperties) as properties for exposing. - foreach (var definition in _pseudoDetailDefinitions.Where(x => x.MasterType == dataObjectType)) - { - var pi = new PseudoDetailPropertyInfo(definition.MasterToDetailPseudoProperty, definition.PseudoPropertyType, definition.MasterType); - AddProperty(typeSettings, pi); - } - } - - /// - /// Builds the name of the entity set. - /// Default logic, for . - /// - /// Type of the data object. - /// The name of appropriate EDM entity set. - private string BuildEntitySetName(Type dataObjectType) - { - PublishNameAttribute attr = dataObjectType.GetCustomAttribute(false); - if (attr != null && !string.IsNullOrEmpty(attr.EntitySetPublishName)) - { - return attr.EntitySetPublishName; - } - - string typeName = BuildEntityTypeName(dataObjectType); - string nameSpace = BuildEntityTypeNamespace(dataObjectType); - return string.Concat(_useNamespaceInEntitySetName ? $"{nameSpace}.{typeName}".Replace(".", string.Empty) : typeName, "s").Replace("_", string.Empty); - } - - /// - /// Builds the name of the entity. - /// Default logic, for . - /// - /// Type of the data object. - /// The name of appropriate EDM entity. - private string BuildEntityTypeName(Type dataObjectType) - { - PublishNameAttribute attr = dataObjectType.GetCustomAttribute(false); - if (attr != null) - { - int lastPos = attr.TypePublishName.LastIndexOf("."); - if (lastPos < 0) - { - return attr.TypePublishName; - } - - return attr.TypePublishName.Substring(lastPos + 1); - } - - return dataObjectType.Name; - } - - /// - /// Builds the namespace of the entity. - /// Default logic, for . - /// - /// Type of the data object. - /// The namespace of appropriate EDM entity. - private string BuildEntityTypeNamespace(Type dataObjectType) - { - PublishNameAttribute attr = dataObjectType.GetCustomAttribute(false); - if (attr != null) - { - int lastPos = attr.TypePublishName.LastIndexOf("."); - if (lastPos >= 0) - { - return attr.TypePublishName.Substring(0, lastPos); - } - - return string.Empty; - } - - return dataObjectType.Namespace; - } - - /// - /// Builds the name of the property. - /// Default logic, for . - /// - /// Property of the data object. - /// The name of appropriate EDM property. - private string BuildEntityPropertyName(PropertyInfo propertyDataObject) - { - PublishNameAttribute attr = propertyDataObject.GetCustomAttribute(true); - if (attr != null) - { - return attr.TypePublishName; - } - - return propertyDataObject.Name; - } - } -} \ No newline at end of file +namespace NewPlatform.Flexberry.ORM.ODataService.Model +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + + using ICSSoft.STORMNET; + using ICSSoft.STORMNET.KeyGen; + + using Microsoft.OData.Edm; + + /// + /// Default implementation of . + /// Builds EDM-model by list of assemblies. + /// + /// + public class DefaultDataObjectEdmModelBuilder : IDataObjectEdmModelBuilder + { + /// + /// The list of assemblies for searching types to expose. + /// + private readonly IEnumerable _searchAssemblies; + + /// + /// Is need to add the whole type namespace for EDM entity set. + /// + private readonly bool _useNamespaceInEntitySetName; + + /// + /// The list of links from master to pseudodetail (pseudoproperty) definitions. + /// + private readonly PseudoDetailDefinitions _pseudoDetailDefinitions; + + /// + /// Additional mapping of CLR type to edm primitive type. When it's required on the application side. + /// + public Dictionary AdditionalMapping { get; } + + /// + /// Delegate for additional filtering exposed types. + /// At the result EDM-model will be added only those types, for that the delegate returned true. + /// + public Func TypeFilter { get; set; } + + /// + /// Delegate for additional filtering exposed properties. + /// At the result EDM-model will be added only those properties, for that the delegate returned true. + /// + public Func PropertyFilter { get; set; } + + /// + /// Delegate for building names for EDM entity sets. + /// + public Func EntitySetNameBuilder { get; set; } + + /// + /// Delegate for building namespaces for EDM entity types. + /// + public Func EntityTypeNamespaceBuilder { get; set; } + + /// + /// Delegate for building names for EDM entity type. + /// + public Func EntityTypeNameBuilder { get; set; } + + /// + /// Delegate for building names for EDM entity property. + /// + public Func EntityPropertyNameBuilder { get; set; } + + /// + /// Dictionary of views to be used to load objects on updates. Used to restrict properties for performance (all props are loaded if view is not specified). + /// + 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); + + /// + /// Initializes a new instance of the class. + /// + /// The list of assemblies for searching types to expose. + /// Is need to add the whole type namespace for EDM entity set. + /// A collection of pseudodetail links. + /// Additional mapping of CLR type to edm primitive type. + public DefaultDataObjectEdmModelBuilder( + IEnumerable searchAssemblies, + bool useNamespaceInEntitySetName = true, + PseudoDetailDefinitions pseudoDetailDefinitions = null, + Dictionary additionalMapping = 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; + _pseudoDetailDefinitions = pseudoDetailDefinitions ?? new PseudoDetailDefinitions(); + + AdditionalMapping = additionalMapping; + + EntitySetNameBuilder = BuildEntitySetName; + EntityPropertyNameBuilder = BuildEntityPropertyName; + EntityTypeNameBuilder = BuildEntityTypeName; + EntityTypeNamespaceBuilder = BuildEntityTypeNamespace; + + if (updateViews != null) + { + SetUpdateView(updateViews); + } + + if (masterLightLoadTypes != null) + { + SetMasterLightLoadTypes(masterLightLoadTypes); + + if (masterLightLoadAllTypes) + { + System.Diagnostics.Debug.WriteLine("Detected usage of masterLightLoadAllTypes parameter together with masterLightLoadTypes in DefaultDataObjectEdmModelBuilder. masterLightLoadTypes will be ignored, all data objects will be loaded in MasterLightLoad mode."); + } + } + + this.MasterLightLoadAllTypes = masterLightLoadAllTypes; + } + + /// + /// Builds instance using specified assemblies. + /// + /// An instance. + public DataObjectEdmModel Build() + { + var meta = new DataObjectEdmMetadata + { + KeyPropertyName = _keyProperty.Name, + KeyProperty = _keyProperty + }; + + var typeFilter = TypeFilter ?? (t => true); + + foreach (Assembly assembly in _searchAssemblies) + { + IEnumerable dataObjectTypes = assembly + .GetTypes() + .Where(t => t.IsSubclassOf(typeof(DataObject))) + .Where(typeFilter); + + foreach (Type dataObjectType in dataObjectTypes) + { + if (!meta.Contains(dataObjectType)) + AddDataObjectWithHierarchy(meta, dataObjectType); + } + } + + if (meta[typeof(DataObject)].KeyType == null) + { + var key = meta[typeof(DataObject)].OwnProperties.FirstOrDefault(p => p.Name == _keyProperty.Name); + if (key == null) + { + throw new ArgumentException("Contract assertion not met: key != null", "value"); + } + + meta[typeof(DataObject)].OwnProperties.Clear(); + foreach (var type in meta.Types) + { + if (type.BaseType == typeof(DataObject) && meta[type].KeyType == null) + { + meta[type].OwnProperties.Add(key); + meta[type].KeyType = typeof(Guid); + } + } + } + + return new DataObjectEdmModel(meta, this); + } + + /// + /// Returns as object. + /// + /// The type of master. + /// The name of the link from master to pseudodetail (pseudoproperty). + /// An instance as object. + public object GetPseudoDetail(Type masterType, string masterToDetailPseudoProperty) + { + return _pseudoDetailDefinitions + .Where(x => x.MasterType == masterType) + .Where(x => x.MasterToDetailPseudoProperty == masterToDetailPseudoProperty) + .FirstOrDefault() + ?.PseudoDetail; + } + + /// + /// Returns instance. + /// + /// instance as object. + /// An instance. + public IPseudoDetailDefinition GetPseudoDetailDefinition(object pseudoDetail) + { + return _pseudoDetailDefinitions + .Where(x => x.PseudoDetail == pseudoDetail) + .FirstOrDefault(); + } + + /// + /// Change default views that would be used to load objects on updates. + /// + /// Should be called before MapDataObjectRoute. + /// Key - DataObject type, value - view to be used for objects of that type on updates. + private void SetUpdateView(IEnumerable> updateViews) + { + if (this.UpdateViews is null) + { + this.UpdateViews = new Dictionary(); + } + + if (updateViews != null) + { + foreach (KeyValuePair kvp in updateViews) + { + SetUpdateView(kvp.Key, kvp.Value); + } + } + } + + /// + /// Change for a specific . Update view would be used to load these objects on updates. + /// + /// Should be called before MapDataObjectRoute. + /// DataObject type for which update view would be set. + /// Update view to be used for objects of type . Setting removes update view for the type. + private void SetUpdateView(Type dataObjectType, View updateView) + { + if (!dataObjectType.IsSubclassOf(typeof(DataObject))) + { + 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) + { + UpdateViews.Remove(dataObjectType); + return; + } + + if (!Information.CheckViewForClasses(updateView.Name, dataObjectType)) + { + throw new ArgumentException($"View from DataObject {updateView.DefineClassType} can not be set for a DataObject of type {dataObjectType}.", nameof(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. + /// + /// The type settings. + /// The data object property. + private static void AddProperty(DataObjectEdmTypeSettings typeSettings, PropertyInfo dataObjectProperty) + { + // Master property. + if (dataObjectProperty.PropertyType.IsSubclassOf(typeof(DataObject))) + { + var masterMetadata = new DataObjectEdmMasterSettings(dataObjectProperty.PropertyType) + { + AllowNull = dataObjectProperty.GetCustomAttribute(typeof(NotNullAttribute)) == null + }; + + typeSettings.MasterProperties.Add(dataObjectProperty, masterMetadata); + return; + } + + // Detail property. + if (dataObjectProperty.PropertyType.IsSubclassOf(typeof(DetailArray))) + { + var detailType = dataObjectProperty.PropertyType.GetProperty("Item", new[] { typeof(int) }).PropertyType; + typeSettings.DetailProperties.Add(dataObjectProperty, new DataObjectEdmDetailSettings(detailType)); + return; + } + + // Link from master to pseudodetail (pseudoproperty). + if (dataObjectProperty is PseudoDetailPropertyInfo) + { + var detailType = dataObjectProperty.PropertyType.GenericTypeArguments[0]; + typeSettings.PseudoDetailProperties.Add(dataObjectProperty, new DataObjectEdmDetailSettings(detailType)); + return; + } + + // Own property. + typeSettings.OwnProperties.Add(dataObjectProperty); + } + + /// + /// Adds the data object for exposing with its whole hierarchy. + /// + /// The metadata object. + /// The type of the data object. + private void AddDataObjectWithHierarchy(DataObjectEdmMetadata meta, Type dataObjectType) + { + // Some base class can be already added. + if (meta.Contains(dataObjectType)) + return; + + if (dataObjectType == typeof(DataObject)) + { + var dataObjectTypeSettings = meta[dataObjectType] = new DataObjectEdmTypeSettings() { KeyType = typeof(Guid), EnableCollection = true, CollectionName = EntitySetNameBuilder(dataObjectType) }; // TODO + AddProperty(dataObjectTypeSettings, typeof(DataObject).GetProperty(_keyProperty.Name)); + return; + } + + Type baseType = dataObjectType.BaseType; + if (baseType == null) + { + throw new ArgumentException("Contract assertion not met: baseType != null", nameof(dataObjectType)); + } + + AddDataObjectWithHierarchy(meta, baseType); + + // Extract user-defined update view: + View updateView = null; + if (UpdateViews != null) + { + UpdateViews.TryGetValue(dataObjectType, out updateView); + } + + var typeSettings = meta[dataObjectType] = new DataObjectEdmTypeSettings + { + EnableCollection = true, + CollectionName = EntitySetNameBuilder(dataObjectType), + DefaultView = DynamicView.Create(dataObjectType, null).View, + UpdateView = updateView, + MasterLightLoad = MasterLightLoadAllTypes || (MasterLightLoadTypes?.Contains(dataObjectType) ?? false), + }; + + AddProperties(dataObjectType, typeSettings); + if (typeSettings.KeyType != null) + meta[baseType].KeyType = null; + } + + private Type GetKeyType(Type dataObjectType) + { + BaseKeyGenerator generator = Activator.CreateInstance(Information.GetKeyGeneratorType(dataObjectType)) as BaseKeyGenerator; + return generator.KeyType; + } + + /// + /// Adds the properties for exposing. + /// + /// The type of the data object. + /// The type settings. + private void AddProperties(Type dataObjectType, DataObjectEdmTypeSettings typeSettings) + { + Func propertyFilter = PropertyFilter ?? (p => true); + + var keyType = GetKeyType(dataObjectType); + if (keyType == GetKeyType(dataObjectType.BaseType)) + { + typeSettings.KeyType = null; + } + else + { + if (dataObjectType.BaseType != typeof(DataObject)) + { + throw new ArgumentException($"Запрещено переопределение ключа в типе {dataObjectType.FullName}, т.к он не наследуется непосредственно от DataObject.", nameof(dataObjectType)); + } + + typeSettings.KeyType = keyType; + } + + IEnumerable properties = dataObjectType + .GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public) + .Where(propertyFilter); + + foreach (var property in properties) + { + bool overridden = false; + foreach (var method in property.GetAccessors(true)) + { + if (!method.Equals(method.GetBaseDefinition())) + { + overridden = true; + break; + } + } + + if (overridden && property.Name != _keyProperty.Name) + continue; + AddProperty(typeSettings, property); + } + + // Add the defined links from master to pseudodetail (pseudoproperties) as properties for exposing. + foreach (var definition in _pseudoDetailDefinitions.Where(x => x.MasterType == dataObjectType)) + { + var pi = new PseudoDetailPropertyInfo(definition.MasterToDetailPseudoProperty, definition.PseudoPropertyType, definition.MasterType); + AddProperty(typeSettings, pi); + } + } + + /// + /// Builds the name of the entity set. + /// Default logic, for . + /// + /// Type of the data object. + /// The name of appropriate EDM entity set. + private string BuildEntitySetName(Type dataObjectType) + { + PublishNameAttribute attr = dataObjectType.GetCustomAttribute(false); + if (attr != null && !string.IsNullOrEmpty(attr.EntitySetPublishName)) + { + return attr.EntitySetPublishName; + } + + string typeName = BuildEntityTypeName(dataObjectType); + string nameSpace = BuildEntityTypeNamespace(dataObjectType); + return string.Concat(_useNamespaceInEntitySetName ? $"{nameSpace}.{typeName}".Replace(".", string.Empty) : typeName, "s").Replace("_", string.Empty); + } + + /// + /// Builds the name of the entity. + /// Default logic, for . + /// + /// Type of the data object. + /// The name of appropriate EDM entity. + private string BuildEntityTypeName(Type dataObjectType) + { + PublishNameAttribute attr = dataObjectType.GetCustomAttribute(false); + if (attr != null) + { + int lastPos = attr.TypePublishName.LastIndexOf("."); + if (lastPos < 0) + { + return attr.TypePublishName; + } + + return attr.TypePublishName.Substring(lastPos + 1); + } + + return dataObjectType.Name; + } + + /// + /// Builds the namespace of the entity. + /// Default logic, for . + /// + /// Type of the data object. + /// The namespace of appropriate EDM entity. + private string BuildEntityTypeNamespace(Type dataObjectType) + { + PublishNameAttribute attr = dataObjectType.GetCustomAttribute(false); + if (attr != null) + { + int lastPos = attr.TypePublishName.LastIndexOf("."); + if (lastPos >= 0) + { + return attr.TypePublishName.Substring(0, lastPos); + } + + return string.Empty; + } + + return dataObjectType.Namespace; + } + + /// + /// Builds the name of the property. + /// Default logic, for . + /// + /// Property of the data object. + /// The name of appropriate EDM property. + private string BuildEntityPropertyName(PropertyInfo propertyDataObject) + { + PublishNameAttribute attr = propertyDataObject.GetCustomAttribute(true); + if (attr != null) + { + return attr.TypePublishName; + } + + return propertyDataObject.Name; + } + } +} 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..7b152ede --- /dev/null +++ b/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/CRUD/Update/MasterLightLoadTest.cs @@ -0,0 +1,77 @@ +#if NETCOREAPP +namespace NewPlatform.Flexberry.ORM.ODataService.Tests.CRUD.Update +{ + using System; + using System.Net; + using System.Net.Http; + using ICSSoft.STORMNET; + 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 MasterLightLoadSettingTest() + { + 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); + } + }); + } + } +} +#endif diff --git a/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/CRUD/Update/ModifyDataTest.cs b/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/CRUD/Update/ModifyDataTest.cs index 1fd0c9f3..d95650f8 100644 --- a/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/CRUD/Update/ModifyDataTest.cs +++ b/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/CRUD/Update/ModifyDataTest.cs @@ -1613,160 +1613,6 @@ public void UpdateDetailAndDetailTest() }); } - /// - /// Осуществляет проверку удаления данных. - /// - [Fact] - public void DeletePlainObjectTest() - { - ActODataService(args => - { - // Создаем объект данных, который потом будем удалять, и добавляем в базу обычным сервисом данных. - Медведь agregator = new Медведь() { МедведьСтрокой = "Agregator" }; - args.DataService.UpdateObject(agregator); - - View view = new View(typeof(Медведь), View.ReadType.OnlyThatObject); - Медведь foundAgregator0 = args.DataService.Query<Медведь>(view).FirstOrDefault(x => x.__PrimaryKey == agregator.__PrimaryKey); - Assert.NotNull(foundAgregator0); - - // Формируем URL запроса к OData-сервису (с идентификатором удаляемой сущности). - string requestUrl = string.Format("http://localhost/odata/{0}({1})", args.Token.Model.GetEdmEntitySet(typeof(Медведь)).Name, agregator.__PrimaryKey); - requestUrl = requestUrl.Replace("{", string.Empty).Replace("}", string.Empty); - - // Обращаемся к OData-сервису и обрабатываем ответ. - using (HttpResponseMessage response = args.HttpClient.DeleteAsync(requestUrl).Result) - { - // Убедимся, что запрос завершился успешно (тело ответа д.б. пустым при отсутствии ошибок удаления). - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - - // Проверяем что объект данных был удален из базы. - Медведь foundAgregator = args.DataService.Query<Медведь>(view).FirstOrDefault(x => x.__PrimaryKey == agregator.__PrimaryKey); - Assert.Null(foundAgregator); - } - }); - } - - /// - /// Осуществляет проверку удаления данных, если детейл и мастер одного типа, но при этом пустые. - /// - [Fact] - public void DeleteObjectWithSameDetailAndMasterEmptyTest() - { - ActODataService(args => - { - // Создаем объект данных, который потом будем удалять, и добавляем в базу обычным сервисом данных. - AgregatorSameMD agregator = new AgregatorSameMD() { Name = "Agregator" }; - args.DataService.UpdateObject(agregator); - - View view = new View(typeof(AgregatorSameMD), View.ReadType.OnlyThatObject); - AgregatorSameMD foundAgregator0 = args.DataService.Query(view).FirstOrDefault(x => x.__PrimaryKey == agregator.__PrimaryKey); - Assert.NotNull(foundAgregator0); - - // Формируем URL запроса к OData-сервису (с идентификатором удаляемой сущности). - string requestUrl = string.Format("http://localhost/odata/{0}({1})", args.Token.Model.GetEdmEntitySet(typeof(AgregatorSameMD)).Name, agregator.__PrimaryKey); - requestUrl = requestUrl.Replace("{", string.Empty).Replace("}", string.Empty); - - // Обращаемся к OData-сервису и обрабатываем ответ. - using (HttpResponseMessage response = args.HttpClient.DeleteAsync(requestUrl).Result) - { - // Убедимся, что запрос завершился успешно (тело ответа д.б. пустым при отсутствии ошибок удаления). - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - - // Проверяем что объект данных был удален из базы. - AgregatorSameMD foundAgregator = args.DataService.Query(view).FirstOrDefault(x => x.__PrimaryKey == agregator.__PrimaryKey); - Assert.Null(foundAgregator); - } - }); - } - - /// - /// Осуществляет проверку удаления данных, если детейл и мастер одного типа? но лишь детейл заполнен. - /// - [Fact] - public void DeleteObjectWithSameDetailAndMasterTest() - { - ActODataService(args => - { - // Создаем объект данных, который потом будем удалять, и добавляем в базу обычным сервисом данных. - AgregatorSameMD agregator = new AgregatorSameMD() { Name = "Agregator" }; - args.DataService.UpdateObject(agregator); - - DetailAndMaster dm = new DetailAndMaster() { Name = "DetailAndMaster" }; - agregator.Details.Add(dm); - args.DataService.UpdateObject(dm); - - AgregatorSameMD agregator2 = new AgregatorSameMD() { Name = "Agregator2" }; - args.DataService.UpdateObject(agregator2); - DetailAndMaster dm2 = new DetailAndMaster() { Name = "DetailAndMaster2" }; - agregator2.Details.Add(dm2); - args.DataService.UpdateObject(dm2); - - View view = new View(typeof(AgregatorSameMD), View.ReadType.OnlyThatObject); - AgregatorSameMD foundAgregator0 = args.DataService.Query(view).FirstOrDefault(x => x.__PrimaryKey == agregator.__PrimaryKey); - Assert.NotNull(foundAgregator0); - - // Формируем URL запроса к OData-сервису (с идентификатором удаляемой сущности). - string requestUrl = string.Format("http://localhost/odata/{0}({1})", args.Token.Model.GetEdmEntitySet(typeof(AgregatorSameMD)).Name, agregator.__PrimaryKey); - requestUrl = requestUrl.Replace("{", string.Empty).Replace("}", string.Empty); - - // Обращаемся к OData-сервису и обрабатываем ответ. - using (HttpResponseMessage response = args.HttpClient.DeleteAsync(requestUrl).Result) - { - // Убедимся, что запрос завершился успешно (тело ответа д.б. пустым при отсутствии ошибок удаления). - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - - // Проверяем что объект данных был удален из базы. - AgregatorSameMD foundAgregator = args.DataService.Query(view).FirstOrDefault(x => x.__PrimaryKey == agregator.__PrimaryKey); - Assert.Null(foundAgregator); - } - }); - } - - /// - /// Осуществляет проверку удаления данных, если детейл и мастер одного типа, но не пустые. - /// - [Fact] - public void DeleteObjectWithSameDetailAndMaster2Test() - { - ActODataService(args => - { - // Создаем объект данных, который потом будем удалять, и добавляем в базу обычным сервисом данных. - AgregatorSameMD agregator = new AgregatorSameMD() { Name = "Agregator" }; - args.DataService.UpdateObject(agregator); - - DetailAndMaster dm = new DetailAndMaster() { Name = "DetailAndMaster" }; - agregator.Details.Add(dm); - args.DataService.UpdateObject(dm); - - AgregatorSameMD agregator2 = new AgregatorSameMD() { Name = "Agregator2" }; - args.DataService.UpdateObject(agregator2); - DetailAndMaster dm2 = new DetailAndMaster() { Name = "DetailAndMaster2" }; - agregator2.Details.Add(dm2); - args.DataService.UpdateObject(dm2); - - agregator.Master = dm2; - args.DataService.UpdateObject(agregator); - - View view = new View(typeof(AgregatorSameMD), View.ReadType.OnlyThatObject); - AgregatorSameMD foundAgregator0 = args.DataService.Query(view).FirstOrDefault(x => x.__PrimaryKey == agregator.__PrimaryKey); - Assert.NotNull(foundAgregator0); - - // Формируем URL запроса к OData-сервису (с идентификатором удаляемой сущности). - string requestUrl = string.Format("http://localhost/odata/{0}({1})", args.Token.Model.GetEdmEntitySet(typeof(AgregatorSameMD)).Name, agregator.__PrimaryKey); - requestUrl = requestUrl.Replace("{", string.Empty).Replace("}", string.Empty); - - // Обращаемся к OData-сервису и обрабатываем ответ. - using (HttpResponseMessage response = args.HttpClient.DeleteAsync(requestUrl).Result) - { - // Убедимся, что запрос завершился успешно (тело ответа д.б. пустым при отсутствии ошибок удаления). - Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); - - // Проверяем что объект данных был удален из базы. - AgregatorSameMD foundAgregator = args.DataService.Query(view).FirstOrDefault(x => x.__PrimaryKey == agregator.__PrimaryKey); - Assert.Null(foundAgregator); - } - }); - } /// /// Тест на изменение ссылки. @@ -1926,5 +1772,172 @@ public void ComplexAggregatorChangeTest() } }); } + + /// + /// Осуществляет проверку удаления данных. + /// + [Fact] + public void DeletePlainObjectTest() + { + ActODataService(args => + { + // Arrange. + // Создаем объект данных, который потом будем удалять, и добавляем в базу обычным сервисом данных. + Медведь agregator = new Медведь() { МедведьСтрокой = "Agregator" }; + args.DataService.UpdateObject(agregator); + + View view = new View(typeof(Медведь), View.ReadType.OnlyThatObject); + Медведь foundAgregator0 = args.DataService.Query<Медведь>(view).FirstOrDefault(x => x.__PrimaryKey == agregator.__PrimaryKey); + Assert.NotNull(foundAgregator0); + + // Формируем URL запроса к OData-сервису (с идентификатором удаляемой сущности). + string requestUrl = string.Format("http://localhost/odata/{0}({1})", args.Token.Model.GetEdmEntitySet(typeof(Медведь)).Name, agregator.__PrimaryKey); + requestUrl = requestUrl.Replace("{", string.Empty).Replace("}", string.Empty); + + // Act. + // Обращаемся к OData-сервису и обрабатываем ответ. + using (HttpResponseMessage response = args.HttpClient.DeleteAsync(requestUrl).Result) + { + //Assert. + // Убедимся, что запрос завершился успешно (тело ответа д.б. пустым при отсутствии ошибок удаления). + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + // Проверяем что объект данных был удален из базы. + Медведь foundAgregator = args.DataService.Query<Медведь>(view).FirstOrDefault(x => x.__PrimaryKey == agregator.__PrimaryKey); + Assert.Null(foundAgregator); + } + }); + } + + /// + /// Осуществляет проверку удаления данных, если детейл и мастер одного типа, но при этом пустые. + /// + [Fact] + public void DeleteObjectWithSameDetailAndMasterEmptyTest() + { + ActODataService(args => + { + // Arrange. + // Создаем объект данных, который потом будем удалять, и добавляем в базу обычным сервисом данных. + AgregatorSameMD agregator = new AgregatorSameMD() { Name = "Agregator" }; + args.DataService.UpdateObject(agregator); + + View view = new View(typeof(AgregatorSameMD), View.ReadType.OnlyThatObject); + AgregatorSameMD foundAgregator0 = args.DataService.Query(view).FirstOrDefault(x => x.__PrimaryKey == agregator.__PrimaryKey); + Assert.NotNull(foundAgregator0); + + // Формируем URL запроса к OData-сервису (с идентификатором удаляемой сущности). + string requestUrl = string.Format("http://localhost/odata/{0}({1})", args.Token.Model.GetEdmEntitySet(typeof(AgregatorSameMD)).Name, agregator.__PrimaryKey); + requestUrl = requestUrl.Replace("{", string.Empty).Replace("}", string.Empty); + + // Act. + // Обращаемся к OData-сервису и обрабатываем ответ. + using (HttpResponseMessage response = args.HttpClient.DeleteAsync(requestUrl).Result) + { + // Assert. + // Убедимся, что запрос завершился успешно (тело ответа д.б. пустым при отсутствии ошибок удаления). + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + // Проверяем что объект данных был удален из базы. + AgregatorSameMD foundAgregator = args.DataService.Query(view).FirstOrDefault(x => x.__PrimaryKey == agregator.__PrimaryKey); + Assert.Null(foundAgregator); + } + }); + } + + /// + /// Осуществляет проверку удаления данных, если детейл и мастер одного типа, но лишь детейл заполнен. + /// + [Fact] + public void DeleteObjectWithSameDetailAndMasterTest() + { + ActODataService(args => + { + // Arrange. + // Создаем объект данных, который потом будем удалять, и добавляем в базу обычным сервисом данных. + AgregatorSameMD agregator = new AgregatorSameMD() { Name = "Agregator" }; + args.DataService.UpdateObject(agregator); + + DetailAndMaster dm = new DetailAndMaster() { Name = "DetailAndMaster" }; + agregator.Details.Add(dm); + args.DataService.UpdateObject(dm); + + AgregatorSameMD agregator2 = new AgregatorSameMD() { Name = "Agregator2" }; + args.DataService.UpdateObject(agregator2); + DetailAndMaster dm2 = new DetailAndMaster() { Name = "DetailAndMaster2" }; + agregator2.Details.Add(dm2); + args.DataService.UpdateObject(dm2); + + View view = new View(typeof(AgregatorSameMD), View.ReadType.OnlyThatObject); + AgregatorSameMD foundAgregator0 = args.DataService.Query(view).FirstOrDefault(x => x.__PrimaryKey == agregator.__PrimaryKey); + Assert.NotNull(foundAgregator0); + + // Формируем URL запроса к OData-сервису (с идентификатором удаляемой сущности). + string requestUrl = string.Format("http://localhost/odata/{0}({1})", args.Token.Model.GetEdmEntitySet(typeof(AgregatorSameMD)).Name, agregator.__PrimaryKey); + requestUrl = requestUrl.Replace("{", string.Empty).Replace("}", string.Empty); + + // Act. + // Обращаемся к OData-сервису и обрабатываем ответ. + using (HttpResponseMessage response = args.HttpClient.DeleteAsync(requestUrl).Result) + { + // Assert. + // Убедимся, что запрос завершился успешно (тело ответа д.б. пустым при отсутствии ошибок удаления). + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + // Проверяем что объект данных был удален из базы. + AgregatorSameMD foundAgregator = args.DataService.Query(view).FirstOrDefault(x => x.__PrimaryKey == agregator.__PrimaryKey); + Assert.Null(foundAgregator); + } + }); + } + + /// + /// Осуществляет проверку удаления данных, если детейл и мастер одного типа, но не пустые. + /// + [Fact] + public void DeleteObjectWithSameDetailAndMaster2Test() + { + ActODataService(args => + { + // Arrange. + // Создаем объект данных, который потом будем удалять, и добавляем в базу обычным сервисом данных. + AgregatorSameMD agregator = new AgregatorSameMD() { Name = "Agregator" }; + args.DataService.UpdateObject(agregator); + + DetailAndMaster dm = new DetailAndMaster() { Name = "DetailAndMaster" }; + agregator.Details.Add(dm); + args.DataService.UpdateObject(dm); + + AgregatorSameMD agregator2 = new AgregatorSameMD() { Name = "Agregator2" }; + args.DataService.UpdateObject(agregator2); + DetailAndMaster dm2 = new DetailAndMaster() { Name = "DetailAndMaster2" }; + agregator2.Details.Add(dm2); + args.DataService.UpdateObject(dm2); + + agregator.Master = dm2; + args.DataService.UpdateObject(agregator); + + View view = new View(typeof(AgregatorSameMD), View.ReadType.OnlyThatObject); + AgregatorSameMD foundAgregator0 = args.DataService.Query(view).FirstOrDefault(x => x.__PrimaryKey == agregator.__PrimaryKey); + Assert.NotNull(foundAgregator0); + + // Формируем URL запроса к OData-сервису (с идентификатором удаляемой сущности). + string requestUrl = string.Format("http://localhost/odata/{0}({1})", args.Token.Model.GetEdmEntitySet(typeof(AgregatorSameMD)).Name, agregator.__PrimaryKey); + requestUrl = requestUrl.Replace("{", string.Empty).Replace("}", string.Empty); + + // Act. + // Обращаемся к OData-сервису и обрабатываем ответ. + using (HttpResponseMessage response = args.HttpClient.DeleteAsync(requestUrl).Result) + { + // Assert. + // Убедимся, что запрос завершился успешно (тело ответа д.б. пустым при отсутствии ошибок удаления). + Assert.Equal(HttpStatusCode.NoContent, response.StatusCode); + + // Проверяем что объект данных был удален из базы. + AgregatorSameMD foundAgregator = args.DataService.Query(view).FirstOrDefault(x => x.__PrimaryKey == agregator.__PrimaryKey); + Assert.Null(foundAgregator); + } + }); + } } } diff --git a/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/CRUD/Update/UpdateViewsTest.cs b/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/CRUD/Update/UpdateViewsTest.cs index b7a9f6ec..e9ffd221 100644 --- a/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/CRUD/Update/UpdateViewsTest.cs +++ b/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/CRUD/Update/UpdateViewsTest.cs @@ -1,78 +1,78 @@ -#if NETCOREAPP -namespace NewPlatform.Flexberry.ORM.ODataService.Tests.CRUD.Update -{ - using System; - using System.Net; - using System.Net.Http; - - using ICSSoft.STORMNET; - using NewPlatform.Flexberry.ORM.ODataService.Tests.Extensions; - using NewPlatform.Flexberry.ORM.ODataService.Tests.Helpers; - - using Xunit; - using Xunit.Abstractions; - - /// - /// Тесты для проверки работы UpdateViews - представлений, которые используются вместо представления по умолчанию при обновлении объекта через OData. - /// Для запуска OData backend используется модифицированная версия Startup - , которая задаёт UpdateView для Берлоги и Медведя. - /// - public class UpdateViewsTest : BaseODataServiceIntegratedTest - { - /// - /// Конструктор по-умолчанию. - /// - /// Фабрика для приложения. - /// Вывод диагностической информации по тестам. - public UpdateViewsTest(CustomWebApplicationFactory factory, ITestOutputHelper output) - : base(factory, output) - { - - } - - /// - /// Проверка работы UpdateView - случай, когда в UpdateView не включен мастер. - /// - [Fact] - public void UpdateViewNoMastersTest() - { - ActODataService(args => - { - // Создаем объекты данных, которые потом будем обновлять, и добавляем в базу обычным сервисом данных. - Медведь медведь1 = new Медведь { ПорядковыйНомер = 1 }; - Медведь медведь2 = new Медведь { ПорядковыйНомер = 2 }; - args.DataService.UpdateObject(медведь1); - args.DataService.UpdateObject(медведь2); - - Берлога берлога1 = new Берлога { Наименование = "Берлога1", Медведь = медведь1 }; - Берлога берлога2 = new Берлога { Наименование = "Берлога2", Медведь = медведь1 }; - Берлога берлога3 = new Берлога { Наименование = "Берлога3", Медведь = медведь1 }; - args.DataService.UpdateObject(берлога1); - args.DataService.UpdateObject(берлога2); - args.DataService.UpdateObject(берлога3); - - // Обновляем ссылку. - берлога1.Медведь = медведь2; - - // Представление, по которому будем обновлять. - string[] берлогаPropertiesNames = - { - Information.ExtractPropertyPath<Берлога>(x => x.__PrimaryKey), - }; - var берлогаDynamicView = new View(new ViewAttribute("берлогаDynamicView", берлогаPropertiesNames), typeof(Берлога)); - - // Преобразуем объект данных в JSON-строку. - string requestJsonData = берлога1.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, берлога1); - - // Сейчас обновление мастеров не поддерживается. - Assert.ThrowsAsync(() => args.HttpClient.PatchAsJsonStringAsync(requestUrl, requestJsonData)); // Если падает Exception, значит представление поменялось и работает. - }); - } - } -} -#endif +#if NETCOREAPP +namespace NewPlatform.Flexberry.ORM.ODataService.Tests.CRUD.Update +{ + using System; + using System.Net; + using System.Net.Http; + + using ICSSoft.STORMNET; + using NewPlatform.Flexberry.ORM.ODataService.Tests.Extensions; + using NewPlatform.Flexberry.ORM.ODataService.Tests.Helpers; + + using Xunit; + using Xunit.Abstractions; + + /// + /// Тесты для проверки работы UpdateViews - представлений, которые используются вместо представления по умолчанию при обновлении объекта через OData. + /// Для запуска OData backend используется модифицированная версия Startup - , которая задаёт UpdateView для Берлоги и Медведя. + /// + public class UpdateViewsTest : BaseODataServiceIntegratedTest + { + /// + /// Конструктор по-умолчанию. + /// + /// Фабрика для приложения. + /// Вывод диагностической информации по тестам. + public UpdateViewsTest(CustomWebApplicationFactory factory, ITestOutputHelper output) + : base(factory, output) + { + + } + + /// + /// Проверка работы UpdateView - случай, когда в UpdateView не включен мастер. + /// + [Fact] + public void UpdateViewNoMastersTest() + { + ActODataService(args => + { + // Создаем объекты данных, которые потом будем обновлять, и добавляем в базу обычным сервисом данных. + Медведь медведь1 = new Медведь { ПорядковыйНомер = 1 }; + Медведь медведь2 = new Медведь { ПорядковыйНомер = 2 }; + args.DataService.UpdateObject(медведь1); + args.DataService.UpdateObject(медведь2); + + Берлога берлога1 = new Берлога { Наименование = "Берлога1", Медведь = медведь1 }; + Берлога берлога2 = new Берлога { Наименование = "Берлога2", Медведь = медведь1 }; + Берлога берлога3 = new Берлога { Наименование = "Берлога3", Медведь = медведь1 }; + args.DataService.UpdateObject(берлога1); + args.DataService.UpdateObject(берлога2); + args.DataService.UpdateObject(берлога3); + + // Обновляем ссылку. + берлога1.Медведь = медведь2; + + // Представление, по которому будем обновлять. + string[] берлогаPropertiesNames = + { + Information.ExtractPropertyPath<Берлога>(x => x.__PrimaryKey), + }; + var берлогаDynamicView = new View(new ViewAttribute("берлогаDynamicView", берлогаPropertiesNames), typeof(Берлога)); + + // Преобразуем объект данных в JSON-строку. + string requestJsonData = берлога1.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, берлога1); + + // Сейчас обновление мастеров не поддерживается. + Assert.ThrowsAsync(() => args.HttpClient.PatchAsJsonStringAsync(requestUrl, requestJsonData)); // Если падает Exception, значит представление поменялось и работает. + }); + } + } +} +#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/Startups/MasterLightLoadTestStartup.cs b/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/Startups/MasterLightLoadTestStartup.cs new file mode 100644 index 00000000..907a7343 --- /dev/null +++ b/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/Startups/MasterLightLoadTestStartup.cs @@ -0,0 +1,74 @@ +#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)); + + var masterLightLoadTypes = new List { typeof(Котенок) }; // <- set MasterLightLoad property for this DataObject + var modelBuilder = new DefaultDataObjectEdmModelBuilder(assemblies, false, pseudoDetailDefinitions, masterLightLoadTypes: masterLightLoadTypes); + + var token = builder.MapDataObjectRoute(modelBuilder); + + container.RegisterInstance(typeof(ManagementToken), token); + }); + } + } +} +#endif diff --git a/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/Startups/UpdateViewsTestStartup.cs b/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/Startups/UpdateViewsTestStartup.cs index 85785963..e788f9ed 100644 --- a/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/Startups/UpdateViewsTestStartup.cs +++ b/Tests/NewPlatform.Flexberry.ORM.ODataService.Tests/Startups/UpdateViewsTestStartup.cs @@ -1,77 +1,77 @@ -#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 UpdateView configuration - a view that is used instead of a default view during data object updates. - /// Differs from TestStartup that it sets UpdateView for and data objects. - /// - public class UpdateViewsTestStartup : Startup - { - /// - /// Initialize new instance of TestStartup. - /// - /// Configuration for new instance. - public UpdateViewsTestStartup(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)); - var updateViews = new Dictionary() // setting updateViews for testing - { - { typeof(Медведь), Медведь.Views.МедведьUpdateView }, - { typeof(Берлога), Берлога.Views.БерлогаUpdateView }, - }; - var modelBuilder = new DefaultDataObjectEdmModelBuilder(assemblies, false, pseudoDetailDefinitions, updateViews: updateViews); - - var token = builder.MapDataObjectRoute(modelBuilder); - - container.RegisterInstance(typeof(ManagementToken), token); - }); - } - } -} -#endif +#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 UpdateView configuration - a view that is used instead of a default view during data object updates. + /// Differs from TestStartup that it sets UpdateView for and data objects. + /// + public class UpdateViewsTestStartup : Startup + { + /// + /// Initialize new instance of TestStartup. + /// + /// Configuration for new instance. + public UpdateViewsTestStartup(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)); + var updateViews = new Dictionary() // setting updateViews for testing + { + { typeof(Медведь), Медведь.Views.МедведьUpdateView }, + { typeof(Берлога), Берлога.Views.БерлогаUpdateView }, + }; + var modelBuilder = new DefaultDataObjectEdmModelBuilder(assemblies, false, pseudoDetailDefinitions, updateViews: updateViews); + + var token = builder.MapDataObjectRoute(modelBuilder); + + container.RegisterInstance(typeof(ManagementToken), token); + }); + } + } +} +#endif