Создание клиент-серверной модели данных для обмена данными между клиентами, написанными на FLUTTER и сервером ASP.NET
Данная утилита позволяет быстро создавть клиент-серверное кроссплатформенное приложение.
Результатом генерации являются:
- проект сервера ASP.NET на языке C#
- модели данных для клиентского приложения Flutter на языке Dart
- Непосредственно генерацию осуществляет данный проект (nsg_generator https://github.com/zenalex/nsg_generator.git)
- Создайте или подготовьте следующие проекты: Flutter - для генерации клиента, ASP.NET - для генерации сервера, Windows Forms - метаданные (при необходимости)
- В проекте Flutter создаем папку сonfig (название любое) вне папки lib (для того, чтобы файлы в ней не были включены в конечный проект). В этой папке (будем называеть её config) будем создавать файлы-описатели классов и вызываемых функций в формате json (кодировка utf-8)
Генерируемые классы на стороне сервера являются "оболочкой" для классов библиотеки, создаваемой при помощи NsgConfigurator. Данную библиотеку классов (приложение Windows Forms) здесь мы называем метаданными
Все данные файлы создаются в папке config и имеют формат json. generation_config.json - Основной файл, содержащий описание всех генерируемых типов данных.
generation_config.json
{
"targetFramework": "net472",
"cSharpPath": "C:/Users/SergeiFdrv/source/repos/MyApp/Server",
"cSharpNamespace": "TechControlServer",
"dartPath": "C:/Users/SergeiFdrv/source/repos/MyApp/Client/lib/model",
"applicationName": "my_app",
"useStaticDatabaseNames": "true",
"controller": [
{
"apiPrefix": "Api",
"className": "DataController",
"useAuthorization": "true",
"dataType": "NsgDataItem",
"serverUri": "http://127.0.0.1:5000",
"method": [
{
"name": "BrandItem",
"description": "Марка",
"apiPrefix": "Brand",
"authorize": "user",
"getterType": "post",
"dataTypeFile": "brand_item.json",
"allowPost": "true",
"allowDelete": "true"
},
{
"name": "ModelItem",
"description": "Модель",
"authorize": "user",
"getterType": "post",
"dataTypeFile": "model_item.json",
"allowPost": "true",
"allowDelete": "true"
},
{
"name": "SizeItem",
"description": "Размеры",
"authorize": "user",
"getterType": "post",
"dataTypeFile": "size_item.json",
"allowPost": "true",
"allowDelete": "true"
},
{
"name": "OrderItem",
"description": "Заказ",
"apiPrefix": "Order",
"authorize": "user",
"getterType": "post",
"dataTypeFile": "order_item.json",
"allowPost": "true",
"allowDelete": "true"
},
{
"name": "OrderTableItem",
"description": "Заказ.Таблица",
"allowGetter": "false",
"dataTypeFile": "order_table_item.json"
},
{
"name": "ToolsItem",
"description": "Инструменты",
"authorize": "user",
"getterType": "post",
"dataTypeFile": "tools_item.json",
"allowPost": "true",
"allowDelete": "true"
}
],
"functions": [
{
"name": "GetBrandOfModel",
"description": "Получение бренда, выпускающего данную модель",
"type": "Reference<BrandItem>",
"authorize": "user",
"params": [
{
"name": "modelId",
"type": "String"
},
{
"name": "someDate",
"type": "DateTime"
},
{
"name": "someString",
"type": "String"
},
{
"name": "someInt",
"type": "int"
}
]
}
]
}
],
"enums": [
{
"className": "EDaysOfWeek",
"description": "Дни недели",
"dataTypeFile": "e_days_of_week.json"
},
{
"className": "ERole",
"description": "Роль",
"dataTypeFile": "e_role.json"
},
{
"className": "EPriority",
"description": "Приоритет",
"dataTypeFile": "e_priority.json"
}
]
}
Версия .NET, под которую будет создана серверная часть. По умолчанию равна net5.0
"targetFramework": "net5.0",
Путь к папке проекта .NET для генерации серверной части
"cSharpPath": "X:/Path/",
.NET namespace для генерации классов
"cSharpNamespace": "namespace",
Путь к папке проекта FLUTTER для генерации классов
"dartPath": "X:/Path",
Генерация серверной части. По умолчанию true
"doCSharp": "false",
Генерация клиентской части. По умолчанию true
"doDart": "false",
На стороне сервера использовать названия полей объектов метаданных из статического класса Names (Например, Контрагенты.Names.Наименование
вместо "Наименование"
). По умолчанию false
"useStaticDatabaseNames": "true",
Массив генерируемых контроллеров
"controller": []
Массив генерируемых перечислений
"enums": []
Префикс для вызова web-api
"apiPrefix": "Api",
Имя класса контроллера. По умолчанию "DataController"
"className": "DataController",
Имя класса, содержащего реализации функций контроллера. По умолчанию className + "Implementation"
"implControllerName": "DataControllerImplementation",
Имя класса, реализующего функции аутентификации. По умолчанию "AuthControllerImplementation"
"implAuthControllerName": "AuthControllerImplementation",
Тип контроллера (пока единственный из доступных)
"dataType": "NsgDataItem",
Uri сервера
"serverUri": "http://server.name:5000",
Использует ли контроллер проверку пользователей
"useAuthorization": "true",
Использует ли клиент метрику (вызывать на клиентской части NsgMetrica.activate())
"useMetrics": "true",
[не используется]
"uploadEnabled": "true",
Требует ли контроллер авторизации пользователей. По умолчанию true
"loginRequired": "true",
Нужно ли создавать данный контроллер в клиентской части. По умолчанию true
"writeOnClient": "true",
Функция переключения между серверами на клиенте. По умолчанию false
"enableServerSwitch": "true",
Нужно ли заменить на сервере вызов await GetUserByToken синхронным методом GetUserByTokenSync. По умолчанию false
"useGetUserByTokenSync": "true",
Пример применения: назначение CurrentCulture в асинхронном контексте работает только до выхода из асихнронного контекста. Поэтому, чтобы назначать локаль по токену пользователя в GetUserByToken, его необходимо сделать синхронным
Массив методов работы с объектами данных. Для каждого элемента будет наздан набор методов
"method": []
Имя класса данных
"name": "UserDataItem",
Описание класса данных для чтения человеком
"description" : "User data item",
Префикс web-api для запроса операций чтения и записи с данным классом. По умолчанию равен name
"apiPrefix": "UserDataItem",
Уровень прав, требуемый для обращения к данным:
- anonymous
- user
"authorize": "user",
Тип HTTP-запроса на чтение (GET или POST)
"getterType": "get",
Имя файла описания структуры полей данного класса
"dataTypeFile": "userItem.json",
Генерировать метод для чтения. По умолчанию true
"allowGetter": "false"
Генерировать метод для записи
"allowPost": "true"
Генерировать метод для удаления
"allowDelete": "true"
Функции
"functions": []
Сервер позволяет управлять настройками пользователей. Для этого необходимо добавить метод под названием UserSettings
и указать файл объекта с привязкой к объекту метаданных.
Требуемые поля:
- идентификатор (ключевое поле, тип String или Guid)
- Name (тип String)
- Settings (тип String)
- UserId (тип String или Guid)
Для UserSettings всегда создаются методы GET, POST и DELETE
(WIP)
Можно настроить синхронизацию объектов между клиентов и сервером. Для этого необходимо в описании объекта задать "isDestributed": "true"
, добавить метод под названием ExchangeRules и табличную часть ExchangeRulesMergingTable
и указать файлы объектов с привязкой к объектам метаданных.
Требуемые поля в ExchangeRules:
- идентификатор (ключевое поле, тип String или Guid)
- objectType (тип String)
- lastExchangeDate (тип DateTime)
- periodicity (тип int)
- priorityForClient (тип bool)
- mergingRules (тип List<ExchangeRulesMergingTable>)
Требуемые поля в ExchangeRulesMergingTable:
- идентификатор (ключевое поле, тип String или Guid)
- fieldName (тип String)
- priorityForClient (тип bool)
Для ExchangeRules всегда создаются методы GET, POST и DELETE
Имя функции
"name": "UserData",
Типы метода HTTP. Поддерживаются GET и POST. Можно через запятую: "get, post"
. По умолчанию "post"
"apiType": "get",
Вместо apiType можно указать поддержку методов по отдельности. По умолчанию httpGet = false, httpPost = true
"httpGet": "true",
"httpPost": "true",
Тип выходных данных функции. Если Image или Binary,
функция будет создана только на сервере и параметры будут читаться из URI (например, function?id=123
)
"type": "int",
Для ссылочных типов - тип генерируемого объекта
"referenceType": "DayOfWeek",
Для ссылочных типов - возвращаемый тип может быть равен null. По умолчанию true
"isNullable": "false",
Отображение прогресса. По умолчанию true
"useProgressDialog": "false",
Сколько попыток соединения нужно предпринимать. По умолчанию 3
"retryCount": 10,
Уровень прав, требуемый для обращения к функции:
- anonymous
- user
"authorize": "user",
Префикс для вызова web-api. По умолчанию равен name
"apiPrefix": "GetUserData",
Описание функции для чтения человеком
"description" : "Get user data",
Текст загрузочного окна во время запроса
"dialogText" : "Getting user data...",
Входные параметры функции
"params": []
Имя параметра
"name": "UserData",
Тип данных параметра
"type": "int",
Для ссылочных типов - тип генерируемого объекта
"referenceType": "DayOfWeek",
Имя класса перечисления
"className": "DaysOfWeek",
Описание перечисления для чтения человеком
"description": "Дни недели",
Имя файла описания структуры полей данного перечисления
"dataTypeFile": "days_of_week.json"
model_item.json
{
"typeName": "ModelItem",
"databaseType": "Модель",
"databaseTypeNamespace": "MyApp.Метаданные.Мониторинг",
"fields": [
{
"name": "Id",
"databaseName": "Идентификатор",
"type": "String",
"isPrimary": "true"
},
{
"name": "Name",
"databaseName": "Наименование",
"type": "String"
},
{
"name": "BrandId",
"description": "Марка",
"databaseName": "Владелец",
"type": "Reference",
"referenceName": "Brand",
"referenceType": "BrandItem"
},
{
"name": "Category",
"databaseName": "Категория",
"type": "Reference",
"referenceType": "CategoryItem"
},
{
"name": "OriginCountry",
"databaseName": "СтранаПроисхождения",
"type": "Reference<CategoryItem>"
},
{
"name": "DesignedBy",
"databaseName": "КемРазработана",
"type": "UntypedReference<PersonItem, CompanyItem>"
},
{
"name": "Specs",
"databaseName": "Характеристики",
"type": "List<Reference>",
"referenceType": "ModelItemSpecTableRow"
},
{
"name": "AdditionalOptions",
"databaseName": "ДополнитенльныеОпции",
"type": "List<ModelItemOptionTableRow>"
},
{
"name": "IsFolder",
"databaseName": "ЭтоГруппа",
"type": "bool"
},
{
"name": "ParentId",
"databaseName": "ИдентификаторРодителя",
"type": "Guid"
}
]
}
Название класса генерируемого объекта
"typeName": "TeamItem",
Предопределенная сущность объекта. Возможные варианты:
- userSettings - класс для хранения настроек пользователя;
- exchangeRules - правила обмена;
- exchangeRulesMergingTable - табличная часть exchangeRules.
Если не указана, будет сгенерирован обычный класс данных
"entityType": "userSettings",
Разрешить наследование от этого объекта. По умолчанию false
"allowExtend": "true",
Для наследуемых (базовых) объектов - название строкового поля для записи свойств наследника
"additionalDataField": "AdditionalProperties",
Для наследуемых (базовых) объектов - название строкового поля для записи типа наследника
"extensionTypeField": "ExtensionType",
Для наследующих (производных) объектов - название базового типа
"extends": "BannerItemElementTable",
Описание поля для чтения человеком. По умолчанию равен databaseType
"description" : "Some item",
Имя типа объекта метаданных (при наличии)
"databaseType": "Команды",
.NET namespace объекта метаданных (при наличии)
"databaseTypeNamespace": "TechControl.Метаданные.Мониторинг",
Перегрузка методов ToString()
"presentation": "{Name} ({LicensePlate})",
Максимальное число объектов, возвращаемое сервером. По умолчанию 100
"maxHttpGetItems": 100,
Стандартное название поля для поиска по периоду для клиентской части
"periodFieldName": "DateDocument",
Стандартное название поля даты последнего изменения для серверной части.
Если задано, перед записью проверяется время предыдущей записи данного объекта. Если за время редактирования объект был записан параллельно, запись будет отменена, сервер вернет ошибку
"lastEditedFieldName": "DateUpdated",
На стороне сервера использовать названия полей объектов метаданных из статического класса Names (Например, Контрагенты.Names.Наименование
вместо "Наименование"
). По умолчанию false
"useStaticDatabaseNames": "true",
Это распределенный объект. По умолчанию false
"isDestributed": "true",
Является ли объект строкой табличной части. По умолчанию - true, если databaseType оканчивается на ".Строка", иначе - false
"isTableRow": "true",
Поля объекта
"fields": []
Имя поля (в C# имя поля не может совпадать с названием класса)
"name": "Id",
Тип поля
"type": "String",
Имя поля в объекте метаданных (при наличии)
"databaseName": "Идентификатор",
Макс. длина строки или значение числа. По умолчанию 0 (без ограничения)
"maxLength": 50,
Описание поля для чтения человеком. По умолчанию равен databaseName
"description" : "Some value",
Если тип поля Image (изображение), для его получения нужен префикс api. По умолчанию равен name
"apiPrefix": "pic0"
Признак ключевого поля
"isPrimary": "true"
Если тип - генерируемый объект, можно указать имя ссылки для Flutter. Значение не должно совпадать с name и referenceName других полей данного объекта. Также в C# имя поля не может совпадать с названием класса. По умолчанию равен name без "Id" в конце
"referenceName": "Position",
Для ссылочных типов - тип генерируемого объекта
"referenceType": "TeamItemMembersTable",
Для нетипизированных ссылок - тип генерируемого объекта
"referenceTypes": [ "BrandItem", "ModelItem" ],
Для нетипизированных ссылок - тип генерируемого объекта по умолчанию.
Если типы указаны в угловых скобках (<>
), равен первому из них.
По умолчанию равен referenceType
или первому элементу referenceTypes
"defaultReferenceType": "TeamItem",
Можно указывать человекочитаемые имена полей. По умолчанию false
"userVisibility": "true",
"userName": "The Field!",
Генерировать поле на клиенте. По умолчанию true
"writeOnClient": "false",
Генерировать поле на сервере. По умолчанию true
"writeOnServer": "false",
Нужно ли при Post заполнять данное поле значением, полученным от клиента. По умолчанию true
"allowPost": "false",
e_days_of_week.json
{
"values": [
{
"codeName": "Monday",
"name": "Понедельник",
"value": "0"
},
{
"codeName": "Tuesday",
"name": "Вторник",
"value": "1"
},
{
"codeName": "Wednesday",
"name": "Среда",
"value": "2"
},
{
"codeName": "Thursday",
"name": "Четверг",
"value": "3"
},
{
"codeName": "Friday",
"name": "Пятница",
"value": "4"
},
{
"codeName": "Saturday",
"name": "Суббота",
"value": "5"
},
{
"codeName": "Sunday",
"name": "Воскресенье",
"value": "6"
}
]
}
Список возможных значений
"values": []
Имя для кода
"codeName": "Unknown",
Имя для человека. По умолчанию равно codeName
"name": "Неизвестно",
Числовой индекс
"value": "0"
тип | Описание |
---|---|
String |
строка |
int |
целое число |
bool |
логическое |
double |
вещественное число (в метаданных используется decimal) |
DateTime |
дата и время |
Reference |
ссылка на объект |
UntypedReference |
нетипизированная ссылка на объект |
List<Reference> |
список объектов (в метаданных - табличная часть) |
Enum |
перечисление |
Image |
изображение |
Binary |
двоичные данные |
Поле referenceType имеет смысл, если type:
- объект (
"type": "Reference"
) - список объектов (
"type": "List<Reference>"
) - перечисление (
"type": "Enum"
)
Альтернативная форма записи type без referenceType:
- объект (
"type": "Reference<
тип>"
) - нетипизированная ссылка (
"type": "UntypedReference<
типы через запятую>"
) - список объектов (
"type": "List<
тип>"
) - перечисление (
"type": "Enum<
тип>"
)
Реализовано наследование объектов друг от друга.
Допустим, есть табличная часть и нужно, чтобы в каждой строке был свой набор данных в зависимости от свойств каждого элемента.
- Прописываем в объекте свойство
"allowExtend": "true"
и заполняем"extensionTypeField"
, т. о. разрешаем делать его базовым. - Создаем новые объекты, в свойство
"extends"
записываем название базового типа.
В производные типы можно добавлять новые поля. Для этого в описании базового типа необходимо прописать "additionalDataField"
.
При сохранении объекта свойства будут записаны в поле, указанное в "additionalDataField"
в виде JSON.
Важные замечания:
Копировать поля базового типа в производные типы НЕ НУЖНО! (кажется очевидным, но на всякий случай)
Названия полей не должны повторяться. В частности, в производном типе не должно быть полей, одноименных полям базового типа. Переопределения (override) генерируемых полей нет
Генерацию можно запустить прямо из консоли PowerShell:
dart bin\nsgCodeGenerator.dart C:\GeneratorConfig [-csharp] [-dart] [-force|-overwrite|-forceoverwrite] [csharp:] [dart:]
, где
bin\nsgCodeGenerator.dart
- путь к точке входа программы,C:\GeneratorConfig
- путь к папке config
Путь к папке config всегда идет первым агрументом. Все остальные - в произвольном порядке.
- Генерировать только C# (если не указано также
-dart
)
-csharp
- Генерировать только Dart (если не указано также
-csharp
)
-dart
- Принудительная перезапись (без этого аргумента при повторной генерации некоторые файлы не перезаписываются)
-force
-overwrite
-forceoverwrite
- Если в папке найден файл .csproj, скопировать его
-copyCsproj
- Если в папке найден файл Program.cs, скопировать его
-copyProgramCs
- Если в папке найден файл Startup.cs, скопировать его
-copyStartupCs
- Не переспрашивать. По умолчанию при использовании forceOverwrite потребуется подтверждение перезаписи
-dontAsk
- Указать путь для генерации проекта C# (вместо указанного в generation_config.json)
csharp:C:\Server
- Указать путь для генерации файлов Dart (вместо указанного в generation_config.json)
dart:C:\Client\lib
Альтернативный метод запуска генерации не требует ввода всех параметров в консоль. В модуле test_main.dart есть метод main, где прописаны настройки и класс Project с несколькими реализациями.
Шаблон описания проекта:
static Project ИмяПроекта = Project(
'путь к папке с JSON-файлами',
'путь для генерации проекта C#',
'путь для генерации файлов Dart');
Если путь для генерации пуст, он будет взят из generation_config.json.
Если не нужно генерировать одну из частей, в конструктор Project можно передать doCSharp: false
или doDart: false
.
В VS Code над методом main появляются кнопки Run и Debug.
Откройте test_main.dart, пропишите проект, при необходимости измените настройки и запустите метод main