Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Abonnements

Service WCF orienté données Agile avec EF4 et “faux” provider LINQ

Contrairement à l’approche de Julie, je suis parti sur une approche Self-Tracking Entities et T4 à fond.

La première étape consiste créer un projet que nous appellerons DAL et dans lequel, on va intégrer un edmx.

Dans mon exemple, je suis parti sur Northwind avec seulement les tables Customers, Orders et [Order Details].

image

Une fois cela fait, on va rajouter un nouvel item de type ADO.NET Self-Tracking Entity Generator à notre projet.

Cela aura pour effet de générer deux templates de génération de code T4 :

  • un qui contiendra les entités + des classes / interfaces destinées au Self-Tracking

image

  • un qui contiendra notre contexte + une classe d’extension methods.

Nous allons déplacer notre template d’entités dans un projet dédié : Entities.

Après avoir rajouté la référence vers Entities à notre projet DAL, nous allons définir dans nos templates le chemin vers l’edmx.

Ensuite, nous allons définir un projet Repositories. Dans ce projet, nous allons définir une interface INorthwindRepository.

 

image

Vous pouvez remarquer la redondance de INorthwindRepository. C’est pourquoi, dans mon exemple, ces deux interfaces sont générées avec un template T4 se basant sur mon edmx. Ainsi, même si je change de modèle de données, je n’aurai rien à changer, juste à regénérer le code de mes templates.

Maintenant, je vais revenir sur notre contexte. En effet, je vais lui faire implémenter INorthwindRepository. Pour cela, je vais modifier le template T4 de manière à obtenir la classe suivante :

image

Qui dit service WCF dit Service Contract. Je vais donc me créer deux nouveaux projets : Services et ServiceContracts (basés tous les deux sur T4).

image

image

Je rappelle qu’à ce moment là, si vous avez déjà les templates T4 (réutilisables d’un projet sur l’autre), vous n’avez toujours pas écrit une ligne de code.

On peut constater l’indépendance des assemblies vis-à-vis de Entity Framework (à l’exception de la couche DAL bien sûr) :

image

Voulant utiliser Unity, j’ai ensuite choisi l’approche proposée par Alexey Zakharov. J’ai donc intégrer son projet WCFFacility et les classes Bootstrapper et UnityServiceLocatorAdapter dans mon projet WCFService. Une fois le svc et le fichier de config définit, mon service est terminé !

 

Maintenant côté client, je voulais rajouter l’utilisation d’un “faux” provider LINQ. Pour cela, je me suis basé sur ce que j’avais déjà fait précédemment lors d’une pres pour la communauté ALT.NET française.

Je me suis d’abord créé un projet Client.LINQ dans lequel j’ai défini deux classes : ClientLINQ et MyQueryable.

public static class ClientLINQ
{
    public static MyQueryable<T> Where<T>(this MyQueryable<T> source, Expression<Func<T, bool>> where)
    {
        source.WhereValue = string.Concat(source.WhereValue ?? "", where.Body.ToString().Replace(string.Format("{0}.", where.Parameters[0].Name), "it.").Replace("\"", "'").Replace("||", " OR ").Replace("&&", " AND "));
        return source;
    }

