Windows Azure Mobiles Services C# Backend (Version FR)
Bonjour à tous;
J’ai eu la chance de présenter, pendant les Techdays 2014, une avant première de la possibilité de créer un backend Windows Azure Mobile Services en C# , tandis que mon collègue Benjamin Talmard présentait une avant première de l’intégration de l’authentification Azure Directory dans Windows Azure Mobile Services.
Quelques jours plus tard, Scott Guthrie a fait l’annonce (entre autres) de ces nouvelles fonctionnalités.
Je vous propose aujourd’hui de revenir plus en profondeur sur l’utilisation de ce service local C#.
Pour rappel, quelques pointeurs sur la présentation du backend C# :
- Scott Guthrie : https://weblogs.asp.net/scottgu/archive/2014/02/20/azure-expressroute-dedicated-networking-web-site-backup-restore-mobile-services-net-support-hadoop-2-2-and-more.aspx
- MSDN : https://www.windowsazure.com/en-us/documentation/articles/mobile-services-dotnet-backend-windows-store-dotnet-get-started/
- Tutorial : https://www.windowsazure.com/en-us/documentation/articles/mobile-services-windows-store-get-started/
Pour accompagner cet article, vous trouverez en pièce jointe une application Windows 8 complète accompagnée du Backend associé : FabrikamFiberArticle.zip
Création du backend sur Azure et déploiement
Cette partie est déjà largement reprise dans les articles et tutoriaux MSDN, je vais donc me consacrer spécifiquement au modèle de développement du projet Backend C# et du client Windows 8.
Le projet Backend C#
Le projet Backend C#, récupéré depuis Windows Azure Mobile Services, contient au final plusieurs éléments:
- WebApiConfig : Configuration Web API de notre backend avec notamment l’initialisation de la base de données.
- ServiceTicketController et ImageController : contrôleur de type Table : Equivalent à l’accès direct aux tables dans la version Node.js
- CustomerController : contrôleur de type ApiController : Equivalent à l’utilisation des API depuis Node.js
- FabrikamFiberContext : Database context, modèle Code First.
- Customer, Image, ServiceTicket : Objets représentant chacun une entité (et finalement une table).
Nous allons détailler les deux types principaux de contrôleurs : TableController et ApiController.
TableController
Les classes dérivant de TableController permettent d’intéragir directement avec les tables SQL.
Si l’on compare avec la version Node.js on pourrait rapprocher les TableController avec les scripts de données node.js :
Un TableController contient principalement trois objets :
- FabrikamFiberContext : Il s’agit du contexte EF DbContext permettant de requêter les données.
- EntityDomainManager : Il s’agit d’un helper implémentant IDomainManager de haut niveau permettant de simplifier les appels “classiques” CRUD à vos tables SQL.
- Services : Classe de services ApiServices proposant divers services comme le Log d’informations ou le système de Push.
FabrikamFiberContext context = new FabrikamFiberContext(Services.Settings.Name.Replace('-', '_'));
DomainManager = new EntityDomainManager<ServiceTicket>(context, Request, Services);
Chaque appel est nomenclaturé pour intercepter les différentes requêtes HTTP :
// GET tables/ServiceTicket
public IQueryable<ServiceTicket> GetAllServiceTickets()
{
return Query();
}
// GET tables/ServiceTicket/48D68C86-6EA6-4C25-AA33-223FC9A27959
public SingleResult<ServiceTicket> GetServiceTicket(string id)
{
return Lookup(id);
}
// PATCH tables/ServiceTicket/48D68C86-6EA6-4C25-AA33-223FC9A27959
public Task<ServiceTicket> PatchServiceTicket(string id, Delta<ServiceTicket> patch)
{
return UpdateAsync(id, patch);
}
// POST tables/ServiceTicket/48D68C86-6EA6-4C25-AA33-223FC9A27959
public async Task<IHttpActionResult> PostServiceTicket(ServiceTicket item)
{
ServiceTicket current = await InsertAsync(item);
return CreatedAtRoute("Tables", new { id = current.Id }, current);
}
// DELETE tables/ServiceTicket/48D68C86-6EA6-4C25-AA33-223FC9A27959
public Task DeleteServiceTicket(string id)
{
return DeleteAsync(id);
}
IDomainManager
Un TableController contient systématiquement un objet héritant de IDomainManager.
Pour simplifier, on peut dire que :
- TableController : S’assure de la structure de l’objet (ITableData) Intercepte les requêtes HTTP, contient les services de log, configuration etc …
- IDomainManager : Gère l’ensemble des services CRUD de votre gestionnaire de données.
public interface IDomainManager<TData> where TData : class, ITableData
{
Task<bool> DeleteAsync(string id);
Task<TData> InsertAsync(TData data);
SingleResult<TData> Lookup(string id);
Task<SingleResult<TData>> LookupAsync(string id);
IQueryable<TData> Query();
Task<IEnumerable<TData>> QueryAsync(ODataQueryOptions query);
Task<TData> ReplaceAsync(string id, TData data);
Task<TData> UpdateAsync(string id, Delta<TData> patch);
}
On peut donc imaginer créer d’autres DomainManager pour accéder à d’autres gestionnaires de données, comme MongoDB, TableStorage, SQLite etc …
Aujourd’hui, vous avez à disposition deux classes héritant IDomainManager :
- EntityDomainManager : Permet de gérer des tables SQL Azure.
- MappedEntityDomainManager : Permet de gérer des tables SQL Azure avec un modèle différent (où les tables ne correspondent pas aux entités)
ApiController
Les éléments héritant d’ApiController correspondent aux API sous Node.js :
ApiController permet de spécialiser vos services. Vous n’accédez pas directement à vos tables, mais à des services POST ou GET (ou autres) que vous spécifiez.
Pour travailler avec ApiController, vous n’avez besoin d’utiliser qu’un objet DbContext. Inutile d’utiliser un IDomainManager.
Vous pouvez aussi utiliser les systèmes de routing (RoutePrefix et Route) pour spécifier chaque url :
[RoutePrefix("api/Customers")]
public class CustomerController : ApiController
{
public ApiServices ApiServices { get; set; }
FabrikamFiberContext context;
protected override void Initialize(HttpControllerContext controllerContext)
{
base.Initialize(controllerContext);
context = new FabrikamFiberContext(ApiServices.Settings.Name.Replace('-', '_'));
}
[RequiresAuthorization(AuthorizationLevel.Application)]
public void Get()
{
ApiServices.Log.Error("Trying access API with GET ");
this.Request.CreateBadRequestResponse("Get Customers not allowed. Try /all ");
}
[Route("all")]
[RequiresAuthorization(AuthorizationLevel.Application)]
public IQueryable<Customer> GetAll()
{
return context.Customers;
}
}
Notez que dans cet exemple j’interdis explicitement l’usage de l’url GET direct en renvoyant une Bad Request
Pour aller plus loin, voici un exemple d’une méthode Merge d’une entité :
[Route("merge")]
[RequiresAuthorization(AuthorizationLevel.Application)]
public Customer MergeCustomer(Delta<Customer> patch)
{
Customer current;
// Get partial entity and the Id
var tmp = patch.GetEntity();
if (tmp == null)
{
ApiServices.Log.Error("Trying Merge customer is in error : Entity not valid ");
throw new HttpResponseException
(this.Request.CreateBadRequestResponse("Entity is not valid"));
}
var customerId = tmp.Id;
if (string.IsNullOrEmpty(customerId))
{
// get entity
current = patch.GetEntity();
// Insert new customer
if (String.IsNullOrEmpty(current.Id) || current.Id == Guid.Empty.ToString())
current.Id = Guid.NewGuid().ToString();
context.Customers.Add(current);
ApiServices.Log.Info("Customer created with Id : " + current.Id.ToString());
}
else
{
current = context.Customers.Find(new[] { customerId });
if (current == null)
{
// insert customer
context.Customers.Add(current);
ApiServices.Log.Info("Customer created with Id : " + current.Id.ToString());
}
else
{
// update original
patch.Patch(current);
ApiServices.Log.Info("Customer updated with Id : " + current.Id.ToString());
}
}
// Check properties
if (String.IsNullOrEmpty(current.FirstName) || string.IsNullOrEmpty(current.LastName))
{
ApiServices.Log.Warn("FirstName and LastName are mandatory for merging a customer");
throw new HttpResponseException
(this.Request.CreateBadRequestResponse("FirstName and LastName are mandatory"));
}
// save entity
context.SaveChanges();
return current;
}
A noter l’utilisation de :
- Delta<T> : Permet de passer un objet non complet. Utile lorsqu’on veut mettre à jour seulement le nom d’un client, sans passer toutes les autres propriétés.
- Delta<T>.GetEntity() : Permet de récupérer l’entité complète de l’objet Delta<T>
- Delta<T>.Patch(item) : Permet de fusionner deux entités. Pratique pour l’update.
- ApiServices.Log() : Permet de loguer les différentes actions.
Vous trouverez le code complet dans l’exemple fournit (fichier CustomerController.cs)
Quelques astuces
Récupérer les informations Utilisateurs
Que ce soit depuis un TableController ou un ApiController, récupérer les informations utilisateurs se fait de manière simple. La petite astuce consiste à bien caster vers le type ServiceUser :
ServiceUser user = (ServiceUser)this.User;
var level = user.Level;
var identities = user.Identities;
Renvoyer une entité personnalisée
Ici on peut imaginer vouloir renvoyer une entité dont on ne connait pas la structure. Pour générer une entité personnalisée, nous allons utiliser grâce à la librairie JSON.NET, l’objet JObject permettant de décrire notre entité.
Dans l’exemple suivant, nous renvoyons le détails utilisateur. Notez l’utilisation d’un objet JsonSerializerSettings pour définir les conventions :
private JObject GetUserDetails()
{
ServiceUser user = (ServiceUser)this.User;
JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings()
{
DefaultValueHandling = DefaultValueHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
string json = JsonConvert.SerializeObject(user.Identities, Formatting.None, jsonSerializerSettings);
JArray identities = JArray.Parse(json);
return new JObject
{
{ "id", user.Id },
{ "level", user.Level.ToString() },
{ "identities", identities }
};
}
Renvoyer une collection d’entités personnalisées
Cette fois ci, c’est l’objet JArray qui va nous permettre de renvoyer un tableau complet d’entités personnalisées :
[Route("fields")]
[RequiresAuthorization(AuthorizationLevel.Application)]
public JArray GetCustomerFields()
{
JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings()
{
DefaultValueHandling = DefaultValueHandling.Ignore,
NullValueHandling = NullValueHandling.Ignore,
ContractResolver = new CamelCasePropertyNamesContractResolver()
};
var customers = from c in context.Customers select new { Id = c.Id, LastName = c.LastName, FirstName = c.FirstName };
string json = JsonConvert.SerializeObject(customers, Formatting.None, jsonSerializerSettings);
JArray customersArray = JArray.Parse(json);
return customersArray;
}
Client Windows 8.1 / Windows Phone 8
Coté client, l’utilisation de l’API MobileServices reste exactement la même que votre backend soit codé sous Node.js ou en C#:
Dans l’exemple fourni, vous trouverez dans la classe DataService, qui contient tout le code nécessaire pour accéder à votre backend.
Initialisation de l’objet MobileService :
// This MobileServiceClient has been configured to communicate with your local
// test project for debugging purposes. Comment out this declaration and use the one
// provided below when you are done developing and deploy your service to the cloud.
MobileService = new MobileServiceClient("https://localhost.fiddler:59217");
// This MobileServiceClient has been configured to communicate with your Mobile Service's url
// and application key. You're all set to start working with your Mobile Service!
// public static MobileServiceClient MobileService = new MobileServiceClient(
//MobileService = new MobileServiceClient(
// "https://fabrikamfiber.azure-mobile.net/",
// "QMizgjEBYjDOWWpsqseCLLoqMralUv88"
// );
Accéder à une table :
ticketTable = Context.Current.MobileService.GetTable<ServiceTicket>();
return await ticketTable.LookupAsync(id);
Accéder à une API personnalisée :
var c = await Context.Current
.MobileService
.InvokeApiAsync<List<Customer>>("Customers/all", HttpMethod.Get, null);
Bon code !
https://www.dotmim.com/SiteFiles/FabrikamFiberArticle.zip
Comments
- Anonymous
May 22, 2014
Bonjour, Excellent article! Merci beaucoup! Partant de là, voici mon scénario: 1 AMS avec son service .Net backend, authentification des ustilisateurs et stockage de leur UserId dans les tables coté serveur. Imaginons que j'ai défini une table CustomElements dans l'api. Mon but serait de synchroniser en mode offline dans une table SQLite grace au dernier sdk d'azure uniquement les éléments de l'utilisateur. Pas tous les éléments de la dite table, uniquement ceux dont l'userid correspond à celui qui est logué. Hors, je vois que le sdk fourni uniquement une méthode GetSyncTable<T>(). Une solution envisageable serait donc de créer une Table et son TableController dans l'api du genre MyCustomElements qui resterait vide et dont le controller renverait les données non pas de la table MyCustomElements mais bien celles filtrées par UserId depuis CustomElements. Mais est-ce possible et n'y a t il pas une solution plus propre qui permettrait de créer un Controller custom qui se chargerait de faire le filtrage et qui servirait à la synchronissation? Quelque chose du genre IMobileServiceSyncTable _myCustomElements = await Context.Current.MobileService.InvokeApiAsync<List<MyCustomElement>>("MyCustomElements/all", HttpMethod.Get, null); Merci beaucoup.