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# :

Pour accompagner cet article, vous trouverez en pièce jointe une application Windows 8 complète accompagnée du Backend associé : FabrikamFiberArticle.zip

image image

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:

image

  • 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 :

image

Un TableController contient principalement trois objets :

  1. FabrikamFiberContext : Il s’agit du contexte EF DbContext permettant de requêter les données.
  2. EntityDomainManager : Il s’agit d’un helper implémentant IDomainManager de haut niveau permettant de simplifier les appels “classiques” CRUD à vos tables SQL.
  3. 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 :

  1. TableController : S’assure de la structure de l’objet (ITableData) Intercepte les requêtes HTTP, contient les services de log, configuration etc …
  2. 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 :

  1. EntityDomainManager : Permet de gérer des tables SQL Azure.
  2. 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 :

image

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 :

  1. 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.
  2. Delta<T>.GetEntity() : Permet de récupérer l’entité complète de l’objet Delta<T>
  3. Delta<T>.Patch(item) : Permet de fusionner deux entités. Pratique pour l’update.
  4. 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