Одним из преимуществ .NET бэкенда мобильных сервисов в Azure является наличие встроенной поддержки не только SQL Database (SQL Azure), но и других хранилищ данных.
При использовании node.js вы можете отказаться от работы с SQL и использовать другие возможные хранилища (например, как это написано в статье Криса Райзнера об Azure Table Storage), но эта функциональность не является встроенной, поэтому придется написать некоторое количество кода самостоятельно.
При использовании .NET большая часть функций для работы с хранилищами, отличными от SQL, уже интегрирована, поэтому нет необходимости создавать «фиктивные» таблицы, как в node.js, только для возможности отправлять запросы к данным.
В этой статье я расскажу о поддержке MongoDB и о том, как можно создавать таблицы, CRUD-операции с которыми будут осуществляться напрямую с коллекцией MongoDB.
Настройка базы данных
Если у вас уже есть MongoDB аккаунт, то можете пропустить этот шаг (запомните строку подключения — она понадобится нам позднее).
В этой статье я буду использовать коллекцию под названием «orders», если такой коллекции не существует, то создавать ее самостоятельно не нужно — бэкэнд создаст ее автоматически.
Для тех кто начинает с нуля, поясняю: в этой статье используется база данных Mongo Labs, которая доступна на портале Microsoft Azure бесплатно (ограниченная версия). Для создания аккаунта переходим на портал Azure, далее нажимаем «New» -> «Store» и выбираем дополнение MongoLab, после чего регистрируем свой аккаунт.
Когда аккаунт будет настроен, нажимаем кнопку «Connection info» для того, чтобы получить URI, необходимый для соединения с базой данных. Сохраним его. Имя вашего аккаунта будет именем базы данных, которую мы будем использовать позднее.
База данных Mongo настроена; нам не нужно создавать коллекцию, так как она будет создана, когда мы первый раз попробуем получить доступ к ней.
Настройка сервиса
В Visual Studio нет возможности создания проекта с бэкэндом, использующим что-то отличное от Entity Framework, поэтому создадим пустой web-проект. Мы начнем с того, что я делал в моей предыдущей статье о создании .NET бэкэнда с нуля, но вместо добавления NuGet-пакета Azure Mobile Services .NET Backend Entity Framework, мы добавим пакет Azure Mobile Services .NET Backend Mongo.
Еще добавим пакет Microsoft.Owin.Host.SystemWeb, нужный для того, чтобы у нас появилась возможность локального запуска для облегчения процесса отладки.
После установки обоих пакетов (и их зависимостей) добавим инициализирующий статический класс WebApiConfig с методом Register по умолчанию:
public static class WebApiConfig
{
public static void Register()
{
ServiceConfig.Initialize(new ConfigBuilder());
}
}
Добавим глобальный класс в приложение для локального вызова инициализатора:
public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
WebApiConfig.Register();
}
}
Определим объектную модель, которая будет храниться в коллекции базы данных. Определим класс «Order», содержащий список элементов.
public class Order : DocumentData
{
public DateTime OrderDate { get; set; }
public string Client { get; set; }
public List<OrderItem> Items { get; set; }
}
public class OrderItem
{
public string Name { get; set; }
public double Quantity { get; set; }
public double Price { get; set; }
}
Модель данных должна реализовывать интерфейс
ItableData
также, как это делается для моделей данных Entity Framework.Для реализации этого интерфейса в Entity Framework мы используем базовый класс EntityData
,
который при использовании MongoDB аналогичен классу DocumentData
.
После этого нам остается только определить в классе модели свойства предметной области. Определение таблиц
Таблицы для MongoDB на самом деле похожи на EF. Операции могут реализовываться по тому же сценарию, исключая только те ситуации, когда нам нужна реализация нового управляющего класса предметной области, для которого мы можем использовать класс MongoDomainManager, доступный из пакета Azure Mobile Services .NET Backend Mongo.
Обратите внимание, что для исполнения операций вы всегда можете использовать типы напрямую из драйвера MongoDB (или любого другого Mongo клиента) для реализации операций, но для общих сценариев необходимую реализацию обеспечивает базовый класс TableController<T>.
public class OrderController : TableController<Order>
{
protected override void Initialize(HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
var connStringName = "mongodb";
var dbName = "MyMongoLab";
var collectionName = "orders";
this.DomainManager = new MongoDomainManager<Order>(connStringName, dbName, collectionName, this.Request, this.Services);
}
public IQueryable<Order> GetAllOrders()
{
return base.Query();
}
public Order GetOneOrder(string id)
{
var result = base.Lookup(id).Queryable.FirstOrDefault();
if (result == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
else
{
return result;
}
}
public Task<Order> PostOrder(Order order)
{
return base.InsertAsync(order);
}
public Task DeleteOrder(string id)
{
return base.DeleteAsync(id);
}
public Task<Order> PatchOrder(string id, Delta<Order> patch)
{
return base.UpdateAsync(id, patch);
}
}
Первый параметр в конструкторе
MongoDomainManager
— имя элемента из раздела <connectionStrings> в конфигурации, который содержит фактическую строку подключения к базе данных (позже мы можем добавить функцию для передачи фактической строки подключения в конструктор). Добавим соответствующий раздел в файл web.config (используем строку подключения, полученную на портале Azure):
<connectionStrings>
<add name="mongodb" connectionString="mongodb://MyMongoLab:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-@dsNNNNNN.mongolab.com:PPPPP/MyMongoLab"/>
</connectionStrings>
Теперь проект должен запуститься.
Тестирование сервиса
Протестируем сервис. Чтобы отправлять запросы сервису, воспользуемся Fiddler.
Во-первых, посмотрим, что нам возвращает GET:
GET http://localhost:54524/tables/order HTTP/1.1
User-Agent: Fiddler
Host: localhost:54524
=-=-=-=-=-=-=-=-=-
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 2
Content-Type: application/json; charset=utf-8
Expires: 0
Server: Microsoft-IIS/8.0
X-Powered-By: ASP.NET
Date: Mon, 14 Apr 2014 15:43:31 GMT
[]
Ничего неожиданного (кроме того, что мы уже имеем коллекцию «orders»).
Добавим в нашу коллекцию пару элементов:
POST http://localhost:54524/tables/order HTTP/1.1
User-Agent: Fiddler
Host: localhost:54524
Content-Length: 211
Content-Type: application/json
{
"client":"John Doe",
"orderDate":"2014-04-13T00:00:00Z",
"items":[
{ "name": "bread", "quantity": 1, "price": 1.99 },
{ "name": "milk", "quantity": 2, "price": 2.99 }
]
}
=-=-=-=-=-=-=-=-=-
HTTP/1.1 200 OK
Content-Length: 383
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
X-Powered-By: ASP.NET
Date: Mon, 14 Apr 2014 15:53:13 GMT
{
"orderDate": "2014-04-13T00:00:00Z",
"client": "John Doe",
"items": [
{
"name": "bread",
"quantity": 1.0,
"price": 1.99
},
{
"name": "milk",
"quantity": 2.0,
"price": 2.99
}
],
"id": "534c0469f76e1e10c4703c2b",
"__createdAt": "2014-04-14T15:53:12.982Z",
"__updatedAt": "2014-04-14T15:53:12.982Z"
}
И еще один:
POST http://localhost:54524/tables/order HTTP/1.1
User-Agent: Fiddler
Host: localhost:54524
Content-Length: 216
Content-Type: application/json
{
"client":"Jane Roe",
"orderDate":"2014-02-22T00:00:00Z",
"items":[
{ "name": "nails", "quantity": 100, "price": 3.50 },
{ "name": "hammer", "quantity": 1, "price": 12.34 }
]
}
=-=-=-=-=-=-=-=-=-
HTTP/1.1 200 OK
Content-Length: 387
Content-Type: application/json; charset=utf-8
Server: Microsoft-IIS/8.0
X-Powered-By: ASP.NET
Date: Mon, 14 Apr 2014 15:53:21 GMT
{
"orderDate": "2014-02-22T00:00:00Z",
"client": "Jane Roe",
"items": [
{
"name": "nails",
"quantity": 100.0,
"price": 3.5
},
{
"name": "hammer",
"quantity": 1.0,
"price": 12.34
}
],
"id": "534c0471f76e1e10c4703c2c",
"__createdAt": "2014-04-14T15:53:21.557Z",
"__updatedAt": "2014-04-14T15:53:21.557Z
}
Отправим еще один GET запрос, чтобы проверить результат:
GET http://localhost:54524/tables/order HTTP/1.1
User-Agent: Fiddler
Host: localhost:54524
=-=-=-=-=-=-=-=-=-
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 239
Content-Type: application/json; charset=utf-8
Expires: 0
Server: Microsoft-IIS/8.0
X-Powered-By: ASP.NET
Date: Mon, 14 Apr 2014 15:55:12 GMT
[
{
"id": "534c0469f76e1e10c4703c2b",
"client": "John Doe",
"orderDate": "2014-04-13T00:00:00Z"
},
{
"id": "534c0471f76e1e10c4703c2c",
"client": "Jane Roe",
"orderDate": "2014-02-22T00:00:00Z"
}
]
Мы получили добавленные ранее элементы, но не получили сложное свойство (список items) в объекте.
Проблема заключается в том, что тип возвращаемого значения функции (IQueryable Order) возвращает комплексные типы, только если это явно указано в запросе (через параметр $expand=<propertyName>).
Полезно иметь метод, возвращающий объект типа queryable, потому что это дополнительно позволяет использовать фильтрацию и сортировку (через параметры $filter и $orderby).
Поэтому мы должны принять решение, стоит ли продолжать использовать queryable и отправлять параметр $expand для возвращения сложных типов или лучше перейти к другому возвращаемому типу.
В последнем случае изменение довольно простое:
public List<Order> GetAllOrders()
{
return base.Query().ToList();
}
Существует несколько вариантов для формирования запросов. Простейший (на сервере) – сделать так, чтобы клиент отправлял $expand параметр в заголовке, и тогда на сервере ничего менять не потребуется.
Отправляем запрос и получаем назад весь документ:
GET http://localhost:54524/tables/order?$expand=items HTTP/1.1
User-Agent: Fiddler
Host: localhost:54524
=-=-=-=-=-=-=-=-=-
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 663
Content-Type: application/json; charset=utf-8
Expires: 0
Server: Microsoft-IIS/8.0
X-Powered-By: ASP.NET
Date: Mon, 14 Apr 2014 17:52:26 GMT
[
{
"id": "534c0469f76e1e10c4703c2b",
"client": "John Doe",
"orderDate": "2014-04-13T00:00:00Z",
"items": [
{
"name": "bread",
"quantity": 1.0,
"price": 1.99
},
{
"name": "milk",
"quantity": 2.0,
"price": 2.99
}
]
},
{
"id": "534c0471f76e1e10c4703c2c",
"client": "Jane Roe",
"orderDate": "2014-02-22T00:00:00Z",
"items": [
{
"name": "nails",
"quantity": 100.0,
"price": 3.5
},
{
"name": "hammer",
"quantity": 1.0,
"price": 12.34
}
]
}
]
Другой вариант – использовать атрибут action filter, который изменит входящий запрос так, чтобы к запросу постоянно добавлялся параметр $expand.
Ниже приведена одна из возможных реализаций:
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
class ExpandPropertyAttribute : ActionFilterAttribute
{
string propertyName;
public ExpandPropertyAttribute(string propertyName)
{
this.propertyName = propertyName;
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
base.OnActionExecuting(actionContext);
var uriBuilder = new UriBuilder(actionContext.Request.RequestUri);
var queryParams = uriBuilder.Query.TrimStart('?').Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries).ToList();
int expandIndex = -1;
for (var i = 0; i < queryParams.Count; i++)
{
if (queryParams[i].StartsWith("$expand", StringComparison.Ordinal))
{
expandIndex = i;
break;
}
}
if (expandIndex < 0)
{
queryParams.Add("$expand=" + this.propertyName);
}
else
{
queryParams[expandIndex] = queryParams[expandIndex] + "," + propertyName;
}
uriBuilder.Query = string.Join("&", queryParams);
actionContext.Request.RequestUri = uriBuilder.Uri;
}
}
И после того, как мы пометим наш метод этим атрибутом:
[ExpandProperty("Items")]
public IQueryable<Order> GetAllOrders()
{
return base.Query();
}
Мы можем отправлять запросы, которые используют другие queryable атрибуты, но вместе с тем возвращают все элементы объекта.
GET http://localhost:54524/tables/order?$orderby=client HTTP/1.1
User-Agent: Fiddler
Host: localhost:54524
=-=-=-=-=-=-=-=-=-
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 663
Content-Type: application/json; charset=utf-8
Expires: 0
Server: Microsoft-IIS/8.0
X-Powered-By: ASP.NET
Date: Mon, 14 Apr 2014 18:37:27 GMT
[
{
"id": "534c0471f76e1e10c4703c2c",
"client": "Jane Roe",
"orderDate": "2014-02-22T00:00:00Z",
"items": [
{
"name": "nails",
"quantity": 100.0,
"price": 3.5
},
{
"name": "hammer",
"quantity": 1.0,
"price": 12.34
}
]
},
{
"id": "534c0469f76e1e10c4703c2b",
"client": "John Doe",
"orderDate": "2014-04-13T00:00:00Z",
"items": [
{
"name": "bread",
"quantity": 1.0,
"price": 1.99
},
{
"name": "milk",
"quantity": 2.0,
"price": 2.99
}
]
}
]
Развертывание
Теперь, когда сервис запускается локально, все готово для его публикации в Azure.
После загрузки профиля публикации с портала, правой кнопкой мыши кликаем на проект в VS и выбираем «Publish» — сервис будет опубликован.
И, если мы опять используем Fiddler, мы должны будем получить два элемента «order» прямо из Azure:
GET http://blog20140413.azure-mobile.net/tables/order HTTP/1.1
User-Agent: Fiddler
Host: blog20140413.azure-mobile.net
=-=-=-=-=-=-=-=-=-
HTTP/1.1 500 Internal Server Error
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 43
Content-Type: application/json; charset=utf-8
Expires: 0
Server: Microsoft-IIS/8.0
X-Powered-By: ASP.NET
Date: Mon, 14 Apr 2014 18:50:22 GMT
{
"message": "An error has occurred."
}
Что-то пошло не так. По умолчанию среда исполнения не возвращает детали ошибок (в целях безопасности), поэтому мы можем проверить лог-файлы на портале и посмотреть, что произошло. Ошибка будет здесь:
Exception=System.ArgumentException: No connection string named 'mongodb' could be found in the service configuration.
at Microsoft.WindowsAzure.Mobile.Service.MongoDomainManager`1.GetMongoContext(String connectionStringName)
at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
at Microsoft.WindowsAzure.Mobile.Service.MongoDomainManager`1..ctor(String connectionStringName, String databaseName, String collectionName, HttpRequestMessage request, ApiServices services)
at MongoDbOnNetBackend.OrderController.Initialize(HttpControllerContext controllerContext)
at System.Web.Http.ApiController.ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken)
at System.Web.Http.Dispatcher.HttpControllerDispatcher.SendAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Web.Http.Dispatcher.HttpControllerDispatcher.d__0.MoveNext(), Id=6133b3eb-9851-4
Проблема в том, что локальный файл web.config, используемый при запуске сервиса локально, не подходит при запуске сервиса в облаке. Нам нужно определить строку подключения другим способом.
К сожалению, из-за этой ошибки у нас нет простой возможности определить строку подключения (портал позволил бы сделать это легко, но пока этой функции там нет), поэтому мы используем обходной путь.
Для этого зайдем на портал в раздел мобильных сервисов и на вкладке «Configure» добавим новый app setting, чьим значением является строка подключения, которую мы определили в файле web.config:
После инициализации контроллера таблицы поменяем строку подключения в настройках сервиса, основываясь на том значении, которое мы получили из настроек приложения.
static bool connectionStringInitialized = false;
private void InitializeConnectionString(string connStringName, string appSettingName)
{
if (!connectionStringInitialized)
{
connectionStringInitialized = true;
if (!this.Services.Settings.Connections.ContainsKey(connStringName))
{
var connFromAppSetting = this.Services.Settings[appSettingName];
var connSetting = new ConnectionSettings(connStringName, connFromAppSetting);
this.Services.Settings.Connections.Add(connStringName, connSetting);
}
}
}
protected override void Initialize(HttpControllerContext controllerContext)
{
var connStringName = "mongodb";
var dbName = "MyMongoLab";
var collectionName = "orders";
// Workaround for lack of connection strings in the portal
InitializeConnectionString(connStringName, "mongoConnectionString");
base.Initialize(controllerContext);
this.DomainManager = new MongoDomainManager<Order>(connStringName, dbName, collectionName, this.Request, this.Services);
}
Теперь, если мы снова развернем сервис, у нас должна будет появиться возможность получать данные таблицы из Azure:
GET http://blog20140413.azure-mobile.net/tables/order HTTP/1.1
User-Agent: Fiddler
Host: blog20140413.azure-mobile.net
x-zumo-application: cOFQkbaAmffuVRBJRpYDKHbNHbtDYG97
=-=-=-=-=-=-=-=-=-
HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Length: 663
Content-Type: application/json; charset=utf-8
Expires: 0
Server: Microsoft-IIS/8.0
X-Powered-By: ASP.NET
Date: Mon, 14 Apr 2014 19:21:11 GMT
[
{
"id": "534c0469f76e1e10c4703c2b",
"client": "John Doe",
"orderDate": "2014-04-13T00:00:00Z",
"items": [
{
"name": "bread",
"quantity": 1.0,
"price": 1.99
},
{
"name": "milk",
"quantity": 2.0,
"price": 2.99
}
]
},
{
"id": "534c0471f76e1e10c4703c2c",
"client": "Jane Roe",
"orderDate": "2014-02-22T00:00:00Z",
"items": [
{
"name": "nails",
"quantity": 100.0,
"price": 3.5
},
{
"name": "hammer",
"quantity": 1.0,
"price": 12.34
}
]
}
]
Напоследок отмечу, что при локальном запуске сервиса по умолчанию не осуществляется аутентификация, поэтому наш запрос может не отправлять ключи. При отправке запроса к серверу в Azure, нужно указать application key (уровень аутентификации по умолчанию) в заголовке «x-zumo-application».
Заключение
.NET бэкэнд для мобильных сервисов Azure предлагает набор провайдеров хранилищ для абстракции табличных данных.
Так как большинство существующих примеров описывают работу с Entity Framework (SQL сервер), то надеюсь, что данный пост позволил вам узнать, как использовать провайдер MongoDB для хранения данных.
И как обычно, мы приветствуем комментарии и советы в блоге, на форумах MSDN или в twitter @AzureMobile.
Полезные ссылки
Бесплатный 30-дневный триал Microsoft Azure;
Бесплатный доступ к ресурсам Microsoft Azure для стартапов, партнеров, преподавателей, подписчиков MSDN;
Центр разработки Microsoft Azure (azurehub.ru) – сценарии, руководства, примеры, рекомендации по выбору сервисов и разработке на Microsoft Azure;
Последние новости Microsoft Azure — Twitter.com/windowsazure_ru.
Сообществе Microsoft Azure на Facebook. Здесь вы найдете экспертов, фотографии и много новостей.
Обучающие курсы виртуальной академии Microsoft (MVA)
Загрузить бесплатную или пробную Visual Studio 2013