    public static MyQueryable<T> OrderBy<T, T2>(this MyQueryable<T> source, Expression<Func<T, T2>> orderBy)
    {
        source.OrderByValue = orderBy.Body.ToString().Replace(string.Format("{0}.", orderBy.Parameters[0].Name), "it.");
        return source;
   
 
    public static MyQueryable<T> Include<T>(this MyQueryable<T> source, string include)
    {
        source.IncludeValues.Add(include);
        return source;
   
 
    public static MyQueryable<T> Skip<T>(this MyQueryable<T> source, int number)
    {
        source.SkipValue = number;
        return source;
   

    public static MyQueryable<T> Take<T>(this MyQueryable<T> source, int number)
    {
        source.TakeValue = number;
        return source;
   

   
public static T FirstOrDefault<T>(this MyQueryable<T> source)
    {
        source.TakeValue = 1;
        return source.AsEnumerable().FirstOrDefault();
    }

    public static T First<T>(this MyQueryable<T> source)
    {
        source.TakeValue = 1;
        return source.AsEnumerable().First();
   

   
public static MyQueryable<T> ToMyQueryable<T>(this IEnumerable<T> source)
    {
        var value = source as MyQueryable<T>;
        if (value == null)
            value = new MyQueryable<T>(source);
        return value;
    }
}

public class MyQueryable<T> : IEnumerable<T>
{
    public MyQueryable()
    {
    }

   
public MyQueryable(IEnumerable<T> enumerable)
    {
        Enumerable = enumerable;
   

    public IEnumerable<T> Enumerable { get; set; } 

    public IEnumerator<T> GetEnumerator()
    {
        return Enumerable.GetEnumerator();
    }
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    private bool _allEntities = true;
    public bool AllEntities
    {
        get { return _allEntities; }
        internal set
        {
            _allEntities = value;
            if (value)
            {
                IncludeValues.Clear();
                WhereValue = null;
                OrderByValue = null;
                SkipValue = null;
                TakeValue = null;
            }
        }
    }
    private ObservableCollection<string> _includeValues;
    public ObservableCollection<string> IncludeValues
    {
        get
        {
            if (_includeValues == null)
            {
                _includeValues = new ObservableCollection<string>();
                _includeValues.CollectionChanged += (sender, e) =>
                    {
                        if (e.Action == NotifyCollectionChangedAction.Add)
                            AllEntities = false;
                    };
            }
            return _includeValues;
        }
    }
    private string _whereValue;
    public string WhereValue
    {
        get { return _whereValue; }
        internal set
        {
            if (value != null)
                AllEntities = false;
            _whereValue = value;
        }
    }
    private string _orderByValue;
    public string OrderByValue
    {
        get { return _orderByValue; }
        internal set
        {
            if (value != null)
                AllEntities = false;
            _orderByValue = value;
        }
    }
    private int? _skipValue;
    public int? SkipValue 
    {
        get { return _skipValue; }
        internal set
        {
            if (value.HasValue)
                AllEntities = false;
            _skipValue = value;
        }
    }
    private int? _takeValue;
    public int? TakeValue 
    {
        get { return _takeValue; }
        internal set
        {
            if (value.HasValue)
                AllEntities = false;
            _takeValue = value;
        }
    }
}

Cela vous paraît peut-être bizarre pour l’instant mais attendez de voir la suite.

Ensuite, je me suis créé un projet de Test. Dans ce projet, j’ajoute la référence vers mon service wcf en incluant la référence vers mon projet Entities et celle vers Client.LINQ. Pour pouvoir utiliser mon “faux” provider LINQ, il me faut des MyQueryable de mes types d’entités. Là-aussi, (j’espère que vous l’aviez deviné), template T4.

public partial class NorthwindClientContext
{     
      private INorthwindService _service;

      public NorthwindClientContext(INorthwindService service)
      {
            _service = service;
     

      public MyQueryable<Customer> Customers
      {
            get
            {
                  var value = new MyQueryable<Customer>();
                  value.Enumerable = GetCustomers(value);
                  return value;
            }
      }
      private IEnumerable<Customer> GetCustomers(MyQueryable<Customer> myQueryable)
      {
            IEnumerable<Customer> value;
            if (myQueryable.AllEntities)
                  value = _service.GetAllCustomers();
            else
                  value = _service.GetCustomers(myQueryable.IncludeValues.ToList(), myQueryable.WhereValue, myQueryable.OrderByValue, myQueryable.SkipValue, myQueryable.TakeValue);
            foreach (var entity in value)
                  yield return entity;
     

      public MyQueryable<Order> Orders
      {
            get
            {
                  var value = new MyQueryable<Order>();
                  value.Enumerable = GetOrders(value);
                  return value;
            }
      }
      private IEnumerable<Order> GetOrders(MyQueryable<Order> myQueryable)
      {
            IEnumerable<Order> value;
            if (myQueryable.AllEntities)
                  value = _service.GetAllOrders();
            else
                  value = _service.GetOrders(myQueryable.IncludeValues.ToList(), myQueryable.WhereValue, myQueryable.OrderByValue, myQueryable.SkipValue, myQueryable.TakeValue);
            foreach (var entity in value)
                  yield return entity;
     

     
public MyQueryable<OrderDetail> OrderDetails
      {
            get
            {
                  var value = new MyQueryable<OrderDetail>();
                  value.Enumerable = GetOrderDetails(value);
                  return value;
            }
      } 
      private IEnumerable<OrderDetail> GetOrderDetails(MyQueryable<OrderDetail> myQueryable)
      {
            IEnumerable<OrderDetail> value;
            if (myQueryable.AllEntities)
                  value = _service.GetAllOrderDetails();
            else
                  value = _service.GetOrderDetails(myQueryable.IncludeValues.ToList(), myQueryable.WhereValue, myQueryable.OrderByValue, myQueryable.SkipValue, myQueryable.TakeValue);
            foreach (var entity in value)
                  yield return entity;
      }
}

L’utilisation du yield return me permet une exécution différé et m’assure que les propriétés de MyQueryable sont correctement renseignées lorsque la méthode GetCustomers / GetOrders / GetOrderDetails est appelée.

Ainsi, le code suivant :

var order = (from o in new NorthwindClientContext(service).Orders.Include("Customer").Include("OrderDetails")
             where o.ShipCity == "PARIS"
             orderby o.OrderDate
             select o).Skip(2).First();

génèrera un appel à la méthode

_service.GetOrders(myQueryable.IncludeValues.ToList(), myQueryable.WhereValue, myQueryable.OrderByValue, myQueryable.SkipValue, myQueryable.TakeValue)

avec les paramètres suivants :

image

Ce qui génèrera la requête SQL suivante incluant tout cela :

SELECT
[Project1].[OrderID] AS [OrderID],
[Project1].[CustomerID] AS [CustomerID],
[Project1].[EmployeeID] AS [EmployeeID],
[Project1].[OrderDate] AS [OrderDate],
[Project1].[RequiredDate] AS [RequiredDate],
[Project1].[ShippedDate] AS [ShippedDate],
[Project1].[ShipVia] AS [ShipVia],
[Project1].[Freight] AS [Freight],
[Project1].[ShipName] AS [ShipName],
[Project1].[ShipAddress] AS [ShipAddress],
[Project1].[ShipCity] AS [ShipCity],
[Project1].[ShipRegion] AS [ShipRegion],
[Project1].[ShipPostalCode] AS [ShipPostalCode],
[Project1].[ShipCountry] AS [ShipCountry],
[Project1].[CustomerID1] AS [CustomerID1],
[Project1].[CompanyName] AS [CompanyName],
[Project1].[ContactName] AS [ContactName],
[Project1].[ContactTitle] AS [ContactTitle],
[Project1].[Address] AS [Address],
[Project1].[City] AS [City],
[Project1].[Region] AS [Region],
[Project1].[PostalCode] AS [PostalCode],
[Project1].[Country] AS [Country],
[Project1].[Phone] AS [Phone],
[Project1].[Fax] AS [Fax],
[Project1].[C1] AS [C1],
[Project1].[OrderID1] AS [OrderID1],
[Project1].[ProductID] AS [ProductID],
[Project1].[UnitPrice] AS [UnitPrice],
[Project1].[Quantity] AS [Quantity],
[Project1].[Discount] AS [Discount]
FROM ( SELECT 
      [Limit1].[OrderID] AS [OrderID], 
      [Limit1].[CustomerID1] AS [CustomerID], 
      [Limit1].[EmployeeID] AS [EmployeeID], 
      [Limit1].[OrderDate] AS [OrderDate], 
      [Limit1].[RequiredDate] AS [RequiredDate], 
      [Limit1].[ShippedDate] AS [ShippedDate], 
      [Limit1].[ShipVia] AS [ShipVia], 
      [Limit1].[Freight] AS [Freight], 
      [Limit1].[ShipName] AS [ShipName], 
      [Limit1].[ShipAddress] AS [ShipAddress], 
      [Limit1].[ShipCity] AS [ShipCity], 
      [Limit1].[ShipRegion] AS [ShipRegion], 
      [Limit1].[ShipPostalCode] AS [ShipPostalCode], 
      [Limit1].[ShipCountry] AS [ShipCountry], 
      [Limit1].[CustomerID2] AS [CustomerID1], 
      [Limit1].[CompanyName] AS [CompanyName], 
      [Limit1].[ContactName] AS [ContactName], 
      [Limit1].[ContactTitle] AS [ContactTitle], 
      [Limit1].[Address] AS [Address], 
      [Limit1].[City] AS [City], 
      [Limit1].[Region] AS [Region], 
      [Limit1].[PostalCode] AS [PostalCode], 
      [Limit1].[Country] AS [Country], 
      [Limit1].[Phone] AS [Phone], 
      [Limit1].[Fax] AS [Fax], 
      [Extent3].[OrderID] AS [OrderID1], 
      [Extent3].[ProductID] AS [ProductID], 
      [Extent3].[UnitPrice] AS [UnitPrice], 
      [Extent3].[Quantity] AS [Quantity], 
      [Extent3].[Discount] AS [Discount], 
      CASE WHEN ([Extent3].[OrderID] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
      FROM   (SELECT TOP (1) [Filter1].[OrderID], [Filter1].[CustomerID1], [Filter1].[EmployeeID], [Filter1].[OrderDate], [Filter1].[RequiredDate], [Filter1].[ShippedDate], [Filter1].[ShipVia], [Filter1].[Freight], [Filter1].[ShipName], [Filter1].[ShipAddress], [Filter1].[ShipCity], [Filter1].[ShipRegion], [Filter1].[ShipPostalCode], [Filter1].[ShipCountry], [Filter1].[CustomerID2], [Filter1].[CompanyName], [Filter1].[ContactName], [Filter1].[ContactTitle], [Filter1].[Address], [Filter1].[City], [Filter1].[Region], [Filter1].[PostalCode], [Filter1].[Country], [Filter1].[Phone], [Filter1].[Fax]
            FROM ( SELECT [Extent1].[OrderID] AS [OrderID], [Extent1].[CustomerID] AS [CustomerID1], [Extent1].[EmployeeID] AS [EmployeeID], [Extent1].[OrderDate] AS [OrderDate], [Extent1].[RequiredDate] AS [RequiredDate], [Extent1].[ShippedDate] AS [ShippedDate], [Extent1].[ShipVia] AS [ShipVia], [Extent1].[Freight] AS [Freight], [Extent1].[ShipName] AS [ShipName], [Extent1].[ShipAddress] AS [ShipAddress], [Extent1].[ShipCity] AS [ShipCity], [Extent1].[ShipRegion] AS [ShipRegion], [Extent1].[ShipPostalCode] AS [ShipPostalCode], [Extent1].[ShipCountry] AS [ShipCountry], [Extent2].[CustomerID] AS [CustomerID2], [Extent2].[CompanyName] AS [CompanyName], [Extent2].[ContactName] AS [ContactName], [Extent2].[ContactTitle] AS [ContactTitle], [Extent2].[Address] AS [Address], [Extent2].[City] AS [City], [Extent2].[Region] AS [Region], [Extent2].[PostalCode] AS [PostalCode], [Extent2].[Country] AS [Country], [Extent2].[Phone] AS [Phone], [Extent2].[Fax] AS [Fax], row_number() OVER (ORDER BY [Extent1].[OrderDate] ASC) AS [row_number]
                  FROM  [dbo].[Orders] AS [Extent1]
                  LEFT OUTER JOIN [dbo].[Customers] AS [Extent2] ON [Extent1].[CustomerID] = [Extent2].[CustomerID]
                  WHERE [Extent1].[ShipCity] = 'PARIS'
            )  AS [Filter1]
            WHERE [Filter1].[row_number] > 2
            ORDER BY [Filter1].[OrderDate] ASC ) AS [Limit1]
      LEFT OUTER JOIN [dbo].[Order Details] AS [Extent3] ON [Limit1].[OrderID] = [Extent3].[OrderID]
)  AS [Project1]

ORDER BY [Project1].[OrderDate] ASC, [Project1].[OrderID] ASC, [Project1].[CustomerID1] ASC, [Project1].[C1] ASC

Si on enlève les Include, la requête SQL est tout de suite plus lisible Smile :

SELECT TOP (1)
[Filter1].[OrderID] AS [OrderID],
[Filter1].[CustomerID] AS [CustomerID],
[Filter1].[EmployeeID] AS [EmployeeID],
[Filter1].[OrderDate] AS [OrderDate],
[Filter1].[RequiredDate] AS [RequiredDate],
[Filter1].[ShippedDate] AS [ShippedDate],
[Filter1].[ShipVia] AS [ShipVia],
[Filter1].[Freight] AS [Freight],
[Filter1].[ShipName] AS [ShipName],
[Filter1].[ShipAddress] AS [ShipAddress],
[Filter1].[ShipCity] AS [ShipCity],
[Filter1].[ShipRegion] AS [ShipRegion],
[Filter1].[ShipPostalCode] AS [ShipPostalCode],
[Filter1].[ShipCountry] AS [ShipCountry]
FROM ( SELECT [Extent1].[OrderID] AS [OrderID], [Extent1].[CustomerID] AS [CustomerID], [Extent1].[EmployeeID] AS [EmployeeID], [Extent1].[OrderDate] AS [OrderDate], [Extent1].[RequiredDate] AS [RequiredDate], [Extent1].[ShippedDate] AS [ShippedDate], [Extent1].[ShipVia] AS [ShipVia], [Extent1].[Freight] AS [Freight], [Extent1].[ShipName] AS [ShipName], [Extent1].[ShipAddress] AS [ShipAddress], [Extent1].[ShipCity] AS [ShipCity], [Extent1].[ShipRegion] AS [ShipRegion], [Extent1].[ShipPostalCode] AS [ShipPostalCode], [Extent1].[ShipCountry] AS [ShipCountry], row_number() OVER (ORDER BY [Extent1].[OrderDate] ASC) AS [row_number]
      FROM [dbo].[Orders] AS [Extent1]
      WHERE [Extent1].[ShipCity] = 'PARIS'
)  AS [Filter1]
WHERE [Filter1].[row_number] > 2

ORDER BY [Filter1].[OrderDate] ASC

On retrouve bien notre WHERE City = ‘Paris’, notre WHERE row_number > 2 (pour le skip), notre ORDER BY OrderDate et notre TOP  1 (pour le First).

 

Attention, ce provider LINQ est un POC. Il manque beaucoup de choses (pas forcément très difficile à rajouter d’ailleurs) tel que l’utilisation des variables et des new (pour comparer par rapport à une date par ex, etc.)

 

Que se passe-t-il si vous intégrez des méthodes non supportées dans ClientLINQ ?

Ca se passe très bien ! Smile

En effet, le résultat de votre méthode non supportée ne sera pas un MyQueryable. Par conséquent, ce sont les méthodes de LINQ To Object qui seront utilisées.

Par exemple, la requête suivante :

var customerInfos = (from o in new NorthwindClientContext(service).Orders.Include("Customer")
                     where o.ShipCity == "PARIS"
                     orderby o.OrderDate
                     group o by o.Customer into g

                     select new { g.Key.CompanyName, g.Key.ContactName, OrdersCount = g.Count() }).ToList();

génèrera un appel à la méthode

_service.GetOrders(myQueryable.IncludeValues.ToList(), myQueryable.WhereValue, myQueryable.OrderByValue, myQueryable.SkipValue, myQueryable.TakeValue)

avec les paramètres suivants :

image

ce qui génèrera la requête SQL suivante :

SELECT
[Extent1].[OrderID] AS [OrderID],
[Extent1].[CustomerID] AS [CustomerID],
[Extent1].[EmployeeID] AS [EmployeeID],
[Extent1].[OrderDate] AS [OrderDate],
[Extent1].[RequiredDate] AS [RequiredDate],
[Extent1].[ShippedDate] AS [ShippedDate],
[Extent1].[ShipVia] AS [ShipVia],
[Extent1].[Freight] AS [Freight],
[Extent1].[ShipName] AS [ShipName],
[Extent1].[ShipAddress] AS [ShipAddress],
[Extent1].[ShipCity] AS [ShipCity],
[Extent1].[ShipRegion] AS [ShipRegion],
[Extent1].[ShipPostalCode] AS [ShipPostalCode],
[Extent1].[ShipCountry] AS [ShipCountry],
[Extent2].[CustomerID] AS [CustomerID1],
[Extent2].[CompanyName] AS [CompanyName],
[Extent2].[ContactName] AS [ContactName],
[Extent2].[ContactTitle] AS [ContactTitle],
[Extent2].[Address] AS [Address],
[Extent2].[City] AS [City],
[Extent2].[Region] AS [Region],
[Extent2].[PostalCode] AS [PostalCode],
[Extent2].[Country] AS [Country],
[Extent2].[Phone] AS [Phone],
[Extent2].[Fax] AS [Fax]
FROM  [dbo].[Orders] AS [Extent1]
LEFT OUTER JOIN [dbo].[Customers] AS [Extent2] ON [Extent1].[CustomerID] = [Extent2].[CustomerID]
WHERE [Extent1].[ShipCity] = 'PARIS'
ORDER BY [Extent1].[OrderDate] ASC

Comme vous pouvez le constater, pas de trace de GROUP BY. Pourtant le résultat a bien pris en compte mon group by (en LINQ To Object).

 

Le but de ce post était certes de présenter un “faux” provider LINQ rigolo en se basant sur le fait que le yield return diffère l’exécution de la méthode mais surtout de vous convaincre (et je suis sûr que vous l’êtes Smile) par le gain de productivité du couple EF / T4. En effet,

  • Vos templates sont réutilisables de projet en projet. Il suffit de changer le path de l’edmx et de dire à Visual Studio de regénérer le code de tous les templates T4.
  • Si vous ne les avez pas déjà écrit, le temps de dev n’est pas proportionnel au nombre d’entités ce qui implique un gain de productivité très rapide par rapport à un développement classique.

Les classes / interfaces générées par T4 peuvent être partial. Il sera donc très simple de les compléter pour rajouter une partie custom.

Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :

Publié mardi 24 novembre 2009 23:41 par Matthieu MEZIL

Commentaires

# re: Service WCF orienté données Agile avec EF4 et “faux” provider LINQ @ mercredi 25 novembre 2009 00:11

Un post avec plein d'images... Ca devrait faire plaisir à Florent Wink

Matthieu MEZIL

# re: Service WCF orienté données Agile avec EF4 et “faux” provider LINQ @ mercredi 25 novembre 2009 11:51

Tu mets ton Edmx dans la DAL :

C'est vrai que l'Edmx permet de modéliser et d'accéder à la BDD, mais dans une approche "model driven", est-ce qu'il n'est pas mieux de le mettre dans une BLL ??

Vincent THAVONEKHAM

thavo

# re: Service WCF orienté données Agile avec EF4 et “faux” provider LINQ @ mercredi 25 novembre 2009 12:03

Tu pourrais mettre la solution en ligne? C'est plus facile pour regarder.

A+

tja

# re: Service WCF orienté données Agile avec EF4 et “faux” provider LINQ @ jeudi 18 février 2010 14:02

Ca m'intéresse aussi, d'avoir les sources de la solution, en terme de bonne pratique cela me semble très bien. merci Matthieu

dauphinus

# re: Service WCF orienté données Agile avec EF4 et “faux” provider LINQ @ jeudi 18 février 2010 17:49

"en terme de bonne pratique cela me semble très bien". Moi aussi Smile

Par contre, je ne souhaite exceptionnellement pas partagé ces sources, souhaintant avoir un ROI.

Par contre j'interviens dans le cadre de missions de conseil sur la création de T4...

Pour plus d'info, n'hésitez pas à me contacter matthieu.mezil@live.fr

Matthieu MEZIL

Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- Merci par Blog de Jérémy Jeanson le 10-01-2019, 20:47

- Office 365: Script PowerShell pour auditer l’usage des Office Groups de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 11:02

- Office 365: Script PowerShell pour auditer l’usage de Microsoft Teams de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 10:39

- Office 365: Script PowerShell pour auditer l’usage de OneDrive for Business de votre tenant par Blog Technique de Romelard Fabrice le 04-25-2019, 15:13

- Office 365: Script PowerShell pour auditer l’usage de SharePoint Online de votre tenant par Blog Technique de Romelard Fabrice le 02-27-2019, 13:39

- Office 365: Script PowerShell pour auditer l’usage d’Exchange Online de votre tenant par Blog Technique de Romelard Fabrice le 02-25-2019, 15:07

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Stream Portal par Blog Technique de Romelard Fabrice le 02-21-2019, 17:56

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Video Portal par Blog Technique de Romelard Fabrice le 02-18-2019, 18:56

- Office 365: Script PowerShell pour extraire les Audit Log basés sur des filtres fournis par Blog Technique de Romelard Fabrice le 01-28-2019, 16:13

- SharePoint Online: Script PowerShell pour désactiver l’Option IRM des sites SPO non autorisés par Blog Technique de Romelard Fabrice le 12-14-2018, 13:01