Una de las características más habituales (y necesarias) de cualquier sistema de Business Intelligence (y de Power BI en concreto) es la capacidad para conectarse a diversas fuentes de datos, tanto aplicaciones de negocio como a las bases de datos directamente.
En este artículo describimos cómo hacer una integración para una aplicación desarrollada por nosotros para implementar un punto de conexión ODATA para poder explotar los datos de nuestra aplicación mediante prácticamente cualquier plataforma de análisis.
La conexión mediante ODATA nos aporta algunas ventajas sobre la conexión directa a base de datos, especialmente en el ámbito de personalización:
- Aísla los datos de análisis de la estructura de la base de datos, al permitirnos definir un esquema propio de los datos que expondremos a Power BI, nos permite evolucionar la estructura de nuestra base de datos transaccional sin afectar a las lecturas dedicadas al análisis, incluso si en un futuro creáramos un repositorio dedicado de lectura, podríamos hacer que el conector ODATA se conectara al nuevo repositorio sin afectar a los clientes que ya se conectan.
- En caso de aplicaciones multitenant nos permite “ocultar” detalles de la arquitectura de datos como sharding, distribución horizontal de tablas en distintas bases de datos o repositorios, etc.
- Permite personalizar la seguridad de forma granular, pudiendo filtrar datos, realizar transformaciones, etc. en base al usuario que realiza la conexión, aplicando la misma lógica que tenemos implementada en nuestra aplicación.
- Evitamos tener que exponer la base de datos de forma pública, realizar configuraciones de red específicas o tener que dar de alta conexiones a clientes específicos.
No obstante, debemos tener en cuenta que, al interponer una capa entre el sistema de análisis y el repositorio de los datos, se puede producir una perdida rendimiento en la importación de los datos, por lo que es posible que haya que implementar medidas como caché, cargas incrementales (que se hacen necesarias a partir de determinado volumen) o simplemente asumir que las importaciones lleven algo más de tiempo.
Para implementar un punto de conexión ODATA en netcore, haremos uso de la librería Microsoft.AspNetCore.OData (https://github.com/OData/AspNetCoreOData) que nos ayuda a implementar tanto la definición del modelo de datos a exponer como a implementar los controladores que realizan la obtención y filtrado de los datos.
En este ejemplo implementaremos el punto de acceso con la versión 7 de la librería.
Definición de las entidades del modelo
En este paso definiremos la estructura de datos que expondremos para que sea importada por Power BI, para ello definiremos las clases de datos y etiquetaremos los campos que sean primary key de cada conjunto de entidades, así como las foreign keys para definir las relaciones entre las entidades.
Esta estructura no tiene por qué coincidir con la de la/las bases de datos subyacentes: debemos diseñarlas pensando que son entidades destinadas al análisis, por lo que pueden integrar datos de distintos orígenes, presentar datos denormalizados, etc.
public class Expediente { [Key] public int ExpedienteId { get; set; } public int Anno { get; set;} public string Codigo { get; set;} [ForeignKey(nameof(Empresa))] public int EmpresaId { get; set; } }
Una vez que tenemos creadas las entidades crearemos una clase que devuelva el modelo, para registrarlo en la infraestructura:
public class ODataModelBuilder { public IEdmModel GetEdmModel(IServiceProvider serviceProvider) { var builder = new ODataConventionModelBuilder(serviceProvider); builder.EntitySet(nameof(Expediente)); builder.EntitySet (nameof(Empresa)); return builder.GetEdmModel(); } }
Esta clase model builder, la registraremos en el método ConfigureServices
de la clase Startup
, junto con los servicios propios de OData:
services.AddOData(); services.AddTransient();
En el método Configure del startup, registraremos el esquema del modelo, para dar una dirección donde Power BI lea los metadatos con la estructura del modelo:
app.UseMvc(routeBuilder => { var modelBuilder = new Docu2ODataModelBuilder(); routeBuilder.MapODataServiceRoute("ODataRoutes", "odata", modelBuilder.GetEdmModel(app.ApplicationServices)); });
En este punto ya tendremos registrada la infraestructura, ahora procederemos a implementar los controladores que accederán a los datos y devuelvan los datos.
Creación de los controladores
Para cada entidad expuesta crearemos un controlador que heredará de la clase ODataController, estos controladores pueden recibir servicios mediante inyección de dependencias como cualquier otro controlador e implementarán un método Get, que será el que obtenga y devuelva los datos:
[Authorize(AuthenticationSchemes = BasicAuthenticationDefaults.AuthenticationScheme)] public class ExpedienteController : Docu2ODataControllerBase { private readonly IDbConnection _conn; private readonly ODataModelBuilder _modelBuilder; public ExpedienteController(IDbConnection conn, Docu2ODataModelBuilder modelBuilder) { this._conn = conn; this._modelBuilder = modelBuilder; } [EnableQuery] public async TaskGet([FromQuery] ODataQueryOptions options) { var model = this._modelBuilder.GetEdmModel(HttpContext.RequestServices); var type = model.FindDeclaredType(typeof(Expediente).FullName); var userId = ClaimsPrincipal.Current.GetUserId(); IQueryable expQuery = ObtenerQueryable(userId); expQuery = options.ApplyTo(expQuery, new ODataQuerySettings()) as IQueryable ; var lista = expQuery.ToList() return Ok(lista); } }
En el código podemos ver como hemos etiquetado la acción mediante EnableQuery y como el método recibe un objeto ODataQueryOptions, ese objeto contiene opciones de filtrado, ordenación, etc. Para aplicarlas se puede hacer bien de forma manual o bien con el método ApplyTo que recibe un IQueryable y aplica las condiciones sin que tengamos que hacer nada.
Conviene en este punto recordar las diferencias entre IQueryable y IEnumerable, a fin de evitar problemas de rendimiento: Si usamos IEnumerable hemos leído ya todos los datos en memoria (e impactado contra la base de datos), mientras que si usamos IQueryable lo que guardamos es la consulta y no impacta contra la base de datos hasta que se materializa (normalmente al hacer un ToList o un ToArray).
También podemos observar que obtenemos el usuario actual y llamamos al método de obtención de los datos con el identificador del usuario, que podremos usar para filtrar o realizar transformaciones a los datos.
Implementación de la seguridad
En este caso, para las acciones de los controladores hemos incluido seguridad para poder controlar el acceso a los datos, en la acción del controlador podemos ver que se obtiene el usuario (y sus claims).
De forma resumida hemos implementado un AuthenticationHandler para el esquema básico, algo parecido a lo que se describe aquí: https://matteosonoio.it/aspnet-core-authentication-schemes
En nuestro caso lo que hacemos es tomar las credenciales que introduce el usuario a la hora de configurar la fuente de datos en Power BI con autenticación básica y comprobarlas contra un usuario de un tenant de Office 365 mediante una llamada al API.
Una vez comprobado, la llamada está autenticada y los claims del usuario están disponibles para que nuestro código pueda adaptar los datos devueltos al usuario concreto que realiza la petición.
Configurar la fuente de datos en Power BI
Para configurar la fuente de datos en Power BI, haremos el procedimiento habitual:
Tras configurar la fuente de datos, ya tendremos nuestros datos disponibles para ser usados en nuestros informes, si apuntamos Power BI a nuestra dirección local, podremos depurar las consultas de los controladores desde Visual Studio en caso de que fuera necesario.
Conclusión
En algunos escenarios puede ser interesante implementar un acceso a nuestros datos más personalizable y flexible que simplemente dar acceso a datos, implantar un punto de conexión OData puede resolver esa necesidad de forma sencilla y rápida, pero hemos de evaluar cuidadosamente los pros y contras y las necesidades concretas de nuestra aplicación y sus usuarios, sin olvidarnos de aspectos como la seguridad, el volumen de datos y el rendimiento.