diff --git a/PBIXInspector.sln b/PBIXInspector.sln
index 16b9ee8..ed1ee38 100644
--- a/PBIXInspector.sln
+++ b/PBIXInspector.sln
@@ -86,6 +86,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BaseThemes", "BaseThemes",
DocsExamples\ReportExample\PBI Inspector Reporting.Report\StaticResources\SharedResources\BaseThemes\CY23SU11.json = DocsExamples\ReportExample\PBI Inspector Reporting.Report\StaticResources\SharedResources\BaseThemes\CY23SU11.json
EndProjectSection
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PBIXInspectorAzureFunctions", "PBIXInspectorAzureFunctions\PBIXInspectorAzureFunctions.csproj", "{B6F334C7-0E39-487E-83D9-76430CCA80B4}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -111,6 +113,10 @@ Global
{2EFEF6B8-E017-4D14-9F30-DCD093CB85CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2EFEF6B8-E017-4D14-9F30-DCD093CB85CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2EFEF6B8-E017-4D14-9F30-DCD093CB85CD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B6F334C7-0E39-487E-83D9-76430CCA80B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B6F334C7-0E39-487E-83D9-76430CCA80B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B6F334C7-0E39-487E-83D9-76430CCA80B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B6F334C7-0E39-487E-83D9-76430CCA80B4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/PBIXInspectorAzureFunctions/.gitignore b/PBIXInspectorAzureFunctions/.gitignore
new file mode 100644
index 0000000..ff5b00c
--- /dev/null
+++ b/PBIXInspectorAzureFunctions/.gitignore
@@ -0,0 +1,264 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# Azure Functions localsettings file
+local.settings.json
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# DNX
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+#*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+# NuGet v3's project.json files produces more ignoreable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+node_modules/
+orleans.codegen.cs
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# JetBrains Rider
+.idea/
+*.sln.iml
+
+# CodeRush
+.cr/
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
\ No newline at end of file
diff --git a/PBIXInspectorAzureFunctions/Dockerfile b/PBIXInspectorAzureFunctions/Dockerfile
new file mode 100644
index 0000000..9219997
--- /dev/null
+++ b/PBIXInspectorAzureFunctions/Dockerfile
@@ -0,0 +1,24 @@
+#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
+
+FROM mcr.microsoft.com/azure-functions/dotnet-isolated:4-dotnet-isolated8.0 AS base
+WORKDIR /home/site/wwwroot
+EXPOSE 8080
+
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
+ARG BUILD_CONFIGURATION=Release
+WORKDIR /src
+COPY ["PBIXInspectorAzureFunctions/PBIXInspectorAzureFunctions.csproj", "PBIXInspectorAzureFunctions/"]
+RUN dotnet restore "./PBIXInspectorAzureFunctions/./PBIXInspectorAzureFunctions.csproj"
+COPY . .
+WORKDIR "/src/PBIXInspectorAzureFunctions"
+RUN dotnet build "./PBIXInspectorAzureFunctions.csproj" -c $BUILD_CONFIGURATION -o /app/build
+
+FROM build AS publish
+ARG BUILD_CONFIGURATION=Release
+RUN dotnet publish "./PBIXInspectorAzureFunctions.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+
+FROM base AS final
+WORKDIR /home/site/wwwroot
+COPY --from=publish /app/publish .
+ENV AzureWebJobsScriptRoot=/home/site/wwwroot \
+ AzureFunctionsJobHost__Logging__Console__IsEnabled=true
\ No newline at end of file
diff --git a/PBIXInspectorAzureFunctions/Inspect.cs b/PBIXInspectorAzureFunctions/Inspect.cs
new file mode 100644
index 0000000..7a439ae
--- /dev/null
+++ b/PBIXInspectorAzureFunctions/Inspect.cs
@@ -0,0 +1,69 @@
+using Azure.Storage.Blobs;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using PBIXInspectorLibrary;
+using PBIXInspectorLibrary.Output;
+using System.Reflection.Metadata;
+using System.Text.Json;
+using System.Threading.Tasks;
+
+namespace PBIXInspectorAzureFunctions
+{
+ public static class Inspect
+ {
+ ///
+ /// Azure function that inspect a report layout json files for compliance with the rules defined in the supplied Rules.json file
+ ///
+ /// The Azure function blob trigger
+ /// The rules file
+ /// The Azure Function context e.g. to retrieve the logger instance
+ /// A json string with the test results
+ /// TODO: Add support for multiple rules files
+ [Function(nameof(Inspect))]
+ [BlobOutput("pbi-inspector-output/{name}")]
+ public static string Run(
+ [BlobTrigger("pbi-report-definitions/{name}")] BlobClient reportDefinitionTriggerClient,
+ [BlobInput("pbi-inspector-rules/Rules.json")] Stream rulesInputStream,
+ FunctionContext context)
+ {
+ var logger = context.GetLogger("Inspect");
+
+ var inspector = new Inspector();
+ inspector.MessageIssued += (sender, e) => logger.LogInformation(e.Message);
+
+ try
+ {
+ var inspectionResults = inspector.Inspect(reportDefinitionTriggerClient.OpenRead(), rulesInputStream);
+
+ var testRun = new TestRun() { CompletionTime = DateTime.Now, TestedFilePath = reportDefinitionTriggerClient.Uri.ToString(), RulesFilePath = "pbi-inspector-rules/Rules.json", Verbose = true, Results = inspectionResults };
+
+ var jsonTestRun = System.Text.Json.JsonSerializer.Serialize(testRun);
+
+ // Blob Output
+ return jsonTestRun;
+ }
+ catch (ArgumentNullException ex)
+ {
+ logger.LogError(ex, "An error occurred during inspection");
+ return string.Empty;
+ }
+ catch (FileNotFoundException ex)
+ {
+ logger.LogError(ex, "An error occurred during inspection");
+ return string.Empty;
+ }
+ catch (PBIXInspectorException ex)
+ {
+ logger.LogError(ex, "An error occurred during inspection");
+ return string.Empty;
+ }
+ finally
+ {
+ logger.LogInformation("Inspection completed");
+ inspector.MessageIssued -= (sender, e) => logger.LogInformation(e.Message);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/PBIXInspectorAzureFunctions/PBIXInspectorAzureFunctions.csproj b/PBIXInspectorAzureFunctions/PBIXInspectorAzureFunctions.csproj
new file mode 100644
index 0000000..550efb0
--- /dev/null
+++ b/PBIXInspectorAzureFunctions/PBIXInspectorAzureFunctions.csproj
@@ -0,0 +1,40 @@
+
+
+ net8.0
+ v4
+ Exe
+ enable
+ enable
+ /home/site/wwwroot
+ Linux
+ b7b3b633-7c13-481a-897b-ecbc9d404a93
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+ Never
+
+
+
+
+
+
\ No newline at end of file
diff --git a/PBIXInspectorAzureFunctions/Program.cs b/PBIXInspectorAzureFunctions/Program.cs
new file mode 100644
index 0000000..9de3b3e
--- /dev/null
+++ b/PBIXInspectorAzureFunctions/Program.cs
@@ -0,0 +1,14 @@
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+var host = new HostBuilder()
+ .ConfigureFunctionsWorkerDefaults()
+ .ConfigureServices(services =>
+ {
+ services.AddApplicationInsightsTelemetryWorkerService();
+ services.ConfigureFunctionsApplicationInsights();
+ })
+ .Build();
+
+host.Run();
diff --git a/PBIXInspectorAzureFunctions/Properties/ServiceDependencies/PBIXInspectorAzureFunctions - Zip Deploy/appInsights1.arm.json b/PBIXInspectorAzureFunctions/Properties/ServiceDependencies/PBIXInspectorAzureFunctions - Zip Deploy/appInsights1.arm.json
new file mode 100644
index 0000000..2fe05ab
--- /dev/null
+++ b/PBIXInspectorAzureFunctions/Properties/ServiceDependencies/PBIXInspectorAzureFunctions - Zip Deploy/appInsights1.arm.json
@@ -0,0 +1,67 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "parameters": {
+ "resourceGroupName": {
+ "type": "string",
+ "defaultValue": "customerjourney",
+ "metadata": {
+ "_parameterType": "resourceGroup",
+ "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking."
+ }
+ },
+ "resourceGroupLocation": {
+ "type": "string",
+ "defaultValue": "eastus",
+ "metadata": {
+ "_parameterType": "location",
+ "description": "Location of the resource group. Resource groups could have different location than resources."
+ }
+ },
+ "resourceLocation": {
+ "type": "string",
+ "defaultValue": "[parameters('resourceGroupLocation')]",
+ "metadata": {
+ "_parameterType": "location",
+ "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there."
+ }
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Resources/resourceGroups",
+ "name": "[parameters('resourceGroupName')]",
+ "location": "[parameters('resourceGroupLocation')]",
+ "apiVersion": "2019-10-01"
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat('customerinsights', subscription().subscriptionId)))]",
+ "resourceGroup": "[parameters('resourceGroupName')]",
+ "apiVersion": "2019-10-01",
+ "dependsOn": [
+ "[parameters('resourceGroupName')]"
+ ],
+ "properties": {
+ "mode": "Incremental",
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "resources": [
+ {
+ "name": "customerinsights",
+ "type": "microsoft.insights/components",
+ "location": "[parameters('resourceLocation')]",
+ "kind": "web",
+ "properties": {},
+ "apiVersion": "2015-05-01"
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "metadata": {
+ "_dependencyType": "appInsights.azure"
+ }
+}
\ No newline at end of file
diff --git a/PBIXInspectorAzureFunctions/Properties/ServiceDependencies/PBIXInspectorAzureFunctions - Zip Deploy/profile.arm.json b/PBIXInspectorAzureFunctions/Properties/ServiceDependencies/PBIXInspectorAzureFunctions - Zip Deploy/profile.arm.json
new file mode 100644
index 0000000..c4d253f
--- /dev/null
+++ b/PBIXInspectorAzureFunctions/Properties/ServiceDependencies/PBIXInspectorAzureFunctions - Zip Deploy/profile.arm.json
@@ -0,0 +1,174 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_dependencyType": "compute.function.windows.appService"
+ },
+ "parameters": {
+ "resourceGroupName": {
+ "type": "string",
+ "defaultValue": "powerbiexport",
+ "metadata": {
+ "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking."
+ }
+ },
+ "resourceGroupLocation": {
+ "type": "string",
+ "defaultValue": "eastus",
+ "metadata": {
+ "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support."
+ }
+ },
+ "resourceName": {
+ "type": "string",
+ "defaultValue": "PBIXInspectorAzureFunctions",
+ "metadata": {
+ "description": "Name of the main resource to be created by this template."
+ }
+ },
+ "resourceLocation": {
+ "type": "string",
+ "defaultValue": "[parameters('resourceGroupLocation')]",
+ "metadata": {
+ "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there."
+ }
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Resources/resourceGroups",
+ "name": "[parameters('resourceGroupName')]",
+ "location": "[parameters('resourceGroupLocation')]",
+ "apiVersion": "2019-10-01"
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
+ "resourceGroup": "[parameters('resourceGroupName')]",
+ "apiVersion": "2019-10-01",
+ "dependsOn": [
+ "[parameters('resourceGroupName')]"
+ ],
+ "properties": {
+ "mode": "Incremental",
+ "expressionEvaluationOptions": {
+ "scope": "inner"
+ },
+ "parameters": {
+ "resourceGroupName": {
+ "value": "[parameters('resourceGroupName')]"
+ },
+ "resourceGroupLocation": {
+ "value": "[parameters('resourceGroupLocation')]"
+ },
+ "resourceName": {
+ "value": "[parameters('resourceName')]"
+ },
+ "resourceLocation": {
+ "value": "[parameters('resourceLocation')]"
+ }
+ },
+ "template": {
+ "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "parameters": {
+ "resourceGroupName": {
+ "type": "string"
+ },
+ "resourceGroupLocation": {
+ "type": "string"
+ },
+ "resourceName": {
+ "type": "string"
+ },
+ "resourceLocation": {
+ "type": "string"
+ }
+ },
+ "variables": {
+ "storage_name": "[toLower(concat('storage', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId))))]",
+ "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
+ "storage_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Storage/storageAccounts/', variables('storage_name'))]",
+ "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]",
+ "function_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/sites/', parameters('resourceName'))]"
+ },
+ "resources": [
+ {
+ "location": "[parameters('resourceLocation')]",
+ "name": "[parameters('resourceName')]",
+ "type": "Microsoft.Web/sites",
+ "apiVersion": "2015-08-01",
+ "tags": {
+ "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty"
+ },
+ "dependsOn": [
+ "[variables('appServicePlan_ResourceId')]",
+ "[variables('storage_ResourceId')]"
+ ],
+ "kind": "functionapp",
+ "properties": {
+ "name": "[parameters('resourceName')]",
+ "kind": "functionapp",
+ "httpsOnly": true,
+ "reserved": false,
+ "serverFarmId": "[variables('appServicePlan_ResourceId')]",
+ "siteConfig": {
+ "alwaysOn": true
+ }
+ },
+ "identity": {
+ "type": "SystemAssigned"
+ },
+ "resources": [
+ {
+ "name": "appsettings",
+ "type": "config",
+ "apiVersion": "2015-08-01",
+ "dependsOn": [
+ "[variables('function_ResourceId')]"
+ ],
+ "properties": {
+ "AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storage_name'), ';AccountKey=', listKeys(variables('storage_ResourceId'), '2017-10-01').keys[0].value, ';EndpointSuffix=', 'core.windows.net')]",
+ "FUNCTIONS_EXTENSION_VERSION": "~3",
+ "FUNCTIONS_WORKER_RUNTIME": "dotnet"
+ }
+ }
+ ]
+ },
+ {
+ "location": "[parameters('resourceGroupLocation')]",
+ "name": "[variables('storage_name')]",
+ "type": "Microsoft.Storage/storageAccounts",
+ "apiVersion": "2017-10-01",
+ "tags": {
+ "[concat('hidden-related:', concat('/providers/Microsoft.Web/sites/', parameters('resourceName')))]": "empty"
+ },
+ "properties": {
+ "supportsHttpsTrafficOnly": true
+ },
+ "sku": {
+ "name": "Standard_LRS"
+ },
+ "kind": "Storage"
+ },
+ {
+ "location": "[parameters('resourceGroupLocation')]",
+ "name": "[variables('appServicePlan_name')]",
+ "type": "Microsoft.Web/serverFarms",
+ "apiVersion": "2015-08-01",
+ "sku": {
+ "name": "S1",
+ "tier": "Standard",
+ "family": "S",
+ "size": "S1"
+ },
+ "properties": {
+ "name": "[variables('appServicePlan_name')]"
+ }
+ }
+ ]
+ }
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/PBIXInspectorAzureFunctions/Properties/ServiceDependencies/PBIXInspectorAzureFunctions - Zip Deploy/storage1.arm.json b/PBIXInspectorAzureFunctions/Properties/ServiceDependencies/PBIXInspectorAzureFunctions - Zip Deploy/storage1.arm.json
new file mode 100644
index 0000000..05fbd24
--- /dev/null
+++ b/PBIXInspectorAzureFunctions/Properties/ServiceDependencies/PBIXInspectorAzureFunctions - Zip Deploy/storage1.arm.json
@@ -0,0 +1,70 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "parameters": {
+ "resourceGroupName": {
+ "type": "string",
+ "defaultValue": "powerbiexport",
+ "metadata": {
+ "_parameterType": "resourceGroup",
+ "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking."
+ }
+ },
+ "resourceGroupLocation": {
+ "type": "string",
+ "defaultValue": "eastus",
+ "metadata": {
+ "_parameterType": "location",
+ "description": "Location of the resource group. Resource groups could have different location than resources."
+ }
+ },
+ "resourceLocation": {
+ "type": "string",
+ "defaultValue": "[parameters('resourceGroupLocation')]",
+ "metadata": {
+ "_parameterType": "location",
+ "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there."
+ }
+ }
+ },
+ "resources": [
+ {
+ "type": "Microsoft.Resources/resourceGroups",
+ "name": "[parameters('resourceGroupName')]",
+ "location": "[parameters('resourceGroupLocation')]",
+ "apiVersion": "2019-10-01"
+ },
+ {
+ "type": "Microsoft.Resources/deployments",
+ "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat('powerbiexportstoracc', subscription().subscriptionId)))]",
+ "resourceGroup": "[parameters('resourceGroupName')]",
+ "apiVersion": "2019-10-01",
+ "dependsOn": [
+ "[parameters('resourceGroupName')]"
+ ],
+ "properties": {
+ "mode": "Incremental",
+ "template": {
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "contentVersion": "1.0.0.0",
+ "resources": [
+ {
+ "sku": {
+ "name": "Standard_LRS",
+ "tier": "Standard"
+ },
+ "kind": "StorageV2",
+ "name": "powerbiexportstoracc",
+ "type": "Microsoft.Storage/storageAccounts",
+ "location": "[parameters('resourceLocation')]",
+ "apiVersion": "2017-10-01"
+ }
+ ]
+ }
+ }
+ }
+ ],
+ "metadata": {
+ "_dependencyType": "storage.azure"
+ }
+}
\ No newline at end of file
diff --git a/PBIXInspectorAzureFunctions/Properties/launchSettings.json b/PBIXInspectorAzureFunctions/Properties/launchSettings.json
new file mode 100644
index 0000000..cf6f479
--- /dev/null
+++ b/PBIXInspectorAzureFunctions/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "profiles": {
+ "PBIXInspectorAzureFunctions": {
+ "commandName": "Project",
+ "commandLineArgs": "--port 7020"
+ },
+ "Docker": {
+ "commandName": "Docker",
+ "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
+ "httpPort": 32631,
+ "useSSL": false
+ }
+ }
+}
\ No newline at end of file
diff --git a/PBIXInspectorAzureFunctions/Properties/serviceDependencies.PBIXInspectorAzureFunctions - Zip Deploy.json b/PBIXInspectorAzureFunctions/Properties/serviceDependencies.PBIXInspectorAzureFunctions - Zip Deploy.json
new file mode 100644
index 0000000..1a2327e
--- /dev/null
+++ b/PBIXInspectorAzureFunctions/Properties/serviceDependencies.PBIXInspectorAzureFunctions - Zip Deploy.json
@@ -0,0 +1,14 @@
+{
+ "dependencies": {
+ "appInsights1": {
+ "resourceId": "/subscriptions/[parameters('subscriptionId')]/resourceGroups/[parameters('resourceGroupName')]/providers/microsoft.insights/components/customerinsights",
+ "type": "appInsights.azure",
+ "connectionId": "APPLICATIONINSIGHTS_CONNECTION_STRING"
+ },
+ "storage1": {
+ "resourceId": "/subscriptions/[parameters('subscriptionId')]/resourceGroups/[parameters('resourceGroupName')]/providers/Microsoft.Storage/storageAccounts/powerbiexportstoracc",
+ "type": "storage.azure",
+ "connectionId": "AzureWebJobsStorage"
+ }
+ }
+}
\ No newline at end of file
diff --git a/PBIXInspectorAzureFunctions/Properties/serviceDependencies.json b/PBIXInspectorAzureFunctions/Properties/serviceDependencies.json
new file mode 100644
index 0000000..d029b88
--- /dev/null
+++ b/PBIXInspectorAzureFunctions/Properties/serviceDependencies.json
@@ -0,0 +1,20 @@
+{
+ "dependencies": {
+ "secrets1": {
+ "type": "secrets"
+ },
+ "storage2": {
+ "type": "storage",
+ "connectionId": "BlobStorageSetting",
+ "dynamicId": null
+ },
+ "appInsights1": {
+ "type": "appInsights",
+ "connectionId": "APPLICATIONINSIGHTS_CONNECTION_STRING"
+ },
+ "storage1": {
+ "type": "storage",
+ "connectionId": "AzureWebJobsStorage"
+ }
+ }
+}
\ No newline at end of file
diff --git a/PBIXInspectorAzureFunctions/Properties/serviceDependencies.local.json b/PBIXInspectorAzureFunctions/Properties/serviceDependencies.local.json
new file mode 100644
index 0000000..4252d26
--- /dev/null
+++ b/PBIXInspectorAzureFunctions/Properties/serviceDependencies.local.json
@@ -0,0 +1,23 @@
+{
+ "dependencies": {
+ "appInsights1": {
+ "type": "appInsights.sdk"
+ },
+ "storage1": {
+ "type": "storage.emulator",
+ "connectionId": "AzureWebJobsStorage"
+ },
+ "secrets1": {
+ "type": "secrets.user"
+ },
+ "storage2": {
+ "containerPorts": "10000:10000,10001:10001,10002:10002",
+ "secretStore": "LocalSecretsFile",
+ "containerName": "azurite",
+ "containerImage": "mcr.microsoft.com/azure-storage/azurite",
+ "type": "storage.container",
+ "connectionId": "BlobStorageSetting",
+ "dynamicId": null
+ }
+ }
+}
\ No newline at end of file
diff --git a/PBIXInspectorAzureFunctions/host.json b/PBIXInspectorAzureFunctions/host.json
new file mode 100644
index 0000000..ee5cf5f
--- /dev/null
+++ b/PBIXInspectorAzureFunctions/host.json
@@ -0,0 +1,12 @@
+{
+ "version": "2.0",
+ "logging": {
+ "applicationInsights": {
+ "samplingSettings": {
+ "isEnabled": true,
+ "excludedTypes": "Request"
+ },
+ "enableLiveMetricsFilters": true
+ }
+ }
+}
\ No newline at end of file
diff --git a/PBIXInspectorCLI/PBIXInspectorCLI.csproj b/PBIXInspectorCLI/PBIXInspectorCLI.csproj
index 4df2d37..4bf5760 100644
--- a/PBIXInspectorCLI/PBIXInspectorCLI.csproj
+++ b/PBIXInspectorCLI/PBIXInspectorCLI.csproj
@@ -2,15 +2,15 @@
Exe
- net6.0
+ net8.0
enable
enable
Program
False
True
- 1.9.5.0
- 1.9.5
- 1.9.5
+ 1.9.6.0
+ 1.9.6
+ 1.9.6
$(VersionPrefix)
$(AssembblyName)
pbiinspector.ico
diff --git a/PBIXInspectorCLI/Properties/PublishProfiles/FolderProfile.pubxml b/PBIXInspectorCLI/Properties/PublishProfiles/FolderProfile.pubxml
new file mode 100644
index 0000000..1ebd083
--- /dev/null
+++ b/PBIXInspectorCLI/Properties/PublishProfiles/FolderProfile.pubxml
@@ -0,0 +1,18 @@
+
+
+
+
+ Release
+ Any CPU
+ bin\Release\net8.0\publish\
+ FileSystem
+ <_TargetId>Folder
+ net8.0
+ false
+ win-x64
+ true
+ true
+
+
\ No newline at end of file
diff --git a/PBIXInspectorLibrary/Inspector.cs b/PBIXInspectorLibrary/Inspector.cs
index 54675e1..f2123c8 100644
--- a/PBIXInspectorLibrary/Inspector.cs
+++ b/PBIXInspectorLibrary/Inspector.cs
@@ -21,11 +21,16 @@ public class Inspector : InspectorBase
private const string CONTEXTARRAY = ".";
internal const char DRILLCHAR = '>';
- private string _pbiFilePath, _rulesFilePath;
+ private string? _pbiFilePath, _rulesFilePath;
private InspectionRules? _inspectionRules;
public event EventHandler? MessageIssued;
+ public Inspector() : base()
+ {
+ AddCustomRulesToRegistry();
+ }
+
///
///
///
@@ -41,7 +46,7 @@ public Inspector(string pbiFilePath, InspectionRules inspectionRules) : base(pbi
///
///
///
- /// Local PBIX file path
+ /// Local PBI file path
/// Local rules json file path
public Inspector(string pbiFilePath, string rulesFilePath) : base(pbiFilePath, rulesFilePath)
{
@@ -50,7 +55,7 @@ public Inspector(string pbiFilePath, string rulesFilePath) : base(pbiFilePath, r
try
{
- var inspectionRules = this.DeserialiseRules(rulesFilePath);
+ var inspectionRules = this.DeserialiseRulesFromFilePath(rulesFilePath);
if (inspectionRules == null || inspectionRules.PbiEntries == null || inspectionRules.PbiEntries.Count == 0)
{
@@ -73,40 +78,14 @@ public Inspector(string pbiFilePath, string rulesFilePath) : base(pbiFilePath, r
AddCustomRulesToRegistry();
}
-
-
- private void AddCustomRulesToRegistry()
- {
- //TODO: Use reflection to add rules
- /*
- System.Reflection.Assembly assembly = Assembly.GetExecutingAssembly();
- string nspace = "PBIXInspectorLibrary.CustomRules";
-
- var q = from t in Assembly.GetExecutingAssembly().GetTypes()
- where t.IsClass && t.Namespace == nspace
- select t;
- q.ToList().ForEach(t1 => Json.Logic.RuleRegistry.AddRule());
- */
-
- Json.Logic.RuleRegistry.AddRule();
- Json.Logic.RuleRegistry.AddRule();
- Json.Logic.RuleRegistry.AddRule();
- Json.Logic.RuleRegistry.AddRule();
- Json.Logic.RuleRegistry.AddRule();
- Json.Logic.RuleRegistry.AddRule();
- Json.Logic.RuleRegistry.AddRule();
- Json.Logic.RuleRegistry.AddRule();
- Json.Logic.RuleRegistry.AddRule();
- Json.Logic.RuleRegistry.AddRule();
- Json.Logic.RuleRegistry.AddRule();
- Json.Logic.RuleRegistry.AddRule();
- }
-
///
- /// Core method
+ /// Runs the inspection rules against the PBI file defined in the Inspector constructor
///
public IEnumerable Inspect()
{
+ if (string.IsNullOrEmpty(_pbiFilePath)) throw new ArgumentNullException(nameof(_pbiFilePath));
+ if (_inspectionRules == null) throw new ArgumentNullException(nameof(_inspectionRules));
+
var testResults = new List();
using (var pbiFile = PbiFileUtils.InitPbiFile(_pbiFilePath))
@@ -163,97 +142,8 @@ public IEnumerable Inspect()
var jo = JToken.ReadFrom(reader);
OnMessageIssued(MessageTypeEnum.Information, string.Format("Running rules for PBI entry \"{0}\"...", entry.Name));
- foreach (var rule in entry.EnabledRules)
- {
- OnMessageIssued(MessageTypeEnum.Information, string.Format("Running Rule \"{0}\".", rule.Name));
- Json.Logic.Rule? jrule = null;
-
- try
- {
- jrule = System.Text.Json.JsonSerializer.Deserialize(rule.Test.Logic);
- }
- catch (System.Text.Json.JsonException e)
- {
- OnMessageIssued(MessageTypeEnum.Error, string.Format("Parsing of logic for rule \"{0}\" failed, resuming to next rule.", rule.Name));
- continue;
- }
-
- //Check if there's a foreach iterator
- if (rule != null && !string.IsNullOrEmpty(rule.ForEachPath))
- {
- var forEachTokens = ExecuteTokensPath(jo, rule.Name, rule.ForEachPath, rule.PathErrorWhenNoMatch);
-
- foreach (var forEachToken in forEachTokens)
- {
-
- var forEachName = !string.IsNullOrEmpty(rule.ForEachPathName) ? ExecuteTokensPath((JObject?)forEachToken, rule.Name, rule.ForEachPathName, rule.PathErrorWhenNoMatch) : null;
- var strForEachName = forEachName != null ? forEachName[0].ToString() : string.Empty;
-
- var forEachDisplayName = !string.IsNullOrEmpty(rule.ForEachPathDisplayName) ? ExecuteTokensPath((JObject?)forEachToken, rule.Name, rule.ForEachPathDisplayName, rule.PathErrorWhenNoMatch) : null;
- var strForEachDisplayName = forEachDisplayName != null ? forEachDisplayName[0].ToString() : string.Empty;
-
- try
- {
- var tokens = ExecuteTokensPath(forEachToken, rule.Name, rule.Path, rule.PathErrorWhenNoMatch);
-
- //HACK
- var contextNodeArray = ConvertToJsonArray(tokens);
-
- bool result = false;
-
- //HACK: checking if the rule's intention is to return an array or a single object
- var node = rule.Path.Contains("*") || rule.Path.Contains("?") ? contextNodeArray : (contextNodeArray != null ? contextNodeArray.FirstOrDefault() : null);
- var newdata = MapRuleDataPointersToValues(node, rule, contextNodeArray);
-
- //TODO: the following commented line does not work with the variableRule implementation with context array passed in.
- //var jruleresult = jrule.Apply(newdata, contextNodeArray);
- var jruleresult = jrule.Apply(newdata);
- result = rule.Test.Expected.IsEquivalentTo(jruleresult);
- var ruleLogType = ConvertRuleLogType(rule.LogType);
- string resultString = string.Concat("\"", strForEachDisplayName, "\" - ", string.Format("Rule \"{0}\" {1} with result: {2}, expected: {3}.", rule != null ? rule.Name : string.Empty, result ? "PASSED" : "FAILED", jruleresult != null ? jruleresult.ToString() : string.Empty, rule.Test.Expected != null ? rule.Test.Expected.ToString() : string.Empty));
-
- //yield return new TestResult { RuleName = rule.Name, ParentName = strForEachName, ParentDisplayName = strForEachDisplayName, Pass = result, Message = resultString, Expected = rule.Test.Expected, Actual = jruleresult};
- testResults.Add(new TestResult { RuleId = rule.Id, RuleName = rule.Name, LogType = ruleLogType, RuleDescription = rule.Description, ParentName = strForEachName, ParentDisplayName = strForEachDisplayName, Pass = result, Message = resultString, Expected = rule.Test.Expected, Actual = jruleresult });
- }
- catch (PBIXInspectorException e)
- {
- testResults.Add(new TestResult { RuleId = rule.Id, RuleName = rule.Name, LogType = MessageTypeEnum.Error, RuleDescription = rule.Description, ParentName = strForEachName, ParentDisplayName = strForEachDisplayName, Pass = false, Message = e.Message, Expected = rule.Test.Expected, Actual = null });
- continue;
- }
- }
- }
- else
- { //TODO: refactor else branch to reuse code from true branch
- var tokens = ExecuteTokensPath(jo, rule.Name, rule.Path, rule.PathErrorWhenNoMatch);
-
- //HACK
- var contextNodeArray = ConvertToJsonArray(tokens);
+ testResults.AddRange(Inspect(jo, entry));
- bool result = false;
-
- try
- {
- //HACK: checking if the rule's intention is to return an array or a single object
- var node = rule.Path.Contains("*") || rule.Path.Contains("?") ? contextNodeArray : (contextNodeArray != null ? contextNodeArray.FirstOrDefault() : null);
- var newdata = MapRuleDataPointersToValues(node, rule, contextNodeArray);
-
- //TODO: the following commented line does not work with the variableRule implementation with context array passed in.
- //var jruleresult = jrule.Apply(newdata, contextNodeArray);
- var jruleresult = jrule.Apply(newdata);
- result = rule.Test.Expected.IsEquivalentTo(jruleresult);
- var ruleLogType = ConvertRuleLogType(rule.LogType);
- string resultString = string.Format("Rule \"{0}\" {1} with result: {2}, expected: {3}.", rule != null ? rule.Name : string.Empty, result ? "PASSED" : "FAILED", jruleresult != null ? jruleresult.ToString() : string.Empty, rule.Test.Expected != null ? rule.Test.Expected.ToString() : string.Empty);
-
- //yield return new TestResult { RuleName = rule.Name, Pass = result, Message = resultString, Expected = rule.Test.Expected, Actual = jruleresult };
- testResults.Add(new TestResult { RuleId = rule.Id, RuleName = rule.Name, LogType = ruleLogType, RuleDescription = rule.Description, Pass = result, Message = resultString, Expected = rule.Test.Expected, Actual = jruleresult });
- }
- catch (PBIXInspectorException e)
- {
- testResults.Add(new TestResult { RuleId = rule.Id, RuleName = rule.Name, LogType = MessageTypeEnum.Error, RuleDescription = rule.Description, Pass = false, Message = e.Message, Expected = rule.Test.Expected, Actual = null });
- continue;
- }
- }
- }
break;
}
case EntryContentTypeEnum.text:
@@ -274,28 +164,248 @@ public IEnumerable Inspect()
return testResults;
}
- private MessageTypeEnum ConvertRuleLogType(string ruleLogType)
+
+ ///
+ /// Runs inspection rules against a PBI file. Only a PBIP report.json stream is currently supported as PBI file input.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public IEnumerable Inspect(Stream pbiInputStream, Stream rulesStream, PbiFile.PBIFileTypeEnum pbiInputFileType = PbiFile.PBIFileTypeEnum.PBIPReport)
+ {
+ if (pbiInputFileType != PbiFile.PBIFileTypeEnum.PBIPReport) throw new NotImplementedException("Only PBIPReport file type is currently supported by this method.");
+ if (pbiInputStream == null) throw new ArgumentNullException(nameof(pbiInputStream));
+ if (rulesStream == null) throw new ArgumentNullException(nameof(rulesStream));
+
+ IEnumerable inspectionResults;
+
+ using (var reader = new StreamReader(pbiInputStream))
+ using (var jsonReader = new JsonTextReader(reader))
+ {
+ var jt = JToken.Load(jsonReader);
+
+ try
+ {
+ var inspectionRules = Inspector.DeserialiseRules(rulesStream);
+ inspectionResults = this.Inspect(jt, inspectionRules);
+ }
+ catch (System.Text.Json.JsonException ex)
+ {
+ throw new PBIXInspectorException("An error occurred during inspection", ex);
+
+ }
+ return inspectionResults;
+ }
+ }
+
+ protected void OnMessageIssued(MessageTypeEnum messageType, string message)
+ {
+ var args = new MessageIssuedEventArgs(message, messageType);
+ OnMessageIssued(args);
+ }
+
+ protected virtual void OnMessageIssued(MessageIssuedEventArgs e)
+ {
+ EventHandler? handler = MessageIssued;
+ if (handler != null)
+ {
+ handler(this, e);
+ }
+ }
+
+ #region private methods
+
+ private void AddCustomRulesToRegistry()
+ {
+ //TODO: Use reflection to add rules
+ /*
+ System.Reflection.Assembly assembly = Assembly.GetExecutingAssembly();
+ string nspace = "PBIXInspectorLibrary.CustomRules";
+
+ var q = from t in Assembly.GetExecutingAssembly().GetTypes()
+ where t.IsClass && t.Namespace == nspace
+ select t;
+ q.ToList().ForEach(t1 => Json.Logic.RuleRegistry.AddRule());
+ */
+
+ Json.Logic.RuleRegistry.AddRule();
+ Json.Logic.RuleRegistry.AddRule();
+ Json.Logic.RuleRegistry.AddRule();
+ Json.Logic.RuleRegistry.AddRule();
+ Json.Logic.RuleRegistry.AddRule();
+ Json.Logic.RuleRegistry.AddRule();
+ Json.Logic.RuleRegistry.AddRule();
+ Json.Logic.RuleRegistry.AddRule();
+ Json.Logic.RuleRegistry.AddRule();
+ Json.Logic.RuleRegistry.AddRule();
+ Json.Logic.RuleRegistry.AddRule();
+ Json.Logic.RuleRegistry.AddRule();
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private IEnumerable Inspect(JToken? jo, InspectionRules? rules)
+ {
+ if (jo == null) throw new ArgumentNullException(nameof(jo));
+ if (rules == null) throw new ArgumentNullException(nameof(rules));
+
+ var testResults = new List();
+
+ foreach (var entry in rules.PbiEntries)
+ {
+ testResults.AddRange(Inspect(jo, entry));
+ }
+
+ return testResults;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private IEnumerable Inspect(JToken? jo, PbiEntry? entry)
{
- if (string.IsNullOrEmpty(ruleLogType)) return MessageTypeEnum.Warning;
+ if (jo == null) throw new ArgumentNullException(nameof(jo));
+ if (entry == null) throw new ArgumentNullException(nameof(entry));
- MessageTypeEnum logType;
+ var testResults = new List();
- switch (ruleLogType.ToLower().Trim())
+ if (entry.ContentType.ToLower() != "json")
{
- case "error":
- logType = MessageTypeEnum.Error;
- break;
- case "warning":
- logType = MessageTypeEnum.Warning;
- break;
- default:
- logType = MessageTypeEnum.Warning;
- break;
+ OnMessageIssued(MessageTypeEnum.Error, string.Format("PBI entry \"{0}\" with content type \"{1}\" is not supported, resuming to next entry.", entry.Name, entry.ContentType));
+ return testResults;
+ }
+
+ OnMessageIssued(MessageTypeEnum.Information, string.Format("Running rules for PBI entry \"{0}\"...", entry.Name));
+ foreach (var rule in entry.EnabledRules)
+ {
+ OnMessageIssued(MessageTypeEnum.Information, string.Format("Running Rule \"{0}\".", rule.Name));
+ Json.Logic.Rule? jrule = null;
+
+ try
+ {
+ jrule = System.Text.Json.JsonSerializer.Deserialize(rule.Test.Logic);
+ }
+ catch (System.Text.Json.JsonException e)
+ {
+ OnMessageIssued(MessageTypeEnum.Error, string.Format("Parsing of logic for rule \"{0}\" failed, resuming to next rule.", rule.Name));
+ continue;
+ }
+
+ //Check if there's a foreach iterator
+ if (rule != null && !string.IsNullOrEmpty(rule.ForEachPath))
+ {
+ var forEachTokens = ExecuteTokensPath(jo, rule.Name, rule.ForEachPath, rule.PathErrorWhenNoMatch);
+
+ foreach (var forEachToken in forEachTokens)
+ {
+
+ var forEachName = !string.IsNullOrEmpty(rule.ForEachPathName) ? ExecuteTokensPath((JObject?)forEachToken, rule.Name, rule.ForEachPathName, rule.PathErrorWhenNoMatch) : null;
+ var strForEachName = forEachName != null ? forEachName[0].ToString() : string.Empty;
+
+ var forEachDisplayName = !string.IsNullOrEmpty(rule.ForEachPathDisplayName) ? ExecuteTokensPath((JObject?)forEachToken, rule.Name, rule.ForEachPathDisplayName, rule.PathErrorWhenNoMatch) : null;
+ var strForEachDisplayName = forEachDisplayName != null ? forEachDisplayName[0].ToString() : string.Empty;
+
+ try
+ {
+ var tokens = ExecuteTokensPath(forEachToken, rule.Name, rule.Path, rule.PathErrorWhenNoMatch);
+
+ //HACK
+ var contextNodeArray = ConvertToJsonArray(tokens);
+
+ bool result = false;
+
+ //HACK: checking if the rule's intention is to return an array or a single object
+ var node = rule.Path.Contains("*") || rule.Path.Contains("?") ? contextNodeArray : (contextNodeArray != null ? contextNodeArray.FirstOrDefault() : null);
+ var newdata = MapRuleDataPointersToValues(node, rule, contextNodeArray);
+
+ //TODO: the following commented line does not work with the variableRule implementation with context array passed in.
+ //var jruleresult = jrule.Apply(newdata, contextNodeArray);
+ var jruleresult = jrule.Apply(newdata);
+ result = rule.Test.Expected.IsEquivalentTo(jruleresult);
+ var ruleLogType = ConvertRuleLogType(rule.LogType);
+ string resultString = string.Concat("\"", strForEachDisplayName, "\" - ", string.Format("Rule \"{0}\" {1} with result: {2}, expected: {3}.", rule != null ? rule.Name : string.Empty, result ? "PASSED" : "FAILED", jruleresult != null ? jruleresult.ToString() : string.Empty, rule.Test.Expected != null ? rule.Test.Expected.ToString() : string.Empty));
+
+ //yield return new TestResult { RuleName = rule.Name, ParentName = strForEachName, ParentDisplayName = strForEachDisplayName, Pass = result, Message = resultString, Expected = rule.Test.Expected, Actual = jruleresult};
+ testResults.Add(new TestResult { RuleId = rule.Id, RuleName = rule.Name, LogType = ruleLogType, RuleDescription = rule.Description, ParentName = strForEachName, ParentDisplayName = strForEachDisplayName, Pass = result, Message = resultString, Expected = rule.Test.Expected, Actual = jruleresult });
+ }
+ catch (PBIXInspectorException e)
+ {
+ testResults.Add(new TestResult { RuleId = rule.Id, RuleName = rule.Name, LogType = MessageTypeEnum.Error, RuleDescription = rule.Description, ParentName = strForEachName, ParentDisplayName = strForEachDisplayName, Pass = false, Message = e.Message, Expected = rule.Test.Expected, Actual = null });
+ continue;
+ }
+ }
+ }
+ else
+ { //TODO: refactor else branch to reuse code from true branch
+ var tokens = ExecuteTokensPath(jo, rule.Name, rule.Path, rule.PathErrorWhenNoMatch);
+
+ //HACK
+ var contextNodeArray = ConvertToJsonArray(tokens);
+
+ bool result = false;
+
+ try
+ {
+ //HACK: checking if the rule's intention is to return an array or a single object
+ var node = rule.Path.Contains("*") || rule.Path.Contains("?") ? contextNodeArray : (contextNodeArray != null ? contextNodeArray.FirstOrDefault() : null);
+ var newdata = MapRuleDataPointersToValues(node, rule, contextNodeArray);
+
+ //TODO: the following commented line does not work with the variableRule implementation with context array passed in.
+ //var jruleresult = jrule.Apply(newdata, contextNodeArray);
+ var jruleresult = jrule.Apply(newdata);
+ result = rule.Test.Expected.IsEquivalentTo(jruleresult);
+ var ruleLogType = ConvertRuleLogType(rule.LogType);
+ string resultString = string.Format("Rule \"{0}\" {1} with result: {2}, expected: {3}.", rule != null ? rule.Name : string.Empty, result ? "PASSED" : "FAILED", jruleresult != null ? jruleresult.ToString() : string.Empty, rule.Test.Expected != null ? rule.Test.Expected.ToString() : string.Empty);
+
+ //yield return new TestResult { RuleName = rule.Name, Pass = result, Message = resultString, Expected = rule.Test.Expected, Actual = jruleresult };
+ testResults.Add(new TestResult { RuleId = rule.Id, RuleName = rule.Name, LogType = ruleLogType, RuleDescription = rule.Description, Pass = result, Message = resultString, Expected = rule.Test.Expected, Actual = jruleresult });
+ }
+ catch (PBIXInspectorException e)
+ {
+ testResults.Add(new TestResult { RuleId = rule.Id, RuleName = rule.Name, LogType = MessageTypeEnum.Error, RuleDescription = rule.Description, Pass = false, Message = e.Message, Expected = rule.Test.Expected, Actual = null });
+ continue;
+ }
+ }
}
- return logType;
+ return testResults;
}
+ private MessageTypeEnum ConvertRuleLogType(string ruleLogType)
+ {
+ if (string.IsNullOrEmpty(ruleLogType)) return MessageTypeEnum.Warning;
+
+ MessageTypeEnum logType;
+
+ switch (ruleLogType.ToLower().Trim())
+ {
+ case "error":
+ logType = MessageTypeEnum.Error;
+ break;
+ case "warning":
+ logType = MessageTypeEnum.Warning;
+ break;
+ default:
+ logType = MessageTypeEnum.Warning;
+ break;
+ }
+
+ return logType;
+ }
+
private JsonArray ConvertToJsonArray(List? tokens)
{
List? nodes = new();
@@ -497,7 +607,7 @@ private JsonArray ConvertToJsonArray(List? tokens)
return newdata;
}
- internal bool EvalPath(string pathString, JsonNode? data, out JsonNode? result)
+ private bool EvalPath(string pathString, JsonNode? data, out JsonNode? result)
{
if (pathString.Contains(DRILLCHAR))
{
@@ -546,7 +656,6 @@ internal bool EvalPath(string pathString, JsonNode? data, out JsonNode? result)
return false;
}
-
private Encoding GetEncodingFromCodePage(int codePage)
{
var enc = Encoding.Unicode;
@@ -563,19 +672,6 @@ private Encoding GetEncodingFromCodePage(int codePage)
return enc;
}
- protected void OnMessageIssued(MessageTypeEnum messageType, string message)
- {
- var args = new MessageIssuedEventArgs(message, messageType);
- OnMessageIssued(args);
- }
-
- protected virtual void OnMessageIssued(MessageIssuedEventArgs e)
- {
- EventHandler? handler = MessageIssued;
- if (handler != null)
- {
- handler(this, e);
- }
- }
+ #endregion
}
}
diff --git a/PBIXInspectorLibrary/InspectorBase.cs b/PBIXInspectorLibrary/InspectorBase.cs
index 302d9bd..be006af 100644
--- a/PBIXInspectorLibrary/InspectorBase.cs
+++ b/PBIXInspectorLibrary/InspectorBase.cs
@@ -1,33 +1,53 @@
//using Newtonsoft.Json;
+using PBIXInspectorLibrary.CustomRules;
using System.Text.Json;
namespace PBIXInspectorLibrary
{
public class InspectorBase
{
- public InspectorBase(string pbixFilePath, InspectionRules inspectionRules)
+ public InspectorBase()
{
}
- public InspectorBase(string pbixFilePath, string rulesFilePath)
+ public InspectorBase(string pbiFilePath, InspectionRules inspectionRules)
{
+ if (string.IsNullOrEmpty(pbiFilePath)) throw new ArgumentNullException(nameof(pbiFilePath));
+ if (!File.Exists(pbiFilePath)) throw new FileNotFoundException();
+ }
+ public InspectorBase(string pbiFilePath, string rulesFilePath)
+ {
+ if (string.IsNullOrEmpty(pbiFilePath)) throw new ArgumentNullException(nameof(pbiFilePath));
}
- public T? DeserialiseRules(string rulesFilePath)
+ public T? DeserialiseRulesFromFilePath(string rulesFilePath)
{
- if (!File.Exists(rulesFilePath)) throw new FileNotFoundException();
+ if (!File.Exists(rulesFilePath)) throw new FileNotFoundException(string.Format("File with path \"{0}\" was not found", rulesFilePath));
string jsonString = File.ReadAllText(rulesFilePath);
- //return JsonConvert.DeserializeObject(jsonString);
+
+ return DeserialiseRules(jsonString);
+
+ }
+
+ public static T? DeserialiseRules(string jsonString)
+ {
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
return JsonSerializer.Deserialize(jsonString, options);
-
}
+ public static T? DeserialiseRules(Stream jsonStream)
+ {
+ var options = new JsonSerializerOptions
+ {
+ PropertyNameCaseInsensitive = true
+ };
+ return JsonSerializer.Deserialize(jsonStream, options);
+ }
}
}
diff --git a/PBIXInspectorLibrary/PBIXInspectorLibrary.csproj b/PBIXInspectorLibrary/PBIXInspectorLibrary.csproj
index f281f71..6bea2a0 100644
--- a/PBIXInspectorLibrary/PBIXInspectorLibrary.csproj
+++ b/PBIXInspectorLibrary/PBIXInspectorLibrary.csproj
@@ -1,14 +1,15 @@
- net6.0
+ net8.0
enable
enable
+ False
-
+
diff --git a/PBIXInspectorLibrary/PbiFile.cs b/PBIXInspectorLibrary/PbiFile.cs
index 5df8e5d..1c2acbc 100644
--- a/PBIXInspectorLibrary/PbiFile.cs
+++ b/PBIXInspectorLibrary/PbiFile.cs
@@ -1,6 +1,6 @@
namespace PBIXInspectorLibrary
{
- internal abstract class PbiFile : IDisposable
+ public abstract class PbiFile : IDisposable
{
private string _filePath = null;
private FileInfo _fileInfo = null;
diff --git a/PBIXInspectorTests/PBIXInspectorTests.csproj b/PBIXInspectorTests/PBIXInspectorTests.csproj
index a62d17b..655ca4e 100644
--- a/PBIXInspectorTests/PBIXInspectorTests.csproj
+++ b/PBIXInspectorTests/PBIXInspectorTests.csproj
@@ -1,7 +1,7 @@
- net6.0
+ net8.0
enable
enable
diff --git a/PBIXInspectorWinForm/PBIXInspectorWinForm.csproj b/PBIXInspectorWinForm/PBIXInspectorWinForm.csproj
index d941b56..322c4ff 100644
--- a/PBIXInspectorWinForm/PBIXInspectorWinForm.csproj
+++ b/PBIXInspectorWinForm/PBIXInspectorWinForm.csproj
@@ -2,13 +2,13 @@
WinExe
- net6.0-windows
+ net8.0-windows7.0
enable
true
enable
- 1.9.5.0
- 1.9.5
- 1.9.5
+ 1.9.6.0
+ 1.9.6
+ 1.9.6
README.md
pbiinspector.png
https://github.com/NatVanG/PBIXInspector
@@ -23,7 +23,7 @@
- PreserveNewest
+ Always
diff --git a/PBIXInspectorWinForm/PBIXInspectorWinForm.pbitool.json b/PBIXInspectorWinForm/PBIXInspectorWinForm.pbitool.json
index f34735a..d6628ce 100644
--- a/PBIXInspectorWinForm/PBIXInspectorWinForm.pbitool.json
+++ b/PBIXInspectorWinForm/PBIXInspectorWinForm.pbitool.json
@@ -1,5 +1,5 @@
{
- "version": "1.9.5",
+ "version": "1.9.6",
"name": "VisOps with PBI Inspector",
"description": "A testing or inspection tool for the visual layer of Microsoft Power BI reports.",
"path": "%PATH%",
diff --git a/PBIXInspectorWinForm/Properties/PublishProfiles/FolderProfile-dotnet.pubxml b/PBIXInspectorWinForm/Properties/PublishProfiles/FolderProfile-dotnet.pubxml
new file mode 100644
index 0000000..92859c6
--- /dev/null
+++ b/PBIXInspectorWinForm/Properties/PublishProfiles/FolderProfile-dotnet.pubxml
@@ -0,0 +1,20 @@
+
+
+
+
+ Release
+ Any CPU
+ bin\Release\net8.0-windows7.0\publish-dotnet\
+ FileSystem
+ <_TargetId>Folder
+ net8.0-windows7.0
+ win-x64
+ true
+ true
+ false
+ None
+ False
+
+
\ No newline at end of file
diff --git a/PBIXInspectorWinForm/Properties/PublishProfiles/FolderProfile.pubxml b/PBIXInspectorWinForm/Properties/PublishProfiles/FolderProfile.pubxml
new file mode 100644
index 0000000..1540634
--- /dev/null
+++ b/PBIXInspectorWinForm/Properties/PublishProfiles/FolderProfile.pubxml
@@ -0,0 +1,20 @@
+
+
+
+
+ Release
+ Any CPU
+ bin\Release\net8.0-windows7.0\publish\
+ FileSystem
+ <_TargetId>Folder
+ net8.0-windows7.0
+ win-x64
+ false
+ true
+ false
+ None
+ False
+
+
\ No newline at end of file
diff --git a/PBIXInspectorWinLibrary/PBIXInspectorWinLibrary.csproj b/PBIXInspectorWinLibrary/PBIXInspectorWinLibrary.csproj
index 5b6dd9d..9f054a5 100644
--- a/PBIXInspectorWinLibrary/PBIXInspectorWinLibrary.csproj
+++ b/PBIXInspectorWinLibrary/PBIXInspectorWinLibrary.csproj
@@ -1,12 +1,12 @@
- net6.0
+ net8.0
enable
enable
- 1.9.5.0
- 1.9.5.0
- 1.9.5
+ 1.9.6.0
+ 1.9.6.0
+ 1.9.6