========================== IAdHocExtension Samples ========================== This article contains integration mode full-code samples for the **IAdHocExtension** APIs, which allow customization code to hook in the report life cycle. Reference Izenda libraries in a New Project ------------------------------------------- #. .. _IAdHocExtension_Sample_Project: .. figure:: /_static/images/IAdHocExtension_Sample_Project.png :align: right :width: 706px New Project Create a new web project in Visual Studio. #. Name it IAdHocExtensionSample. #. Select a location (e.g. D:\\Projects). #. Select to create new solution. #. Give the solution name IAdHocExtensionSample. #. Tick the Create directory for solution check-box. #. Click OK to create the project and solution. |br| #. Create a new folder for Izenda libraries D:\\Projects\\IAdhocExtensionSample\\IAdhocExtensionSample\\izendadll. #. Copy the file Izenda.BI.Framework.dll from Izenda installation folder into the newly-created folder. #. Open Solution Explorer, right-click References in project IAdHocExtensionSample and select Add Reference. #. In Reference Manager pop-up, click Browse and select the file Izenda.BI.Framework.dll (in D:\\Projects\\SetupHiddenFilter). #. Verify the interface by double-clicking Izenda.BI.Framework in References to open Object Browser. #. Expand the nodes to Izenda.BI.Framework.CustomConfiguration and select DefaultAdHocExtension class to see the methods to override. #. Similarly reference the library Izenda.BI.Core.dll and the third-party Newtonsoft.Json.dll. #. Also reference System.Composition.AttributedModel.dll (https://www.nuget.org/packages/System.Composition.AttributedModel version 1.0.31) and System.Web (in Assemblies > Framework). .. figure:: /_static/images/DefaultAdHocExtension_class.png DefaultAdHocExtension class Sample Method Implementations ----------------------------- .. comment: Not highlighted: Pygments does not support interpolated string in C# 6 yet https://bitbucket.org/birkenfeld/pygments-main/issues/1138/supporting-c-60 .. container:: toggle .. container:: header Full sample code: .. code-block:: csharp using Izenda.BI.Core; using Izenda.BI.Core.QueryEngine; using Izenda.BI.Framework.Components.QueryExpressionTree; using Izenda.BI.Framework.Constants; using Izenda.BI.Framework.CustomConfiguration; using Izenda.BI.Framework.Models; using Izenda.BI.Framework.Models.Common; using Izenda.BI.Framework.Models.ReportDesigner; using Izenda.BI.Framework.Utility; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Composition.AttributedModel.dll; using Izenda.BI.Framework.Components.QueryExpressionTree.Operator; using Operator = Izenda.BI.Framework.Enums.DateTimeOperator; namespace IAdHocExtensionSample { [Export(typeof(IAdHocExtension))] public class AdHocExtensionSample : DefaultAdHocExtension { /// /// Sample Requirement: /// If logged user has role Manager --> look up "South America" => "SA", "North America" => "NA" /// If logged user has role Employee --> look up "Europe" => "EU" /// /// /// /// public override List OnPostLoadFilterData(ReportFilterField filterField, List data) { // override dropdown value based on user role for filter on view "OrderDetailsByRegion" and field "CountryRegionName" if (filterField.SourceDataObjectName == "OrderDetailsByRegion" && filterField.SourceFieldName == "CountryRegionName" && (HttpContext.Current.User.IsInRole("Manager") || HttpContext.Current.User.IsInRole("Employee"))) { // override dropdown's value based on User role //Manager, look up "South America" => "SA", "North America" => "NA" if (HttpContext.Current.User.IsInRole("Manager")) { var indexSA = data.IndexOf("South America"); if (indexSA != -1) data[indexSA] = "SA"; var indexNA = data.IndexOf("North America"); if (indexNA != -1) data[indexNA] = "NA"; } // Employee, look up "Europe" => "EU" if (HttpContext.Current.User.IsInRole("Employee")) { var indexEU = data.IndexOf("Europe"); if (indexEU != -1) data[indexEU] = "EU"; } } return base.OnPostLoadFilterData(filterField, data); } /// /// Sample Requirement: /// If logged user has role Manager --> show some regions ("South America", "North America") /// If logged user has role Employee --> show only one region ("Europe") /// /// /// public override List OnLoadFilterDataTree(QuerySourceFieldInfo fieldInfo) { var result = new List(); if (fieldInfo.QuerySourceName == "OrderDetailsByRegion" && fieldInfo.Name == "CountryRegionName" && (HttpContext.Current.User.IsInRole("Manager") || HttpContext.Current.User.IsInRole("Employee"))) { //Node [All] is required for UI to render. var rootNode = new ValueTreeNode { Text = "[All]", Value = "[All]" }; rootNode.Nodes = new List(); if (HttpContext.Current.User.IsInRole("Manager")) { rootNode.Nodes.Add(new ValueTreeNode { Text = "South America", Value = "South America" }); rootNode.Nodes.Add(new ValueTreeNode { Text = "North America", Value = "North America" }); } if (HttpContext.Current.User.IsInRole("Employee")) { rootNode.Nodes.Add(new ValueTreeNode { Text = "Europe", Value = "Europe" }); } result.Add(rootNode); } return result; } /// /// Sample Requirement: /// If logged user has role Manager --> show some regions ("WA", "[Blank]") /// If logged user has role Employee --> show only one region ("Europe") /// /// /// public override ReportFilterSetting SetHiddenFilters(SetHiddenFilterParam param) { var filterFieldName = "ShipRegion"; Func addHiddenFilters = (result, filterPosition, querySource, field, equalOperator, rel) => { var firstFilter = new ReportFilterField { Alias = $"ShipRegion{filterPosition}", QuerySourceId = querySource.Id, SourceDataObjectName = querySource.Name, QuerySourceType = querySource.Type, QuerySourceFieldId = field.Id, SourceFieldName = field.Name, DataType = field.DataType, Position = ++filterPosition, OperatorId = equalOperator, Value = "WA", RelationshipId = rel?.Id, IsParameter = false, ReportFieldAlias = null }; var secondFilter = new ReportFilterField { Alias = $"ShipRegion{filterPosition}", QuerySourceId = querySource.Id, SourceDataObjectName = querySource.Name, QuerySourceType = querySource.Type, QuerySourceFieldId = field.Id, SourceFieldName = field.Name, DataType = field.DataType, Position = ++filterPosition, OperatorId = equalOperator, Value = "[Blank]", RelationshipId = rel?.Id, IsParameter = false, ReportFieldAlias = null }; result.FilterFields.Add(firstFilter); result.FilterFields.Add(secondFilter); var logic = $"({filterPosition - 1} OR {filterPosition})"; if (string.IsNullOrEmpty(result.Logic)) { result.Logic = logic; } return filterPosition; }; var filterSetting = new ReportFilterSetting() { FilterFields = new List() }; var position = 0; var ds = param.ReportDefinition.ReportDataSource; // Build the hidden filters for CountryRegionName foreach (var querySource in param.QuerySources // Scan thru the query sources that are involved in the report .Where(x => x.QuerySourceFields.Any(y => y.Name.Equals(filterFieldName, StringComparison.OrdinalIgnoreCase)))) // Take only query sources that have filter field name { // Pick the relationships that joins the query source as primary source // Setting the join ensure the proper table is assigned when using join alias in the UI var rels = param.ReportDefinition.ReportRelationship. Where(x => x.JoinQuerySourceId == querySource.Id) .ToList(); // Find actual filter field in query source var field = querySource.QuerySourceFields.FirstOrDefault(x => x.Name.Equals(filterFieldName, StringComparison.OrdinalIgnoreCase)); // Pick the equal operator var equalOperator = Izenda.BI.Framework.Enums.FilterOperator.FilterOperator.EqualsManualEntry.GetUid(); // In case there is no relationship that the query source is joined as primary if (rels.Count() == 0) { // Just add hidden filter with null relationship position = addHiddenFilters(filterSetting, position, querySource, field, equalOperator, null); } else { // Loop thru all relationships that the query source is joined as primary and add the hidden field associated with each relationship foreach (var rel in rels) { position = addHiddenFilters(filterSetting, position, querySource, field, equalOperator, rel); } } } return filterSetting; } /// /// Sample Requirement: /// Remove all Map report parts before running /// /// /// public override ReportDefinition OnPreExecute(ReportDefinition report) { if (report.ReportPart.Any(x => x.ReportPartContent.Type == ReportPartContentType.Map)) { var filteredReportPart = report.ReportPart.Where(x => x.ReportPartContent.Type != ReportPartContentType.Map).ToList(); report.ReportPart = filteredReportPart; } return report; } /// /// Sample Requirement: /// Limit the execution result to the first 1000 rows only (although the database may return more than that) /// /// /// public override List> OnPostExecute(QueryTree executedQueryTree, List> result) { return result.Take(1000).ToList(); } /// /// Sample Requirement: /// Log the queries without result limit operator /// /// /// public override QueryTree OnExecuting(QueryTree queryTree) { var nodeVisitor = new QueryTreePathAnalyzeVisitor(new ExtensibilityFactory(), queryTree.ContextData); nodeVisitor.ContextData = queryTree.ContextData; queryTree.Root.Accept(nodeVisitor); var resultLimitOperator = new ResultLimitOperator() { ChildOperand = new Operand() { QuerySource = new QuerySource() } }; try { nodeVisitor.Visit(resultLimitOperator); } catch (Exception) { Console.WriteLine("LOG: Query with no limit"); } return queryTree; } /// /// Sample Requirement: /// If report filter includes only OrderDetailsByRegion.CountryRegionName, return pre-defined list and skip querying the database /// If not, let system query the database /// /// /// public override List OnPreLoadFilterData(ReportFilterSetting filterSetting, out bool handled) { handled = false; List result = null; if (filterSetting.FilterFields.Count == 1 && filterSetting.FilterFields.Any( x => x.SourceDataObjectName.Equals("OrdersByRegion") && x.SourceFieldName.Equals("CountryRegionName"))) { handled = true; result = new List() { "Europe", "North America", "South America" }; } return result; } /// /// Sample Requirement: /// Returns a custom list of 6 In Time Periods /// public override List LoadCustomTimePeriod() { var result = new List { new CustomTimePeriod("Tomorrow", DateTime.Now, DateTime.Now.AddDays(1), Operator.BetweenDateTime), new CustomTimePeriod("Previous Date -> DateTime Now", () => DateTime.Now.AddDays(-1), () => DateTime.Now, Operator.BetweenDateTime), new CustomTimePeriod("Less Than 2 Days Old", 2, Operator.LessThanDaysOld), new CustomTimePeriod("Greater Than 2 Days Old", () => 2, Operator.GreaterThanDaysOld), new CustomTimePeriod(">= Date Time Now + 2 Days", DateTime.Now.AddDays(2), Operator.GreaterThanOrEqualsCalender), new CustomTimePeriod("<= Date Time Now - 2 Days", () => DateTime.Now.AddDays(-2), Operator.LessThanOrEqualsCalendar) }; // using Operator = Izenda.BI.Framework.Enums.DateTimeOperator; return result; } /// /// Sample Requirement: /// Returns a custom list of 3 data formats /// public override List LoadCustomDataFormat() { var result = new List { new DataFormat { Name = "0,000", DataType = DataType.Numeric, Category = IzendaKey.CustomFormat, FormatFunc = (x) => { return ((int)x).ToString("0,000"); } }, new DataFormat { Name = "$0,000", DataType = DataType.Numeric, Category = IzendaKey.CustomFormat, FormatFunc = (x) => { return ((int)x).ToString("$0,000"); } }, new DataFormat { Name = "$0,000", DataType = DataType.Numeric, Category = IzendaKey.CustomFormat, FormatFunc = (x) => { return ((decimal)x).ToString("C0"); } } }; return result; } } } Add the New Library ------------------- #. Build then copy the IAdhocExtensionSample.dll file (it can be found at D:\\Projects\\IAdhocExtensionSample\\IAdhocExtensionSample\\bin) to the Izenda Back-end API folder (at C:\\inetpub\\wwwroot\\Izenda\\API\\bin). #. Restart the Izenda Back-end website. UnitTest for the Samples ------------------------ Add UnitTest Project ~~~~~~~~~~~~~~~~~~~~ #. Rick click Solution in Solution Explorer and select Add > New Project. #. Add a Class Library project named AdHocExtensionSampleTest. #. Reference the project IAdHocExtensionSample (Add Reference and tick IAdHocExtensionSample in Projects > Solution). #. Also reference System.Web (in Assemblies > Framework). #. Reference xUnit Library. #. Open NuGet Package Manager pop-up from Tools > NuGet Package Manager > Manage NuGet Packages for Solution... #. Click Browse tab and enter xunit in the text box to search. #. Select xunit on the left and tick the AdHocExtensionSampleTest project check-box on the right. #. Select version 2.2.0 (working at the time of writing) and click Install. #. Similarly install xunit.runner.visualstudio version 2.2.0 to AdHocExtensionSampleTest project. Implement the UnitTests ~~~~~~~~~~~~~~~~~~~~~~~ #. Add a MockUser class to help testing: .. container:: toggle .. container:: header Code for MockUser.cs: .. code-block:: csharp using System.Security.Principal; namespace IAdHocExtensionSample { class MockUser: IPrincipal { private string role; public MockUser(string role) { this.role = role; } public IIdentity Identity { get; private set; } public bool IsInRole(string role) { return (role == this.role)? true : false; } } } #. Right-click the default Class1.cs file in Solution Explorer and rename it to AdHocExtensionSampleTest.cs, also agree to change the class name to AdHocExtensionSampleTest when asked. #. Implement the tests in xUnit. .. container:: toggle .. container:: header Full sample code: .. code-block:: csharp using System; using System.Collections.Generic; using Xunit; using System.IO; using System.Web; using Izenda.BI.Framework.Models.ReportDesigner; using Izenda.BI.Framework.Models; using Izenda.BI.Framework.Constants; using Izenda.BI.Framework.Components.QueryExpressionTree; namespace IAdHocExtensionSample { public class AdhocExtensionSampleTest { private static Guid querySourceId1 = Guid.Parse("39984D52-C1DE-4388-9ED2-FB7C8C62FD01"); private static Guid fieldId11 = Guid.Parse("39984D52-C1DE-4388-9ED2-FB7C8C62FD11"); private static Guid fieldId12 = Guid.Parse("39984D52-C1DE-4388-9ED2-FB7C8C62FD12"); private static Guid relationshipId1 = Guid.Parse("39984D52-C1DE-4388-9ED2-FB7C8C62FD03"); /// /// Test Filter Manager /// [Fact] public void Execute_HiddenFilter_Manager_Success() { HttpContext.Current = new HttpContext( new HttpRequest("", "http://tempuri.org", ""), new HttpResponse(new StringWriter()) ); HttpContext.Current.User = new MockUser("Manager"); var param = new SetHiddenFilterParam() { QuerySources = new List() { new QuerySource() { Name = "Orders", Id = querySourceId1, Type = "Table", QuerySourceFields = new List() { new QuerySourceField() { Name = "ShipRegion", Id = fieldId11, DataType = "varchar" } } } }, ReportDefinition = new ReportDefinition() { ReportDataSource = new List(), ReportRelationship = new List() } }; var reportFilterSetting = (new AdHocExtensionSample()).SetHiddenFilters(param); Assert.Equal(reportFilterSetting.FilterFields.Count, 2); Assert.Equal(reportFilterSetting.FilterFields[0].Value, "WA"); Assert.Equal(reportFilterSetting.FilterFields[1].Value, "[Blank]"); } /// /// Test Filter Employee /// [Fact] public void Execute_HiddenFilter_Employee_Success() { HttpContext.Current = new HttpContext( new HttpRequest("", "http://tempuri.org", ""), new HttpResponse(new StringWriter()) ); HttpContext.Current.User = new MockUser("Employee"); var param = new SetHiddenFilterParam() { QuerySources = new List() { new QuerySource() { Name = "Orders", Id = querySourceId1, Type = "Table", QuerySourceFields = new List() { new QuerySourceField() { Name = "ShipRegion", Id = fieldId11, DataType = "varchar" } } } }, ReportDefinition = new ReportDefinition() { ReportDataSource = new List(), ReportRelationship = new List() } }; var reportFilterSetting = (new AdHocExtensionSample()).SetHiddenFilters(param); Assert.Equal(reportFilterSetting.FilterFields.Count, 1); Assert.Equal(reportFilterSetting.FilterFields[0].Value, "Europe"); } /// /// Test Filter Manager with Duplicated FieldAlias /// [Fact] public void Execute_HiddenFilter_Manager_Duplicated_FieldAlias() { HttpContext.Current = new HttpContext( new HttpRequest("", "http://tempuri.org", ""), new HttpResponse(new StringWriter()) ); HttpContext.Current.User = new MockUser("Manager"); var param = new SetHiddenFilterParam() { QuerySources = new List() { new QuerySource() { Name = "Orders", Id = querySourceId1, Type = "Table", QuerySourceFields = new List() { new QuerySourceField() { Name = "ShipRegion", Id = fieldId11, DataType = "varchar" } } }, new QuerySource() { Name = "Customers", Id = querySourceId1, Type = "Table", QuerySourceFields = new List() { new QuerySourceField() { Name = "ShipRegion", Id = fieldId12, DataType = "varchar" } } } }, ReportDefinition = new ReportDefinition() { ReportDataSource = new List(), ReportRelationship = new List() { new Relationship() { JoinQuerySourceId = querySourceId1, Id = relationshipId1 } } } }; var reportFilterSetting = (new AdHocExtensionSample()).SetHiddenFilters(param); Assert.Equal(reportFilterSetting.FilterFields.Count, 4); Assert.Equal(reportFilterSetting.FilterFields[0].Value, "WA"); Assert.Equal(reportFilterSetting.FilterFields[1].Value, "[Blank]"); Assert.Equal(reportFilterSetting.FilterFields[2].Value, "WA"); Assert.Equal(reportFilterSetting.FilterFields[3].Value, "[Blank]"); } /// /// Test OnLoadFilterDataTree Manager /// [Fact] public void Execute_OnLoadFilterDataTree_Manager_Success() { HttpContext.Current = new HttpContext( new HttpRequest("", "http://tempuri.org", ""), new HttpResponse(new StringWriter()) ); HttpContext.Current.User = new MockUser("Manager"); var field = new QuerySourceFieldInfo() { QuerySourceName = "OrderDetailsByRegion", Name = "CountryRegionName" }; var result = (new AdHocExtensionSample()).OnLoadFilterDataTree(field); Assert.Equal(result.Count, 1); Assert.Equal(result[0].Nodes.Count, 2); Assert.Equal(result[0].Nodes[0].Value, "South America"); Assert.Equal(result[0].Nodes[1].Value, "North America"); } /// /// Test OnLoadFilterDataTree Employee /// [Fact] public void Execute_OnLoadFilterDataTree_Employee_Success() { HttpContext.Current = new HttpContext( new HttpRequest("", "http://tempuri.org", ""), new HttpResponse(new StringWriter()) ); HttpContext.Current.User = new MockUser("Employee"); var field = new QuerySourceFieldInfo() { QuerySourceName = "OrderDetailsByRegion", Name = "CountryRegionName" }; var result = (new AdHocExtensionSample()).OnLoadFilterDataTree(field); Assert.Equal(result.Count, 1); Assert.Equal(result[0].Nodes.Count, 1); Assert.Equal(result[0].Nodes[0].Value, "Europe"); } /// /// Test OnPostLoadFilterData Manager /// [Fact] public void Execute_OnPostLoadFilterData_Manager_Success() { HttpContext.Current = new HttpContext( new HttpRequest("", "http://tempuri.org", ""), new HttpResponse(new StringWriter()) ); HttpContext.Current.User = new MockUser("Manager"); var field = new ReportFilterField() { SourceDataObjectName = "OrderDetailsByRegion", SourceFieldName = "CountryRegionName" }; var data = new List() { "Antarctica", "Europe", "North America", "South America" }; var result = (new AdHocExtensionSample()).OnPostLoadFilterData(field, data); Assert.Equal(result.Count, 4); Assert.NotEqual(result.IndexOf("SA"), -1); Assert.NotEqual(result.IndexOf("NA"), -1); } /// /// Test OnPostLoadFilterData Employee /// [Fact] public void Execute_OnPostLoadFilterData_Employee_Success() { HttpContext.Current = new HttpContext( new HttpRequest("", "http://tempuri.org", ""), new HttpResponse(new StringWriter()) ); HttpContext.Current.User = new MockUser("Employee"); var field = new ReportFilterField() { SourceDataObjectName = "OrderDetailsByRegion", SourceFieldName = "CountryRegionName" }; var data = new List() { "Antarctica", "Europe", "North America", "South America" }; var result = (new AdHocExtensionSample()).OnPostLoadFilterData(field, data); Assert.Equal(result.Count, 4); Assert.NotEqual(result.IndexOf("EU"), -1); } /// /// Test OnPreExecute /// [Fact] public void Execute_OnPreExecute_Success() { var originalReport = new ReportDefinition() { ReportPart = new List { new ReportPartDefinition { ReportPartContent = new ReportPartMap { Type = ReportPartContentType.Map } }, new ReportPartDefinition { ReportPartContent = new ReportPartGrid { Type = ReportPartContentType.Grid } }, new ReportPartDefinition { ReportPartContent = new ReportPartMap { Type = ReportPartContentType.Map } } } }; var report = (new AdHocExtensionSample()).OnPreExecute(originalReport); Assert.Equal(report.ReportPart.Count, 1); } /// /// Test OnPostExecute /// [Fact] public void Execute_OnPostExecute_Success() { var originalList = new List>(); for (var i = 1; i <= 1010; i++) { originalList.Add(new Dictionary()); } var qt = new QueryTree(); var list = (new AdHocExtensionSample()).OnPostExecute(qt, originalList); Assert.Equal(list.Count, 1000); } /// /// Test OnPreLoadFilterData /// [Fact] public void Execute_OnPreLoadFilterData_Success() { var filterSetting = new ReportFilterSetting() { FilterFields = new List() { new ReportFilterField() { SourceDataObjectName = "OrdersByRegion", SourceFieldName = "CountryRegionName" } } }; bool handled; var list = (new AdHocExtensionSample()).OnPreLoadFilterData(filterSetting, out handled); Assert.Equal(handled, true); Assert.Equal(list.Count, 3); } /// /// Test LoadCustomTimePeriod /// [Fact] public void Execute_LoadCustomTimePeriod_Success() { var list = (new AdHocExtensionSample()).LoadCustomTimePeriod(); Assert.Equal(list.Count, 6); } /// /// Test LoadCustomDataFormat /// [Fact] public void Execute_LoadCustomDataFormat_Success() { var list = (new AdHocExtensionSample()).LoadCustomDataFormat(); Assert.Equal(list.Count, 3); } } } Run the UnitTests ~~~~~~~~~~~~~~~~~ #. Open Test Explorer from Menu > Test > Windows. #. Click Run All in Test Explorer. #. All the tests should be passed. .. figure:: /_static/images/IAdhocExtension_TestScripts.png Test Result