[Перевод] Используем MongoDB в облачном бэкенде мобильных приложений
Одним из преимуществ .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
public class OrderController: TableController
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=
Полезно иметь метод, возвращающий объект типа queryable, потому что это дополнительно позволяет использовать фильтрацию и сортировку (через параметры $filter и $orderby).
Поэтому мы должны принять решение, стоит ли продолжать использовать queryable и отправлять параметр $expand для возвращения сложных типов или лучше перейти к другому возвращаемому типу.
В последнем случае изменение довольно простое:
public List
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
=-=-=-=-=-=-=-=-=- 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
=-=-=-=-=-=-=-=-=- 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