diff --git a/.editorconfig b/.editorconfig index a3b81254..46da7f34 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,152 +1,146 @@ -# Remove the line below if you want to inherit .editorconfig settings from higher directories -root = true +# editorconfig.org -# C# files -[*.cs] +# Source: https://github.com/dotnet/runtime/blob/main/.editorconfig +# Lines marked with (*) are customizations relative to the upstream source. -#### Core EditorConfig Options #### +# top-most EditorConfig file +root = true -# Indentation and spacing -indent_size = 4 +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation +[*] +insert_final_newline = true indent_style = space -tab_width = 4 - -# New line preferences -end_of_line = crlf -insert_final_newline = false - -#### .NET Coding Conventions #### - -# Organize usings -dotnet_separate_import_directive_groups = false -dotnet_sort_system_directives_first = false -file_header_template = unset - -# this. and Me. preferences -dotnet_style_qualification_for_event = false -dotnet_style_qualification_for_field = false -dotnet_style_qualification_for_method = false -dotnet_style_qualification_for_property = false - -# Language keywords vs BCL types preferences -dotnet_style_predefined_type_for_locals_parameters_members = true -dotnet_style_predefined_type_for_member_access = true - -# Parentheses preferences -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity -dotnet_style_parentheses_in_other_operators = never_if_unnecessary -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity - -# Modifier preferences -dotnet_style_require_accessibility_modifiers = for_non_interface_members - -# Expression-level preferences -dotnet_style_coalesce_expression = true -dotnet_style_collection_initializer = true -dotnet_style_explicit_tuple_names = true -dotnet_style_namespace_match_folder = true -dotnet_style_null_propagation = true -dotnet_style_object_initializer = true -dotnet_style_operator_placement_when_wrapping = beginning_of_line -dotnet_style_prefer_auto_properties = true -dotnet_style_prefer_compound_assignment = true -dotnet_style_prefer_conditional_expression_over_assignment = true -dotnet_style_prefer_conditional_expression_over_return = true -dotnet_style_prefer_inferred_anonymous_type_member_names = true -dotnet_style_prefer_inferred_tuple_names = true -dotnet_style_prefer_is_null_check_over_reference_equality_method = true -dotnet_style_prefer_simplified_boolean_expressions = true -dotnet_style_prefer_simplified_interpolation = true - -# Field preferences -dotnet_style_readonly_field = true - -# Parameter preferences -dotnet_code_quality_unused_parameters = all - -# Suppression preferences -dotnet_remove_unnecessary_suppression_exclusions = none - -# New line preferences -dotnet_style_allow_multiple_blank_lines_experimental = true -dotnet_style_allow_statement_immediately_after_block_experimental = true - -#### C# Coding Conventions #### - -# var preferences -csharp_style_var_elsewhere = false -csharp_style_var_for_built_in_types = false -csharp_style_var_when_type_is_apparent = false - -# Expression-bodied members -csharp_style_expression_bodied_accessors = true -csharp_style_expression_bodied_constructors = false -csharp_style_expression_bodied_indexers = true -csharp_style_expression_bodied_lambdas = true -csharp_style_expression_bodied_local_functions = false -csharp_style_expression_bodied_methods = false -csharp_style_expression_bodied_operators = false -csharp_style_expression_bodied_properties = true - -# Pattern matching preferences -csharp_style_pattern_matching_over_as_with_null_check = true -csharp_style_pattern_matching_over_is_with_cast_check = true -csharp_style_prefer_not_pattern = true -csharp_style_prefer_pattern_matching = true -csharp_style_prefer_switch_expression = true - -# Null-checking preferences -csharp_style_conditional_delegate_call = true - -# Modifier preferences -csharp_prefer_static_local_function = true -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async - -# Code-block preferences -csharp_prefer_braces = true -csharp_prefer_simple_using_statement = true -csharp_style_namespace_declarations = block_scoped +indent_size = 4 +trim_trailing_whitespace = true -# Expression-level preferences -csharp_prefer_simple_default_expression = true -csharp_style_deconstructed_variable_declaration = true -csharp_style_implicit_object_creation_when_type_is_apparent = true -csharp_style_inlined_variable_declaration = true -csharp_style_pattern_local_over_anonymous_function = true -csharp_style_prefer_index_operator = true -csharp_style_prefer_null_check_over_type_check = true -csharp_style_prefer_range_operator = true -csharp_style_throw_expression = true -csharp_style_unused_value_assignment_preference = discard_variable -csharp_style_unused_value_expression_statement_preference = discard_variable - -# 'using' directive preferences -csharp_using_directive_placement = outside_namespace +# Specify UTF-8 without byte-order mark +[*.{csproj,locproj,nativeproj,proj,resx,slnx,vbproj}] +charset = utf-8 -# New line preferences -csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true -csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true -csharp_style_allow_embedded_statements_on_same_line_experimental = true - -#### C# Formatting Rules #### +# Generated code +[*{_AssemblyInfo.cs,.notsupported.cs,AsmOffsets.cs}] +generated_code = true +# C# files +[*.cs] # New line preferences -csharp_new_line_before_catch = true +csharp_new_line_before_open_brace = all csharp_new_line_before_else = true +csharp_new_line_before_catch = true csharp_new_line_before_finally = true -csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_before_members_in_object_initializers = true -csharp_new_line_before_open_brace = all +csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_between_query_expression_clauses = true # Indentation preferences csharp_indent_block_contents = true csharp_indent_braces = false csharp_indent_case_contents = true -csharp_indent_case_contents_when_block = true -csharp_indent_labels = one_less_than_current +csharp_indent_case_contents_when_block = true # (*) upstream: false csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion + +# avoid this. unless absolutely necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Types: use keywords instead of BCL types, and permit var only when the type is clear +csharp_style_var_for_built_in_types = true:suggestion # (*) upstream: false +csharp_style_var_when_type_is_apparent = true:none # (*) upstream: false +csharp_style_var_elsewhere = true:suggestion # (*) upstream: false +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# name all constant fields using PascalCase +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# static fields should have s_ prefix +dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion +dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields +dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static +dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected +dotnet_naming_style.static_prefix_style.required_prefix = s_ +dotnet_naming_style.static_prefix_style.capitalization = camel_case + +# internal and private fields should be _camelCase +dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style +dotnet_naming_symbols.private_internal_fields.applicable_kinds = field +dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case + +# Code style defaults +csharp_using_directive_placement = outside_namespace:suggestion +dotnet_sort_system_directives_first = true +csharp_prefer_braces = true:silent +csharp_preserve_single_line_blocks = true:none +csharp_preserve_single_line_statements = false:none +csharp_prefer_static_local_function = true:suggestion +csharp_prefer_simple_using_statement = false:none +csharp_style_prefer_switch_expression = true:suggestion +dotnet_style_readonly_field = true:suggestion +csharp_style_namespace_declarations = file_scoped:suggestion # (*) added + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_collection_expression = true # (*) upstream: when_types_exactly_match +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion # (*) upstream: true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +csharp_prefer_simple_default_expression = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_constructors = true:silent +csharp_style_expression_bodied_operators = true:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = true:silent + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +# Null checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Other features +csharp_style_prefer_index_operator = false:none +csharp_style_prefer_range_operator = false:none +csharp_style_pattern_local_over_anonymous_function = false:none + +# IDE0071: Simplify interpolation - keep as silent hint since ReadOnlySpan.ToString() is required for netstandard targets +#dotnet_diagnostic.IDE0071.severity = silent (*) removed + +# IDE0031: Use null propagation - keep as silent hint to avoid build errors with TreatWarningsAsErrors +#dotnet_diagnostic.IDE0031.severity = silent (*) removed # Space preferences csharp_space_after_cast = false @@ -172,48 +166,45 @@ csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_parentheses = false csharp_space_between_square_brackets = false -# Wrapping preferences -csharp_preserve_single_line_blocks = true -csharp_preserve_single_line_statements = true - -#### Naming styles #### - -# Naming rules +# Default analyzed API surface = 'all' (public APIs + non-public APIs) +dotnet_code_quality.api_surface = all # (*) added -dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion -dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface -dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i +# License header +#file_header_template = (*) removed -dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.types_should_be_pascal_case.symbols = types -dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case +# C++ Files +[*.{cpp,h,in}] +curly_bracket_next_line = true +indent_brace_style = Allman -dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members -dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +indent_size = 2 -# Symbol specifications +# Xml build files +[*.builds] +indent_size = 2 -dotnet_naming_symbols.interface.applicable_kinds = interface -dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = +# Xml files +[*.{resx,ruleset,slnx,stylecop,xml}] +indent_size = 2 -dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum -dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = - -dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method -dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = +# Xml resource files +[*.resx] +# match Visual Studio behavior +insert_final_newline = false +trim_trailing_whitespace = false -# Naming styles +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case +# Data serialization +[*.{json,yaml,yml}] +indent_size = 2 -dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = -dotnet_naming_style.begins_with_i.capitalization = pascal_case +# Shell scripts +[*.sh] +end_of_line = lf +[*.{cmd,bat}] +end_of_line = crlf diff --git a/src/Bravo.csproj b/src/Bravo.csproj index 9ab49a40..db2588e4 100644 --- a/src/Bravo.csproj +++ b/src/Bravo.csproj @@ -14,6 +14,7 @@ Sqlbi.$(MSBuildProjectName.Replace(" ", "_")) enable 14.0 + disable True $(NoWarn);1591 true diff --git a/src/Controllers/AnalyzeModelController.cs b/src/Controllers/AnalyzeModelController.cs index 1858f14b..f8cf5422 100644 --- a/src/Controllers/AnalyzeModelController.cs +++ b/src/Controllers/AnalyzeModelController.cs @@ -1,209 +1,214 @@ -namespace Sqlbi.Bravo.Controllers +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Mime; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Models; +using Sqlbi.Bravo.Models.AnalyzeModel; +using Sqlbi.Bravo.Services; + +namespace Sqlbi.Bravo.Controllers; + +/// +/// AnalyzeModel module controller +/// +/// Status400BadRequest - See the "instance" and "detail" properties to identify the specific occurrence of the problem +[Route("api/[action]")] +[ApiController] +[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] +public class AnalyzeModelController( + IAnalyzeModelService analyzeModelService, + IAuthenticationService authenticationService) : ControllerBase { - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Models; - using Sqlbi.Bravo.Models.AnalyzeModel; - using Sqlbi.Bravo.Services; - using System.Windows.Forms; + private readonly IAnalyzeModelService _analyzeModelService = analyzeModelService; + private readonly IAuthenticationService _authenticationService = authenticationService; + private readonly SaveFileDialog _exportVpaxDialog = new() + { + Title = "Save VPAX", + Filter = "VPAX file (*.vpax)|*.vpax|Obfuscated VPAX file (*.ovpax)|*.ovpax", + DefaultExt = "vpax", + AddExtension = true, + OverwritePrompt = true, + CheckPathExists = true, + ValidateNames = true + }; /// - /// AnalyzeModel module controller + /// Returns a database model from the VPAX file provided as multipart form data. + /// An optional obfuscation dictionary file can be included to deobfuscate the VPAX. /// - /// Status400BadRequest - See the "instance" and "detail" properties to identify the specific occurrence of the problem - [Route("api/[action]")] - [ApiController] - [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] - public class AnalyzeModelController( - IAnalyzeModelService analyzeModelService, - IAuthenticationService authenticationService) : ControllerBase + /// Status200OK - Success + [HttpPost] + [ActionName("GetModelFromVpax")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TabularDatabase))] + [ProducesDefaultResponseType] + public IActionResult GetDatabase(IFormFile[] files, CancellationToken cancellationToken) { - private readonly IAnalyzeModelService _analyzeModelService = analyzeModelService; - private readonly IAuthenticationService _authenticationService = authenticationService; - private readonly SaveFileDialog _exportVpaxDialog = new() - { - Title = "Save VPAX", - Filter = "VPAX file (*.vpax)|*.vpax|Obfuscated VPAX file (*.ovpax)|*.ovpax", - DefaultExt = "vpax", - AddExtension = true, - OverwritePrompt = true, - CheckPathExists = true, - ValidateNames = true - }; - - /// - /// Returns a database model from the VPAX file provided as multipart form data. - /// An optional obfuscation dictionary file can be included to deobfuscate the VPAX. - /// - /// Status200OK - Success - [HttpPost] - [ActionName("GetModelFromVpax")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TabularDatabase))] - [ProducesDefaultResponseType] - public IActionResult GetDatabase(IFormFile[] files, CancellationToken cancellationToken) - { - using var vpaxStream = files[0].OpenReadStream(); - using var obfuscatorDictionaryStream = files.ElementAtOrDefault(1)?.OpenReadStream(); + using var vpaxStream = files[0].OpenReadStream(); + using var obfuscatorDictionaryStream = files.ElementAtOrDefault(1)?.OpenReadStream(); - var database = _analyzeModelService.GetDatabase(vpaxStream, obfuscatorDictionaryStream); - return Ok(database); - } + var database = _analyzeModelService.GetDatabase(vpaxStream, obfuscatorDictionaryStream); + return Ok(database); + } - /// - /// Returns a database model from a PBIDesktop instance - /// - /// Status200OK - Success - [HttpPost] - [ActionName("GetModelFromReport")] - [Consumes(MediaTypeNames.Application.Json)] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TabularDatabase))] - [ProducesDefaultResponseType] - public IActionResult GetDatabase(PBIDesktopReport report, CancellationToken cancellationToken) - { - var database = _analyzeModelService.GetDatabase(report, cancellationToken); - return Ok(database); - } + /// + /// Returns a database model from a PBIDesktop instance + /// + /// Status200OK - Success + [HttpPost] + [ActionName("GetModelFromReport")] + [Consumes(MediaTypeNames.Application.Json)] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TabularDatabase))] + [ProducesDefaultResponseType] + public IActionResult GetDatabase(PBIDesktopReport report, CancellationToken cancellationToken) + { + var database = _analyzeModelService.GetDatabase(report, cancellationToken); + return Ok(database); + } - /// - /// Returns a database model from a PBICloud dataset - /// - /// Status200OK - Success - /// Status401Unauthorized - Sign-in required - [HttpPost] - [ActionName("GetModelFromDataset")] - [Consumes(MediaTypeNames.Application.Json)] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TabularDatabase))] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesDefaultResponseType] - public async Task GetDatabase(PBICloudDataset dataset, CancellationToken cancellationToken) - { - var session = await _authenticationService.EnsureSignedInAsync(cancellationToken); - if (session is null) - return Unauthorized(); + /// + /// Returns a database model from a PBICloud dataset + /// + /// Status200OK - Success + /// Status401Unauthorized - Sign-in required + [HttpPost] + [ActionName("GetModelFromDataset")] + [Consumes(MediaTypeNames.Application.Json)] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TabularDatabase))] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesDefaultResponseType] + public async Task GetDatabase(PBICloudDataset dataset, CancellationToken cancellationToken) + { + var session = await _authenticationService.EnsureSignedInAsync(cancellationToken); + if (session is null) + return Unauthorized(); - var database = _analyzeModelService.GetDatabase(dataset, session.AuthenticationResult.AccessToken, cancellationToken); - return Ok(database); - } + var database = _analyzeModelService.GetDatabase(dataset, session.AuthenticationResult.AccessToken, cancellationToken); + return Ok(database); + } - /// - /// Returns a list of all PBICloud datasets - /// - /// Status200OK - Success - /// Status401Unauthorized - Sign-in required - [HttpGet] - [ActionName("ListDatasets")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesDefaultResponseType] - public async Task GetDatasets(CancellationToken cancellationToken) - { - var session = await _authenticationService.EnsureSignedInAsync(cancellationToken); - if (session is null) - return Unauthorized(); + /// + /// Returns a list of all PBICloud datasets + /// + /// Status200OK - Success + /// Status401Unauthorized - Sign-in required + [HttpGet] + [ActionName("ListDatasets")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesDefaultResponseType] + public async Task GetDatasets(CancellationToken cancellationToken) + { + var session = await _authenticationService.EnsureSignedInAsync(cancellationToken); + if (session is null) + return Unauthorized(); - var datasets = await _analyzeModelService.GetDatasetsAsync(session, cancellationToken); - return Ok(datasets); - } + var datasets = await _analyzeModelService.GetDatasetsAsync(session, cancellationToken); + return Ok(datasets); + } - /// - /// Returns a list of all open - /// - /// Status200OK - Success - /// Status204NoContent - User canceled the operation - [HttpGet] - [ActionName("ListReports")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesDefaultResponseType] - public IActionResult GetReports(CancellationToken cancellationToken) + /// + /// Returns a list of all open + /// + /// Status200OK - Success + /// Status204NoContent - User canceled the operation + [HttpGet] + [ActionName("ListReports")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesDefaultResponseType] + public IActionResult GetReports(CancellationToken cancellationToken) + { + try { - try - { - var reports = _analyzeModelService.GetReports(cancellationToken); - return Ok(reports); - } - catch (OperationCanceledException) - { - return NoContent(); - } + var reports = _analyzeModelService.GetReports(cancellationToken); + return Ok(reports); } - - /// - /// Exports the specified Power BI Desktop report to a VPAX file, - /// allowing the user to select the file location and export mode. - /// - /// - /// The method displays a dialog for the user to choose the destination file - /// and export mode. The export mode can be either default or obfuscated, - /// depending on the user's selection. - /// - /// Status200OK - Success - /// Status204NoContent - User canceled the operation - [HttpPost] - [ActionName("ExportVpaxFromReport")] - [Consumes(MediaTypeNames.Application.Json)] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesDefaultResponseType] - public IActionResult ExportVpax(PBIDesktopReport report, CancellationToken cancellationToken) + catch (OperationCanceledException) { - _exportVpaxDialog.FileName = report.ReportName; + return NoContent(); + } + } + + /// + /// Exports the specified Power BI Desktop report to a VPAX file, + /// allowing the user to select the file location and export mode. + /// + /// + /// The method displays a dialog for the user to choose the destination file + /// and export mode. The export mode can be either default or obfuscated, + /// depending on the user's selection. + /// + /// Status200OK - Success + /// Status204NoContent - User canceled the operation + [HttpPost] + [ActionName("ExportVpaxFromReport")] + [Consumes(MediaTypeNames.Application.Json)] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesDefaultResponseType] + public IActionResult ExportVpax(PBIDesktopReport report, CancellationToken cancellationToken) + { + _exportVpaxDialog.FileName = report.ReportName; - var dialogResult = _exportVpaxDialog.ShowDialogOnStaThread(); - if (dialogResult != DialogResult.OK) - return NoContent(); + var dialogResult = _exportVpaxDialog.ShowDialogOnStaThread(); + if (dialogResult != DialogResult.OK) + return NoContent(); - var path = _exportVpaxDialog.FileName!; - var mode = _exportVpaxDialog.FilterIndex == 1 ? ExportVpaxMode.Default : ExportVpaxMode.Obfuscated; + var path = _exportVpaxDialog.FileName!; + var mode = _exportVpaxDialog.FilterIndex == 1 ? ExportVpaxMode.Default : ExportVpaxMode.Obfuscated; - _analyzeModelService.ExportVpax(report, mode, path, cancellationToken); - return Ok(); - } + _analyzeModelService.ExportVpax(report, mode, path, cancellationToken); + return Ok(); + } - /// - /// Exports the specified Power BI dataset to a VPAX file, - /// allowing the user to select the export mode and destination. - /// - /// - /// The method displays a dialog for the user to choose the destination file - /// and export mode. The export mode can be either default or obfuscated, - /// depending on the user's selection. - /// - /// Status200OK - Success - /// Status204NoContent - User canceled the operation - [HttpPost] - [ActionName("ExportVpaxFromDataset")] - [Consumes(MediaTypeNames.Application.Json)] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesDefaultResponseType] - public async Task ExportVpax(PBICloudDataset dataset, CancellationToken cancellationToken) - { - _exportVpaxDialog.FileName = dataset.DisplayName; + /// + /// Exports the specified Power BI dataset to a VPAX file, + /// allowing the user to select the export mode and destination. + /// + /// + /// The method displays a dialog for the user to choose the destination file + /// and export mode. The export mode can be either default or obfuscated, + /// depending on the user's selection. + /// + /// Status200OK - Success + /// Status204NoContent - User canceled the operation + [HttpPost] + [ActionName("ExportVpaxFromDataset")] + [Consumes(MediaTypeNames.Application.Json)] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesDefaultResponseType] + public async Task ExportVpax(PBICloudDataset dataset, CancellationToken cancellationToken) + { + _exportVpaxDialog.FileName = dataset.DisplayName; - var dialogResult = _exportVpaxDialog.ShowDialogOnStaThread(); - if (dialogResult != DialogResult.OK) - return NoContent(); + var dialogResult = _exportVpaxDialog.ShowDialogOnStaThread(); + if (dialogResult != DialogResult.OK) + return NoContent(); - var session = await _authenticationService.EnsureSignedInAsync(cancellationToken); - if (session is null) - return Unauthorized(); + var session = await _authenticationService.EnsureSignedInAsync(cancellationToken); + if (session is null) + return Unauthorized(); - var path = _exportVpaxDialog.FileName!; - var mode = _exportVpaxDialog.FilterIndex == 1 ? ExportVpaxMode.Default : ExportVpaxMode.Obfuscated; - var accessToken = session.AuthenticationResult.AccessToken; + var path = _exportVpaxDialog.FileName!; + var mode = _exportVpaxDialog.FilterIndex == 1 ? ExportVpaxMode.Default : ExportVpaxMode.Obfuscated; + var accessToken = session.AuthenticationResult.AccessToken; - _analyzeModelService.ExportVpax(dataset, accessToken, mode, path, cancellationToken); - return Ok(); - } + _analyzeModelService.ExportVpax(dataset, accessToken, mode, path, cancellationToken); + return Ok(); } } diff --git a/src/Controllers/ApplicationController.cs b/src/Controllers/ApplicationController.cs index 277fdc48..11a6c191 100644 --- a/src/Controllers/ApplicationController.cs +++ b/src/Controllers/ApplicationController.cs @@ -1,285 +1,284 @@ -namespace Sqlbi.Bravo.Controllers +using System; +using System.Collections.Generic; +using System.Net.Mime; +using System.Threading; +using System.Threading.Tasks; +using Hellang.Middleware.ProblemDetails; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Sqlbi.Bravo.Infrastructure; +using Sqlbi.Bravo.Infrastructure.Configuration; +using Sqlbi.Bravo.Infrastructure.Configuration.Settings; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Security; +using Sqlbi.Bravo.Infrastructure.Telemetry; +using Sqlbi.Bravo.Models; + +namespace Sqlbi.Bravo.Controllers; + +/// +/// Application controller +/// +/// Status400BadRequest - See the "instance" and "detail" properties to identify the specific occurrence of the problem +[Route("api/[action]")] +[ApiController] +[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] +public class ApplicationController : ControllerBase { - using Hellang.Middleware.ProblemDetails; - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.Configuration; - using Sqlbi.Bravo.Infrastructure.Configuration.Settings; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Security; - using Sqlbi.Bravo.Infrastructure.Telemetry; - using Sqlbi.Bravo.Models; - using System; - using System.Collections.Generic; - using System.Net.Mime; - using System.Threading; - using System.Threading.Tasks; + private readonly ITelemetryService _telemetryService; + + public ApplicationController(ITelemetryService telemetryService) + { + _telemetryService = telemetryService; + } /// - /// Application controller + /// Get the application options /// - /// Status400BadRequest - See the "instance" and "detail" properties to identify the specific occurrence of the problem - [Route("api/[action]")] - [ApiController] - [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] - public class ApplicationController : ControllerBase + /// Status200OK - Success + [HttpGet] + [ActionName("GetOptions")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(BravoOptions))] + [ProducesDefaultResponseType] + public IActionResult GetOptions() { - private readonly ITelemetryService _telemetryService; + var options = BravoOptions.CreateFromUserPreferences(); + return Ok(options); + } - public ApplicationController(ITelemetryService telemetryService) + /// + /// Update the application options + /// + /// Status200OK - Success + /// Status400BadRequest - See the "instance" and "detail" properties to identify the specific occurrence of the problem + [HttpPost] + [ActionName("UpdateOptions")] + [Consumes(MediaTypeNames.Application.Json)] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesDefaultResponseType] + public IActionResult UpdateOptions(BravoOptions options) + { + if (options.Proxy is not null) { - _telemetryService = telemetryService; + if (options.Proxy.Address.IsEmptyOrWhiteSpace() == true) + options.Proxy.Address = null; + + if (options.Proxy.BypassList.IsEmptyOrWhiteSpace() == true) + options.Proxy.BypassList = null; } - /// - /// Get the application options - /// - /// Status200OK - Success - [HttpGet] - [ActionName("GetOptions")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(BravoOptions))] - [ProducesDefaultResponseType] - public IActionResult GetOptions() + try { - var options = BravoOptions.CreateFromUserPreferences(); - return Ok(options); + options.SaveToUserPreferences(); } - - /// - /// Update the application options - /// - /// Status200OK - Success - /// Status400BadRequest - See the "instance" and "detail" properties to identify the specific occurrence of the problem - [HttpPost] - [ActionName("UpdateOptions")] - [Consumes(MediaTypeNames.Application.Json)] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesDefaultResponseType] - public IActionResult UpdateOptions(BravoOptions options) + catch (Exception ex) { - if (options.Proxy is not null) - { - if (options.Proxy.Address.IsEmptyOrWhiteSpace() == true) - options.Proxy.Address = null; + var problem = StatusCodeProblemDetails.Create(StatusCodes.Status400BadRequest); + problem.Detail = ex.Message; - if (options.Proxy.BypassList.IsEmptyOrWhiteSpace() == true) - options.Proxy.BypassList = null; - } - - try - { - options.SaveToUserPreferences(); - } - catch (Exception ex) - { - var problem = StatusCodeProblemDetails.Create(StatusCodes.Status400BadRequest); - problem.Detail = ex.Message; + return BadRequest(problem); + } - return BadRequest(problem); - } + _telemetryService.TelemetryEnabled = UserPreferences.Current.TelemetryEnabled; - _telemetryService.TelemetryEnabled = UserPreferences.Current.TelemetryEnabled; + return Ok(); + } - return Ok(); - } + /// + /// Change the current window theme + /// + /// Status200OK - Success + [HttpGet] + [ActionName("ChangeTheme")] + [Consumes(MediaTypeNames.Application.Json)] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesDefaultResponseType] + public IActionResult ChangeTheme(ThemeType theme) + { + ThemeHelper.ChangeTheme(theme); + return Ok(); + } - /// - /// Change the current window theme - /// - /// Status200OK - Success - [HttpGet] - [ActionName("ChangeTheme")] - [Consumes(MediaTypeNames.Application.Json)] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesDefaultResponseType] - public IActionResult ChangeTheme(ThemeType theme) - { - ThemeHelper.ChangeTheme(theme); + /// + /// Opens the provided URL using the system's default browser + /// + /// Status200OK - Success + /// Status403Forbidden - The address provided is invalid or not allowed + [HttpGet] + [ActionName("NavigateTo")] + [Consumes(MediaTypeNames.Application.Json)] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesDefaultResponseType] + public IActionResult BrowserNavigateTo(Uri address) + { + if (ProcessHelper.OpenBrowser(address)) return Ok(); - } - - /// - /// Opens the provided URL using the system's default browser - /// - /// Status200OK - Success - /// Status403Forbidden - The address provided is invalid or not allowed - [HttpGet] - [ActionName("NavigateTo")] - [Consumes(MediaTypeNames.Application.Json)] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesDefaultResponseType] - public IActionResult BrowserNavigateTo(Uri address) - { - if (ProcessHelper.OpenBrowser(address)) - return Ok(); - return Forbid(); - } + return Forbid(); + } - /// - /// Launches the Power BI Desktop process after displaying a dialog box that prompts the user to select the PBIX file to be opened - /// - /// Status200OK - Success - /// Status204NoContent - User canceled action (e.g. 'Cancel' button has been pressed on a dialog box) - /// Status403Forbidden - The path is invalid or not allowed - [HttpGet] - [ActionName("PBIDesktopOpenPBIX")] - [Consumes(MediaTypeNames.Application.Json)] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PBIDesktopReport))] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesDefaultResponseType] - public IActionResult PBIDesktopOpenPBIX(bool waitForStarted, CancellationToken cancellationToken) + /// + /// Launches the Power BI Desktop process after displaying a dialog box that prompts the user to select the PBIX file to be opened + /// + /// Status200OK - Success + /// Status204NoContent - User canceled action (e.g. 'Cancel' button has been pressed on a dialog box) + /// Status403Forbidden - The path is invalid or not allowed + [HttpGet] + [ActionName("PBIDesktopOpenPBIX")] + [Consumes(MediaTypeNames.Application.Json)] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PBIDesktopReport))] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesDefaultResponseType] + public IActionResult PBIDesktopOpenPBIX(bool waitForStarted, CancellationToken cancellationToken) + { + if (WindowDialogHelper.OpenFileDialog(filter: "PBIX files (*.pbix)|*.pbix", out var path, cancellationToken)) { - if (WindowDialogHelper.OpenFileDialog(filter: "PBIX files (*.pbix)|*.pbix", out var path, cancellationToken)) + if (ProcessHelper.OpenShellExecute(path, waitForStarted, out var processId, cancellationToken)) { - if (ProcessHelper.OpenShellExecute(path, waitForStarted, out var processId, cancellationToken)) - { - var report = PBIDesktopReport.CreateFrom(processId.Value); - return Ok(report); - } - - return Forbid(); + var report = PBIDesktopReport.CreateFrom(processId.Value); + return Ok(report); } - return NoContent(); + return Forbid(); } - /// - /// Opens the folder or file path provided - /// - /// Status200OK - Success - /// Status403Forbidden - The path is invalid or not allowed - [HttpGet] - [ActionName("FileSystemOpen")] - [Consumes(MediaTypeNames.Application.Json)] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesDefaultResponseType] - public IActionResult FileSystemOpen(string path) - { - if (ProcessHelper.Open(path)) - return Ok(); + return NoContent(); + } - return Forbid(); - } + /// + /// Opens the folder or file path provided + /// + /// Status200OK - Success + /// Status403Forbidden - The path is invalid or not allowed + [HttpGet] + [ActionName("FileSystemOpen")] + [Consumes(MediaTypeNames.Application.Json)] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesDefaultResponseType] + public IActionResult FileSystemOpen(string path) + { + if (ProcessHelper.Open(path)) + return Ok(); + + return Forbid(); + } + + /// + /// Gets all the for the application + /// + /// Status200OK - Success + [HttpGet] + [ActionName("GetDiagnostics")] + [Consumes(MediaTypeNames.Application.Json)] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] + [ProducesDefaultResponseType] + public IActionResult GetDiagnostics(bool? all = null) + { + var messages = new SortedList(); - /// - /// Gets all the for the application - /// - /// Status200OK - Success - [HttpGet] - [ActionName("GetDiagnostics")] - [Consumes(MediaTypeNames.Application.Json)] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] - [ProducesDefaultResponseType] - public IActionResult GetDiagnostics(bool? all = null) + foreach (var message in AppEnvironment.Diagnostics.Values) { - var messages = new SortedList(); - - foreach (var message in AppEnvironment.Diagnostics.Values) + if (all == true || message.ReadTimestamp is null) { - if (all == true || message.ReadTimestamp is null) - { - message.ReadTimestamp = DateTime.UtcNow; - messages.Add(message.Timestamp, message); - } + message.ReadTimestamp = DateTime.UtcNow; + messages.Add(message.Timestamp, message); } - - return Ok(messages.Values); } - /// - /// Gets the current application version for the specified - /// - /// Status200OK - Success - [HttpGet] - [ActionName("GetCurrentVersion")] - [Consumes(MediaTypeNames.Application.Json)] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(BravoUpdate))] - [ProducesDefaultResponseType] - public async Task GetCurrentVersion(UpdateChannelType updateChannel, CancellationToken cancellationToken) - { - var bravoUpdate = await CommonHelper.CheckForUpdateAsync(updateChannel, cancellationToken); - if (bravoUpdate.IsNewerVersion) - System.Media.SystemSounds.Beep.Play(); - - return Ok(bravoUpdate); - } + return Ok(messages.Values); + } - /// - /// Displays a dialog box that prompts the user to enter credentials to authenticate to the HTTP proxy - /// - /// Status200OK - Success - /// Status204NoContent - User canceled action (e.g. 'Cancel' button has been pressed on a dialog box) - [HttpGet] - [ActionName("UpdateProxyCredentials")] - [Consumes(MediaTypeNames.Application.Json)] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesDefaultResponseType] - public IActionResult UpdateProxyCredentials() - { - var credentialOptions = new CredentialDialogOptions(caption: "Enter proxy credentials", message: "Enter credentials to authenticate to the HTTP Proxy") - { - HwndParent = ProcessHelper.GetCurrentProcessMainWindowHandle(), - }; + /// + /// Gets the current application version for the specified + /// + /// Status200OK - Success + [HttpGet] + [ActionName("GetCurrentVersion")] + [Consumes(MediaTypeNames.Application.Json)] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(BravoUpdate))] + [ProducesDefaultResponseType] + public async Task GetCurrentVersion(UpdateChannelType updateChannel, CancellationToken cancellationToken) + { + var bravoUpdate = await CommonHelper.CheckForUpdateAsync(updateChannel, cancellationToken); + if (bravoUpdate.IsNewerVersion) + System.Media.SystemSounds.Beep.Play(); - var networkCredential = CredentialDialog.PromptForCredentials(credentialOptions); - if (networkCredential is not null) - { - CredentialManager.WriteCredential(AppEnvironment.CredentialManagerProxyCredentialName, networkCredential.UserName, networkCredential.Password); - return Ok(); - } + return Ok(bravoUpdate); + } - return NoContent(); - } + /// + /// Displays a dialog box that prompts the user to enter credentials to authenticate to the HTTP proxy + /// + /// Status200OK - Success + /// Status204NoContent - User canceled action (e.g. 'Cancel' button has been pressed on a dialog box) + [HttpGet] + [ActionName("UpdateProxyCredentials")] + [Consumes(MediaTypeNames.Application.Json)] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesDefaultResponseType] + public IActionResult UpdateProxyCredentials() + { + var credentialOptions = new CredentialDialogOptions(caption: "Enter proxy credentials", message: "Enter credentials to authenticate to the HTTP Proxy") + { + HwndParent = ProcessHelper.GetCurrentProcessMainWindowHandle(), + }; - /// - /// Removes credentials to authenticate to the HTTP proxy, if any - /// - /// Status200OK - Success - [HttpGet] - [ActionName("DeleteProxyCredentials")] - [Consumes(MediaTypeNames.Application.Json)] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesDefaultResponseType] - public IActionResult DeleteProxyCredentials() + var networkCredential = CredentialDialog.PromptForCredentials(credentialOptions); + if (networkCredential is not null) { - _ = CredentialManager.DeleteCredential(AppEnvironment.CredentialManagerProxyCredentialName); + CredentialManager.WriteCredential(AppEnvironment.CredentialManagerProxyCredentialName, networkCredential.UserName, networkCredential.Password); return Ok(); } - /// - /// Opens a Windows Control Panel items based on the canonical name provided - /// - /// Status200OK - Success - [HttpGet] - [ActionName("OpenControlPanelItem")] - [Consumes(MediaTypeNames.Application.Json)] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesDefaultResponseType] - public IActionResult OpenControlPanelItem(string canonicalName) - { - // Opens Windows Credential Manager on Windows Credentials page - // /name Microsoft.CredentialManager /page ?SelectedVault=CredmanVault + return NoContent(); + } - ProcessHelper.OpenControlPanelItem(canonicalName); - return Ok(); - } + /// + /// Removes credentials to authenticate to the HTTP proxy, if any + /// + /// Status200OK - Success + [HttpGet] + [ActionName("DeleteProxyCredentials")] + [Consumes(MediaTypeNames.Application.Json)] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesDefaultResponseType] + public IActionResult DeleteProxyCredentials() + { + _ = CredentialManager.DeleteCredential(AppEnvironment.CredentialManagerProxyCredentialName); + return Ok(); + } + + /// + /// Opens a Windows Control Panel items based on the canonical name provided + /// + /// Status200OK - Success + [HttpGet] + [ActionName("OpenControlPanelItem")] + [Consumes(MediaTypeNames.Application.Json)] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesDefaultResponseType] + public IActionResult OpenControlPanelItem(string canonicalName) + { + // Opens Windows Credential Manager on Windows Credentials page + // /name Microsoft.CredentialManager /page ?SelectedVault=CredmanVault + + ProcessHelper.OpenControlPanelItem(canonicalName); + return Ok(); } } diff --git a/src/Controllers/AuthenticationController.cs b/src/Controllers/AuthenticationController.cs index eabaa04a..15729844 100644 --- a/src/Controllers/AuthenticationController.cs +++ b/src/Controllers/AuthenticationController.cs @@ -1,120 +1,124 @@ -namespace Sqlbi.Bravo.Controllers +using System; +using System.Net.Mime; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Sqlbi.Bravo.Infrastructure; +using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; +using Sqlbi.Bravo.Models; +using Sqlbi.Bravo.Models.Authentication; +using Sqlbi.Bravo.Services; + +namespace Sqlbi.Bravo.Controllers; + +/// +/// Authentication controller +/// +/// Status400BadRequest - See the "instance" and "detail" properties to identify the specific occurrence of the problem +[Route("auth/[action]")] +[ApiController] +[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] +public sealed class AuthenticationController( + ICloudApiClient cloudApiClient, + IAuthenticationService authenticationService) : ControllerBase { - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; - using Sqlbi.Bravo.Models; - using Sqlbi.Bravo.Models.Authentication; - using Sqlbi.Bravo.Services; + private readonly IAuthenticationService _authenticationService = authenticationService; + private readonly ICloudApiClient _cloudApiClient = cloudApiClient; /// - /// Authentication controller + /// Returns the list of available PowerBI cloud environments for the specified email account. /// - /// Status400BadRequest - See the "instance" and "detail" properties to identify the specific occurrence of the problem - [Route("auth/[action]")] - [ApiController] - [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] - public sealed class AuthenticationController( - ICloudApiClient cloudApiClient, - IAuthenticationService authenticationService) : ControllerBase + /// Status200OK - Success + [HttpGet] + [ActionName("GetEnvironments")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetEnvironmentsResponse))] + [ProducesDefaultResponseType] + public async Task GetEnvironmentsAsync( + [FromQuery] GetEnvironmentsRequest request, + CancellationToken cancellationToken) { - private readonly IAuthenticationService _authenticationService = authenticationService; - private readonly ICloudApiClient _cloudApiClient = cloudApiClient; + var environments = await _authenticationService.GetEnvironmentsAsync( + request.Email, + cancellationToken); + + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(AuthenticationController)}.{nameof(GetEnvironmentsAsync)}", JsonSerializer.Serialize(environments)); - /// - /// Returns the list of available PowerBI cloud environments for the specified email account. - /// - /// Status200OK - Success - [HttpGet] - [ActionName("GetEnvironments")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(GetEnvironmentsResponse))] - [ProducesDefaultResponseType] - public async Task GetEnvironmentsAsync( - [FromQuery] GetEnvironmentsRequest request, - CancellationToken cancellationToken) + var response = new GetEnvironmentsResponse(environments); + return Ok(response); + } + + /// + /// Attempts to authenticate and acquire an access token for the account to access the PowerBI cloud services + /// + /// Status200OK - Success + /// Status204NoContent - Sign-in was canceled by the user + [HttpPost] + [ActionName("SignIn")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(SignInResponse))] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesDefaultResponseType] + public async Task SignInAsync( + SignInRequest request, + CancellationToken cancellationToken) + { + try { - var environments = await _authenticationService.GetEnvironmentsAsync( + var session = await _authenticationService.SignInAsync( request.Email, + request.Environment.ToModel(), cancellationToken); - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(AuthenticationController)}.{nameof(GetEnvironmentsAsync)}", JsonSerializer.Serialize(environments)); - - var response = new GetEnvironmentsResponse(environments); + var response = new SignInResponse(session.AuthenticationResult); return Ok(response); } - - /// - /// Attempts to authenticate and acquire an access token for the account to access the PowerBI cloud services - /// - /// Status200OK - Success - /// Status204NoContent - Sign-in was canceled by the user - [HttpPost] - [ActionName("SignIn")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(SignInResponse))] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesDefaultResponseType] - public async Task SignInAsync( - SignInRequest request, - CancellationToken cancellationToken) + catch (OperationCanceledException) { - try - { - var session = await _authenticationService.SignInAsync( - request.Email, - request.Environment.ToModel(), - cancellationToken); - - var response = new SignInResponse(session.AuthenticationResult); - return Ok(response); - } - catch (OperationCanceledException) - { - return NoContent(); - } + return NoContent(); } + } - /// - /// Clear the token cache for all the accounts - /// - /// Status200OK - Success - [HttpGet] - [ActionName("SignOut")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesDefaultResponseType] - public async Task SignOutAsync(CancellationToken cancellationToken) - { - await _authenticationService.SignOutAsync(cancellationToken); - return Ok(); - } + /// + /// Clear the token cache for all the accounts + /// + /// Status200OK - Success + [HttpGet] + [ActionName("SignOut")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesDefaultResponseType] + public async Task SignOutAsync(CancellationToken cancellationToken) + { + await _authenticationService.SignOutAsync(cancellationToken); + return Ok(); + } - /// - /// Returns the account profile picture as base64 encoded image [data:image/jpeg;base64,...] - /// - /// Status200OK - Success - /// Status404NotFound - Current account has no profile picture - /// Status401Unauthorized - Sign-in required - [HttpGet] - [ActionName("GetUserAvatar")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(string))] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesDefaultResponseType] - public async Task GetUserAvatarAsync(CancellationToken cancellationToken) - { - var session = await _authenticationService.EnsureSignedInAsync(cancellationToken); - if (session is null) - return Unauthorized(); + /// + /// Returns the account profile picture as base64 encoded image [data:image/jpeg;base64,...] + /// + /// Status200OK - Success + /// Status404NotFound - Current account has no profile picture + /// Status401Unauthorized - Sign-in required + [HttpGet] + [ActionName("GetUserAvatar")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(string))] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesDefaultResponseType] + public async Task GetUserAvatarAsync(CancellationToken cancellationToken) + { + var session = await _authenticationService.EnsureSignedInAsync(cancellationToken); + if (session is null) + return Unauthorized(); - var avatar = await _cloudApiClient.GetUserPhotoAsync(session, cancellationToken); - if (avatar is null) - return NotFound(); + var avatar = await _cloudApiClient.GetUserPhotoAsync(session, cancellationToken); + if (avatar is null) + return NotFound(); - return Ok(avatar); - } + return Ok(avatar); } } diff --git a/src/Controllers/ExportDataController.cs b/src/Controllers/ExportDataController.cs index 10e1e079..e006ecb9 100644 --- a/src/Controllers/ExportDataController.cs +++ b/src/Controllers/ExportDataController.cs @@ -1,237 +1,236 @@ -namespace Sqlbi.Bravo.Controllers +using System.IO; +using System.Net.Mime; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Windows; +using Sqlbi.Bravo.Models; +using Sqlbi.Bravo.Models.ExportData; +using Sqlbi.Bravo.Services; + +namespace Sqlbi.Bravo.Controllers; + +/// +/// ExportData module controller +/// +/// Status400BadRequest - See the "instance" and "detail" properties to identify the specific occurrence of the problem +[Route("api/[action]")] +[ApiController] +[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] +public class ExportDataController : ControllerBase { - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Windows; - using Sqlbi.Bravo.Models; - using Sqlbi.Bravo.Models.ExportData; - using Sqlbi.Bravo.Services; - using System.IO; - using System.Net.Mime; - using System.Threading; - using System.Threading.Tasks; - using System.Windows.Forms; + private readonly IExportDataService _exportDataService; + private readonly IAuthenticationService _authenticationService; + + public ExportDataController(IExportDataService exportDataService, IAuthenticationService authenticationService) + { + _exportDataService = exportDataService; + _authenticationService = authenticationService; + } /// - /// ExportData module controller + /// Exports tables from a using the provided format settings /// - /// Status400BadRequest - See the "instance" and "detail" properties to identify the specific occurrence of the problem - [Route("api/[action]")] - [ApiController] - [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] - public class ExportDataController : ControllerBase + /// Status200OK - Success + /// Status204NoContent - User canceled action (e.g. 'Cancel' button has been pressed on a dialog box) + [HttpPost] + [ActionName("ExportCsvFromReport")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ExportDataJob))] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesDefaultResponseType] + public IActionResult ExportDelimitedTextFile(ExportDelimitedTextFromPBIReportRequest request, CancellationToken cancellationToken) { - private readonly IExportDataService _exportDataService; - private readonly IAuthenticationService _authenticationService; - - public ExportDataController(IExportDataService exportDataService, IAuthenticationService authenticationService) + if (WindowDialogHelper.BrowseFolderDialog(out var path, cancellationToken)) { - _exportDataService = exportDataService; - _authenticationService = authenticationService; + if (request.Settings!.CreateSubfolder) + { + if (!GetExportSubfolderPath(ref path, name: request.Report!.ReportName)) + return NoContent(); + } + + var job = _exportDataService.ExportDelimitedTextFile(request.Report!, request.Settings!, path, cancellationToken); + return Ok(job); } - /// - /// Exports tables from a using the provided format settings - /// - /// Status200OK - Success - /// Status204NoContent - User canceled action (e.g. 'Cancel' button has been pressed on a dialog box) - [HttpPost] - [ActionName("ExportCsvFromReport")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ExportDataJob))] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesDefaultResponseType] - public IActionResult ExportDelimitedTextFile(ExportDelimitedTextFromPBIReportRequest request, CancellationToken cancellationToken) + return NoContent(); + } + + /// + /// Exports tables from a using the provided format settings + /// + /// Status200OK - Success + /// Status204NoContent - User canceled action (e.g. 'Cancel' button has been pressed on a dialog box) + /// Status401Unauthorized - Sign-in required + [HttpPost] + [ActionName("ExportCsvFromDataset")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ExportDataJob))] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesDefaultResponseType] + public async Task ExportDelimitedTextFile(ExportDelimitedTextFromPBICloudDatasetRequest request, CancellationToken cancellationToken) + { + var session = await _authenticationService.EnsureSignedInAsync(cancellationToken); + if (session is null) + return Unauthorized(); + + if (WindowDialogHelper.BrowseFolderDialog(out var path, cancellationToken)) { - if (WindowDialogHelper.BrowseFolderDialog(out var path, cancellationToken)) + if (request.Settings!.CreateSubfolder) { - if (request.Settings!.CreateSubfolder) - { - if (!GetExportSubfolderPath(ref path, name: request.Report!.ReportName)) - return NoContent(); - } - - var job = _exportDataService.ExportDelimitedTextFile(request.Report!, request.Settings!, path, cancellationToken); - return Ok(job); + if (!GetExportSubfolderPath(ref path, name: request.Dataset!.DisplayName)) + return NoContent(); } - - return NoContent(); + + var job = _exportDataService.ExportDelimitedTextFile(request.Dataset!, request.Settings!, path, session.AuthenticationResult.AccessToken, cancellationToken); + return Ok(job); } - /// - /// Exports tables from a using the provided format settings - /// - /// Status200OK - Success - /// Status204NoContent - User canceled action (e.g. 'Cancel' button has been pressed on a dialog box) - /// Status401Unauthorized - Sign-in required - [HttpPost] - [ActionName("ExportCsvFromDataset")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ExportDataJob))] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesDefaultResponseType] - public async Task ExportDelimitedTextFile(ExportDelimitedTextFromPBICloudDatasetRequest request, CancellationToken cancellationToken) + return NoContent(); + } + + /// + /// Exports tables from a using the provided format settings + /// + /// Status200OK - Success + /// Status204NoContent - User canceled action (e.g. 'Cancel' button has been pressed on a dialog box) + [HttpPost] + [ActionName("ExportXlsxFromReport")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ExportDataJob))] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesDefaultResponseType] + public IActionResult ExportExcelFile(ExportExcelFromPBIReportRequest request, CancellationToken cancellationToken) + { + if (WindowDialogHelper.SaveFileDialog(fileName: request.Report!.ReportName, filter: null, defaultExt: "XLSX", out var path, cancellationToken)) { - var session = await _authenticationService.EnsureSignedInAsync(cancellationToken); - if (session is null) - return Unauthorized(); + var job = _exportDataService.ExportExcelFile(request.Report, request.Settings!, path, cancellationToken); + return Ok(job); + } - if (WindowDialogHelper.BrowseFolderDialog(out var path, cancellationToken)) - { - if (request.Settings!.CreateSubfolder) - { - if (!GetExportSubfolderPath(ref path, name: request.Dataset!.DisplayName)) - return NoContent(); - } + return NoContent(); + } - var job = _exportDataService.ExportDelimitedTextFile(request.Dataset!, request.Settings!, path, session.AuthenticationResult.AccessToken, cancellationToken); - return Ok(job); - } + /// + /// Exports tables from a using the provided format settings + /// + /// Status200OK - Success + /// Status204NoContent - User canceled action (e.g. a 'Cancel' button has been pressed on a dialog box) + /// Status401Unauthorized - Sign-in required + [HttpPost] + [ActionName("ExportXlsxFromDataset")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ExportDataJob))] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesDefaultResponseType] + public async Task ExportExcelFile(ExportExcelFromPBICloudDatasetRequest request, CancellationToken cancellationToken) + { + var session = await _authenticationService.EnsureSignedInAsync(cancellationToken); + if (session is null) + return Unauthorized(); - return NoContent(); + if (WindowDialogHelper.SaveFileDialog(fileName: request.Dataset!.DisplayName, filter: null, defaultExt: "XLSX", out var path, cancellationToken)) + { + var job = _exportDataService.ExportExcelFile(request.Dataset, request.Settings!, path, session.AuthenticationResult.AccessToken, cancellationToken); + return Ok(job); } - /// - /// Exports tables from a using the provided format settings - /// - /// Status200OK - Success - /// Status204NoContent - User canceled action (e.g. 'Cancel' button has been pressed on a dialog box) - [HttpPost] - [ActionName("ExportXlsxFromReport")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ExportDataJob))] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesDefaultResponseType] - public IActionResult ExportExcelFile(ExportExcelFromPBIReportRequest request, CancellationToken cancellationToken) - { - if (WindowDialogHelper.SaveFileDialog(fileName: request.Report!.ReportName, filter: null, defaultExt: "XLSX", out var path, cancellationToken)) - { - var job = _exportDataService.ExportExcelFile(request.Report, request.Settings!, path, cancellationToken); - return Ok(job); - } + return NoContent(); + } + + /// + /// Returns the details of a export job to allow monitoring of ongoing activity + /// + /// Status200OK - Success + /// Status204NoContent - Export job not available for querying + [HttpPost] + [ActionName("QueryExportFromReport")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ExportDataJob))] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesDefaultResponseType] + public IActionResult QueryExportJob(PBIDesktopReport report) + { + var job = _exportDataService.QueryExportJob(report); + if (job is null) return NoContent(); - } - /// - /// Exports tables from a using the provided format settings - /// - /// Status200OK - Success - /// Status204NoContent - User canceled action (e.g. a 'Cancel' button has been pressed on a dialog box) - /// Status401Unauthorized - Sign-in required - [HttpPost] - [ActionName("ExportXlsxFromDataset")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ExportDataJob))] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesDefaultResponseType] - public async Task ExportExcelFile(ExportExcelFromPBICloudDatasetRequest request, CancellationToken cancellationToken) - { - var session = await _authenticationService.EnsureSignedInAsync(cancellationToken); - if (session is null) - return Unauthorized(); + return Ok(job); + } - if (WindowDialogHelper.SaveFileDialog(fileName: request.Dataset!.DisplayName, filter: null, defaultExt: "XLSX", out var path, cancellationToken)) - { - var job = _exportDataService.ExportExcelFile(request.Dataset, request.Settings!, path, session.AuthenticationResult.AccessToken, cancellationToken); - return Ok(job); - } + /// + /// Returns the details of a export job to allow monitoring of ongoing activity + /// + /// Status200OK - Success + /// Status204NoContent - Export job not available for querying + [HttpPost] + [ActionName("QueryExportFromDataset")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ExportDataJob))] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesDefaultResponseType] + public IActionResult QueryExportJob(PBICloudDataset dataset) + { + var job = _exportDataService.QueryExportJob(dataset); + if (job is null) return NoContent(); - } - /// - /// Returns the details of a export job to allow monitoring of ongoing activity - /// - /// Status200OK - Success - /// Status204NoContent - Export job not available for querying - [HttpPost] - [ActionName("QueryExportFromReport")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ExportDataJob))] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesDefaultResponseType] - public IActionResult QueryExportJob(PBIDesktopReport report) - { - var job = _exportDataService.QueryExportJob(report); - - if (job is null) - return NoContent(); + return Ok(job); + } - return Ok(job); + private static bool GetExportSubfolderPath(ref string path, string? name) + { + if (name.IsNullOrWhiteSpace()) + { + name = "New Folder"; } - /// - /// Returns the details of a export job to allow monitoring of ongoing activity - /// - /// Status200OK - Success - /// Status204NoContent - Export job not available for querying - [HttpPost] - [ActionName("QueryExportFromDataset")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(ExportDataJob))] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesDefaultResponseType] - public IActionResult QueryExportJob(PBICloudDataset dataset) - { - var job = _exportDataService.QueryExportJob(dataset); + var subfolderName = name.ReplaceInvalidPathChars(); + var subfolderPath = Path.Combine(path, subfolderName); - if (job is null) - return NoContent(); + if (Directory.Exists(subfolderPath)) + { + var overwriteButton = new TaskDialogCommandLinkButton("&Overwrite", "Overwrite and replace files in the destination folder"); + var keepbothButton = new TaskDialogCommandLinkButton("&Keep Both", "Files will be exported to a new folder"); + var cancelButton = new TaskDialogCommandLinkButton("&Cancel", "Cancel export"); + var heading = $"The destination folder you chose already contains a subfolder named '{subfolderName}'"; + var text = "Choose an option to proceed"; - return Ok(job); - } + var clickedButton = MessageDialog.ShowDialog(heading, text, footnoteText: null, allowCancel: true, overwriteButton, keepbothButton, cancelButton); - private static bool GetExportSubfolderPath(ref string path, string? name) - { - if (name.IsNullOrWhiteSpace()) + if (clickedButton == overwriteButton) { - name = "New Folder"; + // } - - var subfolderName = name.ReplaceInvalidPathChars(); - var subfolderPath = Path.Combine(path, subfolderName); - - if (Directory.Exists(subfolderPath)) + else if (clickedButton == keepbothButton) { - var overwriteButton = new TaskDialogCommandLinkButton("&Overwrite", "Overwrite and replace files in the destination folder"); - var keepbothButton = new TaskDialogCommandLinkButton("&Keep Both", "Files will be exported to a new folder"); - var cancelButton = new TaskDialogCommandLinkButton("&Cancel", "Cancel export"); - var heading = $"The destination folder you chose already contains a subfolder named '{ subfolderName }'"; - var text = "Choose an option to proceed"; - - var clickedButton = MessageDialog.ShowDialog(heading, text, footnoteText: null, allowCancel: true, overwriteButton, keepbothButton, cancelButton); - - if (clickedButton == overwriteButton) + for (var i = 1; /**/ ; i++) { - // - } - else if (clickedButton == keepbothButton) - { - for (var i = 1; /**/ ; i++) - { - var uniquePath = Path.Combine(path, $"{ subfolderName } - { i }"); + var uniquePath = Path.Combine(path, $"{subfolderName} - {i}"); - if (!Directory.Exists(uniquePath)) - { - subfolderPath = uniquePath; - break; - } + if (!Directory.Exists(uniquePath)) + { + subfolderPath = uniquePath; + break; } } - else - { - path = string.Empty; - return false; - } } - - path = subfolderPath; - return true; + else + { + path = string.Empty; + return false; + } } + + path = subfolderPath; + return true; } } diff --git a/src/Controllers/FormatDaxController.cs b/src/Controllers/FormatDaxController.cs index 9a30f9b9..93843d22 100644 --- a/src/Controllers/FormatDaxController.cs +++ b/src/Controllers/FormatDaxController.cs @@ -1,92 +1,90 @@ -namespace Sqlbi.Bravo.Controllers +using System.Net.Mime; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Sqlbi.Bravo.Models; +using Sqlbi.Bravo.Models.FormatDax; +using Sqlbi.Bravo.Services; + +namespace Sqlbi.Bravo.Controllers; + +/// +/// FormatDax module controller +/// +/// Status400BadRequest - See the "instance" and "detail" properties to identify the specific occurrence of the problem +[Route("api/[action]")] +[ApiController] +[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] +public class FormatDaxController : ControllerBase { - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Sqlbi.Bravo.Infrastructure.Services.PowerBI; - using Sqlbi.Bravo.Models; - using Sqlbi.Bravo.Models.FormatDax; - using Sqlbi.Bravo.Services; - using System.Net.Mime; - using System.Threading; - using System.Threading.Tasks; + private readonly IFormatDaxService _formatDaxService; + private readonly IAuthenticationService _authenticationService; + + public FormatDaxController(IFormatDaxService formatDaxService, IAuthenticationService authenticationService) + { + _formatDaxService = formatDaxService; + _authenticationService = authenticationService; + } /// - /// FormatDax module controller + /// Format the provided DAX measures by using daxformatter.com service /// - /// Status400BadRequest - See the "instance" and "detail" properties to identify the specific occurrence of the problem - [Route("api/[action]")] - [ApiController] - [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] - public class FormatDaxController : ControllerBase + /// Status200OK - Success + [HttpPost] + [ActionName("FormatDax")] + [Consumes(MediaTypeNames.Application.Json)] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(FormatDaxResponse))] + [ProducesDefaultResponseType] + public async Task Format(FormatDaxRequest request) { - private readonly IFormatDaxService _formatDaxService; - private readonly IAuthenticationService _authenticationService; - - public FormatDaxController(IFormatDaxService formatDaxService, IAuthenticationService authenticationService) - { - _formatDaxService = formatDaxService; - _authenticationService = authenticationService; - } + var formattedMeasures = await _formatDaxService.FormatAsync(request.Measures!, request.Options!); - /// - /// Format the provided DAX measures by using daxformatter.com service - /// - /// Status200OK - Success - [HttpPost] - [ActionName("FormatDax")] - [Consumes(MediaTypeNames.Application.Json)] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(FormatDaxResponse))] - [ProducesDefaultResponseType] - public async Task Format(FormatDaxRequest request) - { - var formattedMeasures = await _formatDaxService.FormatAsync(request.Measures!, request.Options!); + // TODO: view TODO on FormatDaxResponse class + //var response = new FormatDaxResponse + //{ + // Measures = formattedMeasures, + //}; - // TODO: view TODO on FormatDaxResponse class - //var response = new FormatDaxResponse - //{ - // Measures = formattedMeasures, - //}; - - return Ok(formattedMeasures); - } + return Ok(formattedMeasures); + } - /// - /// Update a by applying changes to formatted measures - /// - /// Status200OK - Success - [HttpPost] - [ActionName("UpdateReport")] - [Consumes(MediaTypeNames.Application.Json)] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(DatabaseUpdateResult))] - [ProducesDefaultResponseType] - public IActionResult Update(UpdatePBIDesktopReportRequest request) - { - var updateResult = _formatDaxService.Update(request.Report!, request.Measures!); - return Ok(updateResult); - } + /// + /// Update a by applying changes to formatted measures + /// + /// Status200OK - Success + [HttpPost] + [ActionName("UpdateReport")] + [Consumes(MediaTypeNames.Application.Json)] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(DatabaseUpdateResult))] + [ProducesDefaultResponseType] + public IActionResult Update(UpdatePBIDesktopReportRequest request) + { + var updateResult = _formatDaxService.Update(request.Report!, request.Measures!); + return Ok(updateResult); + } - /// - /// Update a by applying changes to formatted measures - /// - /// Status200OK - Success - /// Status401Unauthorized - Sign-in required - [HttpPost] - [ActionName("UpdateDataset")] - [Consumes(MediaTypeNames.Application.Json)] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(DatabaseUpdateResult))] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesDefaultResponseType] - public async Task Update(UpdatePBICloudDatasetRequest request, CancellationToken cancellationToken) - { - var session = await _authenticationService.EnsureSignedInAsync(cancellationToken); - if (session is null) - return Unauthorized(); + /// + /// Update a by applying changes to formatted measures + /// + /// Status200OK - Success + /// Status401Unauthorized - Sign-in required + [HttpPost] + [ActionName("UpdateDataset")] + [Consumes(MediaTypeNames.Application.Json)] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(DatabaseUpdateResult))] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesDefaultResponseType] + public async Task Update(UpdatePBICloudDatasetRequest request, CancellationToken cancellationToken) + { + var session = await _authenticationService.EnsureSignedInAsync(cancellationToken); + if (session is null) + return Unauthorized(); - var updateResult = _formatDaxService.Update(request.Dataset!, request.Measures!, session.AuthenticationResult.AccessToken); - return Ok(updateResult); - } + var updateResult = _formatDaxService.Update(request.Dataset!, request.Measures!, session.AuthenticationResult.AccessToken); + return Ok(updateResult); } } diff --git a/src/Controllers/ManageDatesController.cs b/src/Controllers/ManageDatesController.cs index ed6d25f3..17bc6a9c 100644 --- a/src/Controllers/ManageDatesController.cs +++ b/src/Controllers/ManageDatesController.cs @@ -1,88 +1,87 @@ -namespace Sqlbi.Bravo.Controllers +using System.Collections.Generic; +using System.Net.Mime; +using System.Threading; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Sqlbi.Bravo.Models; +using Sqlbi.Bravo.Models.ManageDates; +using Sqlbi.Bravo.Services; + +namespace Sqlbi.Bravo.Controllers; + +/// +/// ManageDates module controller +/// +/// Status400BadRequest - See the "instance" and "detail" properties to identify the specific occurrence of the problem +[Route("ManageDates/[action]")] +[ApiController] +[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] +public class ManageDatesController : ControllerBase { - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Sqlbi.Bravo.Models; - using Sqlbi.Bravo.Models.ManageDates; - using Sqlbi.Bravo.Services; - using System.Collections.Generic; - using System.Net.Mime; - using System.Threading; + private readonly IManageDatesService _manageDatesService; + + public ManageDatesController(IManageDatesService manageDatesService) + { + _manageDatesService = manageDatesService; + } /// - /// ManageDates module controller + /// Gets all the available from the embedded templates /// - /// Status400BadRequest - See the "instance" and "detail" properties to identify the specific occurrence of the problem - [Route("ManageDates/[action]")] - [ApiController] - [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] - public class ManageDatesController : ControllerBase + /// Status200OK - Success + [HttpPost] + [ActionName("GetConfigurationsForReport")] // TODO: raname GetConfigurations + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] + [ProducesDefaultResponseType] + public IActionResult GetConfigurations(PBIDesktopReport report, CancellationToken cancellationToken) { - private readonly IManageDatesService _manageDatesService; - - public ManageDatesController(IManageDatesService manageDatesService) - { - _manageDatesService = manageDatesService; - } - - /// - /// Gets all the available from the embedded templates - /// - /// Status200OK - Success - [HttpPost] - [ActionName("GetConfigurationsForReport")] // TODO: raname GetConfigurations - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] - [ProducesDefaultResponseType] - public IActionResult GetConfigurations(PBIDesktopReport report, CancellationToken cancellationToken) - { - var configurations = _manageDatesService.GetConfigurations(report, cancellationToken); - return Ok(configurations); - } + var configurations = _manageDatesService.GetConfigurations(report, cancellationToken); + return Ok(configurations); + } - /// - /// Validate the provided against the tabular model - /// - /// Status200OK - Success - [HttpPost] - [ActionName("ValidateConfigurationForReport")] // TODO: raname ValidateConfiguration - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(DateConfiguration))] - [ProducesDefaultResponseType] - public IActionResult ValidateConfiguration(ValidateConfigurationRequest request, CancellationToken cancellationToken) - { - var configuration = _manageDatesService.ValidateConfiguration(request.Report!, request.Configuration!, cancellationToken); - return Ok(configuration); - } + /// + /// Validate the provided against the tabular model + /// + /// Status200OK - Success + [HttpPost] + [ActionName("ValidateConfigurationForReport")] // TODO: raname ValidateConfiguration + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(DateConfiguration))] + [ProducesDefaultResponseType] + public IActionResult ValidateConfiguration(ValidateConfigurationRequest request, CancellationToken cancellationToken) + { + var configuration = _manageDatesService.ValidateConfiguration(request.Report!, request.Configuration!, cancellationToken); + return Ok(configuration); + } - /// - /// Applies the provided without commit changes and returns a preview of changes to objects and data - /// - /// Status200OK - Success - [HttpPost] - [ActionName("GetPreviewChangesFromReport")] // TODO: rename to GetPreviewChanges - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Dax.Template.Model.ModelChanges))] - [ProducesDefaultResponseType] - public IActionResult GetPreviewChanges(PreviewChangesRequest request, CancellationToken cancellationToken) - { - var modelChanges = _manageDatesService.GetPreviewChanges(request.Report!, request.Settings!, cancellationToken); - return Ok(modelChanges); - } + /// + /// Applies the provided without commit changes and returns a preview of changes to objects and data + /// + /// Status200OK - Success + [HttpPost] + [ActionName("GetPreviewChangesFromReport")] // TODO: rename to GetPreviewChanges + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Dax.Template.Model.ModelChanges))] + [ProducesDefaultResponseType] + public IActionResult GetPreviewChanges(PreviewChangesRequest request, CancellationToken cancellationToken) + { + var modelChanges = _manageDatesService.GetPreviewChanges(request.Report!, request.Settings!, cancellationToken); + return Ok(modelChanges); + } - /// - /// Update the by appliying the provided - /// - /// Status200OK - Success - [HttpPost] - [ActionName("UpdateReport")] // TODO: rename to ApplyConfiguration - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesDefaultResponseType] - public IActionResult ApplyConfiguration(ApplyConfigurationRequest request, CancellationToken cancellationToken) - { - _manageDatesService.ApplyConfiguration(request.Report!, request.Configuration!, cancellationToken); - return Ok(); - } + /// + /// Update the by appliying the provided + /// + /// Status200OK - Success + [HttpPost] + [ActionName("UpdateReport")] // TODO: rename to ApplyConfiguration + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesDefaultResponseType] + public IActionResult ApplyConfiguration(ApplyConfigurationRequest request, CancellationToken cancellationToken) + { + _manageDatesService.ApplyConfiguration(request.Report!, request.Configuration!, cancellationToken); + return Ok(); } } diff --git a/src/Controllers/TemplateDevelopmentController.cs b/src/Controllers/TemplateDevelopmentController.cs index b5a3e612..c7c62b2d 100644 --- a/src/Controllers/TemplateDevelopmentController.cs +++ b/src/Controllers/TemplateDevelopmentController.cs @@ -1,238 +1,238 @@ -namespace Sqlbi.Bravo.Controllers +using System; +using System.Collections.Generic; +using System.Net.Mime; +using System.Threading; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Models; +using Sqlbi.Bravo.Models.AnalyzeModel; +using Sqlbi.Bravo.Models.ManageDates; +using Sqlbi.Bravo.Models.TemplateDevelopment; +using Sqlbi.Bravo.Services; + +namespace Sqlbi.Bravo.Controllers; + +/// +/// TemplateDevelopment module controller +/// +/// Status400BadRequest - See the "instance" and "detail" properties to identify the specific occurrence of the problem +[Route("TemplateDevelopment/[action]")] +[ApiController] +[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] +public class TemplateDevelopmentController : ControllerBase { - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Models; - using Sqlbi.Bravo.Models.AnalyzeModel; - using Sqlbi.Bravo.Models.ManageDates; - using Sqlbi.Bravo.Models.TemplateDevelopment; - using Sqlbi.Bravo.Services; - using System.Collections.Generic; - using System.Net.Mime; - using System.Threading; + internal static string ControllerPathSegment = "/TemplateDevelopment"; + + private readonly ITemplateDevelopmentService _templateDevelopmentService; + private readonly IAnalyzeModelService _analyzeModelService; + + public TemplateDevelopmentController(ITemplateDevelopmentService templateDevelopmentService, IAnalyzeModelService analyzeModelService) + { + _templateDevelopmentService = templateDevelopmentService; + _analyzeModelService = analyzeModelService; + } /// - /// TemplateDevelopment module controller + /// Returns all the of type from the organization repository /// - /// Status400BadRequest - See the "instance" and "detail" properties to identify the specific occurrence of the problem - [Route("TemplateDevelopment/[action]")] - [ApiController] - [ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ProblemDetails))] - public class TemplateDevelopmentController : ControllerBase + /// Status200OK - Success + [HttpGet] + [ActionName("GetOrganizationCustomPackages")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesDefaultResponseType] + public IActionResult GetOrganizationCustomPackages(CancellationToken cancellationToken) { - internal static string ControllerPathSegment = "/TemplateDevelopment"; - - private readonly ITemplateDevelopmentService _templateDevelopmentService; - private readonly IAnalyzeModelService _analyzeModelService; + var customPackages = _templateDevelopmentService.GetOrganizationCustomPackages(); + return Ok(customPackages); + } - public TemplateDevelopmentController(ITemplateDevelopmentService templateDevelopmentService, IAnalyzeModelService analyzeModelService) - { - _templateDevelopmentService = templateDevelopmentService; - _analyzeModelService = analyzeModelService; - } + /// + /// Displays a dialog box that prompts the user to select a '.package.json' file or 'code-workspace' file and returns the + /// + /// Status200OK - Success + /// Status204NoContent - User canceled action (e.g. 'Cancel' button has been pressed on a dialog box) + [HttpGet] + [ActionName("BrowseUserCustomPackage")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(CustomPackage))] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesDefaultResponseType] + public IActionResult BrowseUserCustomPackage(bool includeWorkspaces, CancellationToken cancellationToken) + { + var filter = includeWorkspaces ? "Template package or workspace (*.package.json, *.code-workspace)|*.package.json;*.code-workspace" : "Template package (*.package.json)|*.package.json"; - /// - /// Returns all the of type from the organization repository - /// - /// Status200OK - Success - [HttpGet] - [ActionName("GetOrganizationCustomPackages")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesDefaultResponseType] - public IActionResult GetOrganizationCustomPackages(CancellationToken cancellationToken) + if (WindowDialogHelper.OpenFileDialog(filter, out var path, cancellationToken)) { - var customPackages = _templateDevelopmentService.GetOrganizationCustomPackages(); - return Ok(customPackages); + var customPackage = _templateDevelopmentService.GetUserCustomPackage(path); + return Ok(customPackage); } - /// - /// Displays a dialog box that prompts the user to select a '.package.json' file or 'code-workspace' file and returns the - /// - /// Status200OK - Success - /// Status204NoContent - User canceled action (e.g. 'Cancel' button has been pressed on a dialog box) - [HttpGet] - [ActionName("BrowseUserCustomPackage")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(CustomPackage))] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesDefaultResponseType] - public IActionResult BrowseUserCustomPackage(bool includeWorkspaces, CancellationToken cancellationToken) - { - var filter = includeWorkspaces ? "Template package or workspace (*.package.json, *.code-workspace)|*.package.json;*.code-workspace" : "Template package (*.package.json)|*.package.json"; + return NoContent(); + } - if (WindowDialogHelper.OpenFileDialog(filter, out var path, cancellationToken)) - { - var customPackage = _templateDevelopmentService.GetUserCustomPackage(path); - return Ok(customPackage); - } + /// + /// Gets all the available from the embedded templates + /// + /// Status200OK - Success + [HttpGet] + [ActionName("GetConfigurations")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] + [ProducesDefaultResponseType] + public IActionResult GetConfigurations() + { + var configurations = _templateDevelopmentService.GetConfigurations(); + return Ok(configurations); + } - return NoContent(); - } + /// + /// Get the from a custom template package file + /// + /// Status200OK - Success + [HttpGet] + [ActionName("GetPackageConfiguration")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(DateConfiguration))] + [ProducesDefaultResponseType] + public IActionResult GetPackageConfiguration(string path) + { + var configuration = _templateDevelopmentService.GetPackageConfiguration(path); + return Ok(configuration); + } - /// - /// Gets all the available from the embedded templates - /// - /// Status200OK - Success - [HttpGet] - [ActionName("GetConfigurations")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] - [ProducesDefaultResponseType] - public IActionResult GetConfigurations() - { - var configurations = _templateDevelopmentService.GetConfigurations(); - return Ok(configurations); - } + /// + /// Validate the by verifying the existence of the .code-workspace and package.json files + /// + /// Status200OK - Success + [HttpPost] + [ActionName("ValidateCustomPackage")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(CustomPackage))] + [ProducesDefaultResponseType] + public IActionResult Validate(CustomPackage customPackage, CancellationToken cancellationToken) + { + var validatedCustomPackage = _templateDevelopmentService.Validate(customPackage); + return Ok(validatedCustomPackage); + } - /// - /// Get the from a custom template package file - /// - /// Status200OK - Success - [HttpGet] - [ActionName("GetPackageConfiguration")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(DateConfiguration))] - [ProducesDefaultResponseType] - public IActionResult GetPackageConfiguration(string path) + /// + /// Create and initialize a new template development workspace by cloning an existing template from the provided + /// + /// Status200OK - Success + /// Status204NoContent - User canceled action (e.g. 'Cancel' button has been pressed on a dialog box) + [HttpPost] + [ActionName("CreateWorkspace")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(CustomPackage))] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesDefaultResponseType] + public IActionResult CreateWorkspace(CreateWorkspaceRequest request, CancellationToken cancellationToken) + { + if (WindowDialogHelper.BrowseFolderDialog(out var path, cancellationToken)) { - var configuration = _templateDevelopmentService.GetPackageConfiguration(path); - return Ok(configuration); + var customPackage = _templateDevelopmentService.CreateWorkspace(path, request.Name!, request.Configuration!); + return Ok(customPackage); } - /// - /// Validate the by verifying the existence of the .code-workspace and package.json files - /// - /// Status200OK - Success - [HttpPost] - [ActionName("ValidateCustomPackage")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(CustomPackage))] - [ProducesDefaultResponseType] - public IActionResult Validate(CustomPackage customPackage, CancellationToken cancellationToken) - { - var validatedCustomPackage = _templateDevelopmentService.Validate(customPackage); - return Ok(validatedCustomPackage); - } + return NoContent(); + } - /// - /// Create and initialize a new template development workspace by cloning an existing template from the provided - /// - /// Status200OK - Success - /// Status204NoContent - User canceled action (e.g. 'Cancel' button has been pressed on a dialog box) - [HttpPost] - [ActionName("CreateWorkspace")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(CustomPackage))] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesDefaultResponseType] - public IActionResult CreateWorkspace(CreateWorkspaceRequest request, CancellationToken cancellationToken) - { - if (WindowDialogHelper.BrowseFolderDialog(out var path, cancellationToken)) - { - var customPackage = _templateDevelopmentService.CreateWorkspace(path, request.Name!, request.Configuration!); - return Ok(customPackage); - } + /// + /// Configure an existing template development workspace by updating the bravo-config.json file + /// + /// Status200OK - Success + /// Status404NotFound - The selected folder does not contain the Bravo workspace configuration file + [HttpGet] + [ActionName("ConfigureWorkspace")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesDefaultResponseType] + public IActionResult ConfigureWorkspace(string workspacePath, bool openCodeWorkspace, CancellationToken cancellationToken) + { + if (_templateDevelopmentService.ConfigureWorkspace(workspacePath, openCodeWorkspace)) + return Ok(); - return NoContent(); - } + return NotFound(); + } - /// - /// Configure an existing template development workspace by updating the bravo-config.json file - /// - /// Status200OK - Success - /// Status404NotFound - The selected folder does not contain the Bravo workspace configuration file - [HttpGet] - [ActionName("ConfigureWorkspace")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesDefaultResponseType] - public IActionResult ConfigureWorkspace(string workspacePath, bool openCodeWorkspace, CancellationToken cancellationToken) + /// + /// Launches the Power BI Desktop process after displaying a dialog box that prompts the user to select the PBIX file to be opened + /// + /// Status200OK - Success + /// Status403Forbidden - The path is invalid or not allowed + [HttpGet] + [ActionName("PBIDesktopOpenPBIX")] + [Consumes(MediaTypeNames.Application.Json)] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PBIDesktopReport))] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesDefaultResponseType] + public IActionResult PBIDesktopOpenPBIX(string path, bool waitForStarted, CancellationToken cancellationToken) + { + if (ProcessHelper.OpenShellExecute(path, waitForStarted, out var processId, cancellationToken)) { - if (_templateDevelopmentService.ConfigureWorkspace(workspacePath, openCodeWorkspace)) - return Ok(); - - return NotFound(); + var report = PBIDesktopReport.CreateFrom(processId.Value); + return Ok(report); } + return Forbid(); + } - /// - /// Launches the Power BI Desktop process after displaying a dialog box that prompts the user to select the PBIX file to be opened - /// - /// Status200OK - Success - /// Status403Forbidden - The path is invalid or not allowed - [HttpGet] - [ActionName("PBIDesktopOpenPBIX")] - [Consumes(MediaTypeNames.Application.Json)] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(PBIDesktopReport))] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesDefaultResponseType] - public IActionResult PBIDesktopOpenPBIX(string path, bool waitForStarted, CancellationToken cancellationToken) + /// + /// Returns a list of all open + /// + /// Status200OK - Success + /// Status204NoContent - User canceled the operation + [HttpGet] + [ActionName("GetReports")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesDefaultResponseType] + public IActionResult GetReports(CancellationToken cancellationToken) + { + try { - if (ProcessHelper.OpenShellExecute(path, waitForStarted, out var processId, cancellationToken)) - { - var report = PBIDesktopReport.CreateFrom(processId.Value); - return Ok(report); - } - return Forbid(); + var reports = _analyzeModelService.GetReports(cancellationToken); + return Ok(reports); } - - /// - /// Returns a list of all open - /// - /// Status200OK - Success - /// Status204NoContent - User canceled the operation - [HttpGet] - [ActionName("GetReports")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(IEnumerable))] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesDefaultResponseType] - public IActionResult GetReports(CancellationToken cancellationToken) + catch (OperationCanceledException) { - try - { - var reports = _analyzeModelService.GetReports(cancellationToken); - return Ok(reports); - } - catch (OperationCanceledException) - { - return NoContent(); - } + return NoContent(); } + } - /// - /// Returns a database model from a PBIDesktop instance - /// - /// Status200OK - Success - [HttpPost] - [ActionName("GetModel")] - [Consumes(MediaTypeNames.Application.Json)] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TabularDatabase))] - [ProducesDefaultResponseType] - public IActionResult GetDatabase(PBIDesktopReport report, CancellationToken cancellationToken) - { - var database = _analyzeModelService.GetDatabase(report, cancellationToken); - return Ok(database); - } + /// + /// Returns a database model from a PBIDesktop instance + /// + /// Status200OK - Success + [HttpPost] + [ActionName("GetModel")] + [Consumes(MediaTypeNames.Application.Json)] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(TabularDatabase))] + [ProducesDefaultResponseType] + public IActionResult GetDatabase(PBIDesktopReport report, CancellationToken cancellationToken) + { + var database = _analyzeModelService.GetDatabase(report, cancellationToken); + return Ok(database); + } - /// - /// Applies the provided file without commit changes and returns a preview of changes to objects and data - /// - /// Status200OK - Success - [HttpPost] - [ActionName("GetPreviewChanges")] - [Produces(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Dax.Template.Model.ModelChanges))] - [ProducesDefaultResponseType] - public IActionResult GetPreviewChanges(WorkspacePreviewChangesRequest request, CancellationToken cancellationToken) - { - var modelChanges = _templateDevelopmentService.GetPreviewChanges(request.Report!, request.Settings!, cancellationToken); - return Ok(modelChanges); - } + /// + /// Applies the provided file without commit changes and returns a preview of changes to objects and data + /// + /// Status200OK - Success + [HttpPost] + [ActionName("GetPreviewChanges")] + [Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(Dax.Template.Model.ModelChanges))] + [ProducesDefaultResponseType] + public IActionResult GetPreviewChanges(WorkspacePreviewChangesRequest request, CancellationToken cancellationToken) + { + var modelChanges = _templateDevelopmentService.GetPreviewChanges(request.Report!, request.Settings!, cancellationToken); + return Ok(modelChanges); } } diff --git a/src/GlobalUsings.cs b/src/GlobalUsings.cs deleted file mode 100644 index b99c99bc..00000000 --- a/src/GlobalUsings.cs +++ /dev/null @@ -1,13 +0,0 @@ -global using System; -global using System.Collections.Generic; -global using System.Diagnostics; -global using System.Diagnostics.CodeAnalysis; -global using System.IO; -global using System.Linq; -global using System.Net; -global using System.Net.Mime; -global using System.Text; -global using System.Text.Json; -global using System.Threading; -global using System.Threading.Tasks; -global using System.Globalization; diff --git a/src/Infrastructure/AppEnvironment.cs b/src/Infrastructure/AppEnvironment.cs index a08d80ae..a8e35fd9 100644 --- a/src/Infrastructure/AppEnvironment.cs +++ b/src/Infrastructure/AppEnvironment.cs @@ -1,269 +1,268 @@ -namespace Sqlbi.Bravo.Infrastructure +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Text.Json; +using Microsoft.Win32; +using Sqlbi.Bravo.Infrastructure.Configuration; +using Sqlbi.Bravo.Infrastructure.Configuration.Settings; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Security; +using Sqlbi.Bravo.Models; +using Sqlbi.Bravo.Models.FormatDax; + +namespace Sqlbi.Bravo.Infrastructure; + +internal static class AppEnvironment { - using Microsoft.Win32; - using Sqlbi.Bravo.Infrastructure.Configuration; - using Sqlbi.Bravo.Infrastructure.Configuration.Settings; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Security; - using Sqlbi.Bravo.Models; - using Sqlbi.Bravo.Models.FormatDax; - using System; - using System.Collections.Concurrent; - using System.Diagnostics; - using System.Drawing; - using System.IO; - using System.Linq; - using System.Runtime.InteropServices; - using System.Runtime.Versioning; - using System.Text.Json; - - internal static class AppEnvironment + private static readonly Lazy _deploymentMode; + + public static readonly string ApiAuthenticationSchema = "BravoAuth"; + public static readonly string ApiAuthenticationToken = Cryptography.GenerateSimpleToken(); + public static readonly string ApiAuthenticationTokenTemplateDevelopment = Cryptography.GenerateSimpleToken(); + public static readonly string ApplicationManufacturer = "SQLBI"; + public static readonly string ApplicationWebsiteUrl = "https://bravo.bi"; + public static readonly string ApplicationName = "Bravo"; + public static readonly string ApplicationStoreAliasName = "BravoStore"; + public static readonly string ApplicationMainWindowTitle = "Bravo for Power BI"; + public static readonly string ApplicationInstanceUniqueName = $"{ApplicationName}-{Guid.NewGuid():D}"; + public static readonly string ApplicationRegistryKeyName = $@"SOFTWARE\{ApplicationManufacturer}\{ApplicationName}"; + public static readonly string ApplicationRegistryApplicationTelemetryEnabledValue = "applicationTelemetryEnabled"; + public static readonly string ApplicationRegistryApplicationTitleVersionHiddenValue = "applicationTitleVersionHidden"; + public static readonly string ApplicationRegistryApplicationInstallFolderValue = "installFolder"; + public static readonly string PBIDesktopProcessName = "PBIDesktop"; + public static readonly string PBIDesktopSSASProcessImageName = "msmdsrv.exe"; + public static readonly string[] PBIDesktopMainWindowTitleSuffixes = new string[] { - private static readonly Lazy _deploymentMode; - - public static readonly string ApiAuthenticationSchema = "BravoAuth"; - public static readonly string ApiAuthenticationToken = Cryptography.GenerateSimpleToken(); - public static readonly string ApiAuthenticationTokenTemplateDevelopment = Cryptography.GenerateSimpleToken(); - public static readonly string ApplicationManufacturer = "SQLBI"; - public static readonly string ApplicationWebsiteUrl = "https://bravo.bi"; - public static readonly string ApplicationName = "Bravo"; - public static readonly string ApplicationStoreAliasName = "BravoStore"; - public static readonly string ApplicationMainWindowTitle = "Bravo for Power BI"; - public static readonly string ApplicationInstanceUniqueName = $"{ApplicationName}-{Guid.NewGuid():D}"; - public static readonly string ApplicationRegistryKeyName = $@"SOFTWARE\{ ApplicationManufacturer }\{ ApplicationName }"; - public static readonly string ApplicationRegistryApplicationTelemetryEnabledValue = "applicationTelemetryEnabled"; - public static readonly string ApplicationRegistryApplicationTitleVersionHiddenValue = "applicationTitleVersionHidden"; - public static readonly string ApplicationRegistryApplicationInstallFolderValue = "installFolder"; - public static readonly string PBIDesktopProcessName = "PBIDesktop"; - public static readonly string PBIDesktopSSASProcessImageName = "msmdsrv.exe"; - public static readonly string[] PBIDesktopMainWindowTitleSuffixes = new string[] - { - // The PBIDesktop main window title is culture-specific. - // The suffix is not always present, for example, it is not added when the save/share function in OneDrive/SharePoint is active. - - // Different dash characters are used as a separator - // See https://github.com/sql-bi/Bravo/issues/476 - " \u002D Power BI Desktop", // Dash Punctuation - minus hyphen - " \u2013 Power BI Desktop", // Dash Punctuation - en dash - " \u2014 Power BI Desktop", // Dash Punctuation - em dash - - // The whitespace character may not be present - Swedish/sv - // See https://github.com/sql-bi/Bravo/issues/510 - "\u2013Power BI Desktop", // Dash Punctuation - en dash - - // NBSP char instead of whitespace - Latvian/lv - "\u00A0\u2014 Power BI Desktop", - }; - public static readonly Color ThemeColorDark = ColorTranslator.FromHtml("#202020"); - public static readonly Color ThemeColorLight = ColorTranslator.FromHtml("#F3F3F3"); - public static readonly DaxLineBreakStyle FormatDaxLineBreakDefault = DaxLineBreakStyle.InitialLineBreak; - public static readonly string CredentialManagerProxyCredentialName = "Bravo for Power BI/proxy"; - - public static readonly string[] TrustedUriHosts = new[] - { - "bravo.bi", - "sqlbi.com", - "github.com", - "microsoft.com", - "daxformatter.com", - "bravorelease.blob.core.windows.net", - "code.visualstudio.com", - "marketplace.visualstudio.com" - }; - - static AppEnvironment() - { - Debug.Assert(Environment.ProcessPath is not null); + // The PBIDesktop main window title is culture-specific. + // The suffix is not always present, for example, it is not added when the save/share function in OneDrive/SharePoint is active. + + // Different dash characters are used as a separator + // See https://github.com/sql-bi/Bravo/issues/476 + " \u002D Power BI Desktop", // Dash Punctuation - minus hyphen + " \u2013 Power BI Desktop", // Dash Punctuation - en dash + " \u2014 Power BI Desktop", // Dash Punctuation - em dash + + // The whitespace character may not be present - Swedish/sv + // See https://github.com/sql-bi/Bravo/issues/510 + "\u2013Power BI Desktop", // Dash Punctuation - en dash + + // NBSP char instead of whitespace - Latvian/lv + "\u00A0\u2014 Power BI Desktop", + }; + public static readonly Color ThemeColorDark = ColorTranslator.FromHtml("#202020"); + public static readonly Color ThemeColorLight = ColorTranslator.FromHtml("#F3F3F3"); + public static readonly DaxLineBreakStyle FormatDaxLineBreakDefault = DaxLineBreakStyle.InitialLineBreak; + public static readonly string CredentialManagerProxyCredentialName = "Bravo for Power BI/proxy"; + + public static readonly string[] TrustedUriHosts = new[] + { + "bravo.bi", + "sqlbi.com", + "github.com", + "microsoft.com", + "daxformatter.com", + "bravorelease.blob.core.windows.net", + "code.visualstudio.com", + "marketplace.visualstudio.com" + }; + + static AppEnvironment() + { + Debug.Assert(Environment.ProcessPath is not null); - _deploymentMode = new(() => GetDeploymentMode()); - using var currentProcess = Process.GetCurrentProcess(); + _deploymentMode = new(() => GetDeploymentMode()); + using var currentProcess = Process.GetCurrentProcess(); - ProcessId = Environment.ProcessId; - SessionId = currentProcess.SessionId; - ProcessPath = Environment.ProcessPath!; + ProcessId = Environment.ProcessId; + SessionId = currentProcess.SessionId; + ProcessPath = Environment.ProcessPath!; - VersionInfo = new AppVersionInfo(); - ApplicationDataPath = Path.Combine(Environment.GetFolderPath(DeploymentMode == AppDeploymentMode.Packaged ? Environment.SpecialFolder.UserProfile : Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.DoNotVerify), ApplicationName); - ApplicationTempPath = Path.Combine(ApplicationDataPath, ".temp"); - UserSettingsFilePath = Path.Combine(ApplicationDataPath, "usersettings.json"); - MsalTokenCacheFilePath = Path.Combine(ApplicationDataPath, ".msalcache"); - WebView2VersionInfo = WebView2Helper.GetRuntimeVersionInfo(); + VersionInfo = new AppVersionInfo(); + ApplicationDataPath = Path.Combine(Environment.GetFolderPath(DeploymentMode == AppDeploymentMode.Packaged ? Environment.SpecialFolder.UserProfile : Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.DoNotVerify), ApplicationName); + ApplicationTempPath = Path.Combine(ApplicationDataPath, ".temp"); + UserSettingsFilePath = Path.Combine(ApplicationDataPath, "usersettings.json"); + MsalTokenCacheFilePath = Path.Combine(ApplicationDataPath, ".msalcache"); + WebView2VersionInfo = WebView2Helper.GetRuntimeVersionInfo(); - Diagnostics = new ConcurrentDictionary(); - DefaultJsonOptions = new(JsonSerializerDefaults.Web) { MaxDepth = 32 }; // see Microsoft.AspNetCore.Mvc.JsonOptions.JsonSerializerOptions + Diagnostics = new ConcurrentDictionary(); + DefaultJsonOptions = new(JsonSerializerDefaults.Web) { MaxDepth = 32 }; // see Microsoft.AspNetCore.Mvc.JsonOptions.JsonSerializerOptions - AddEnvironmentDiagnosticInfo(); - } + AddEnvironmentDiagnosticInfo(); + } - /// - /// Specifies the unique identifier that spans a period of time from log in to log out and is generated by the operating system when the user's session is created. - /// It allows us to correctly identify the ownership of the process in case multiple sessions are active or in multi-session environments such as Remote Desktop Services (a.k.a Terminal Services). - /// - public static int SessionId { get; } + /// + /// Specifies the unique identifier that spans a period of time from log in to log out and is generated by the operating system when the user's session is created. + /// It allows us to correctly identify the ownership of the process in case multiple sessions are active or in multi-session environments such as Remote Desktop Services (a.k.a Terminal Services). + /// + public static int SessionId { get; } - public static int ProcessId { get; } + public static int ProcessId { get; } - public static string ProcessPath { get; } + public static string ProcessPath { get; } - public static AppPublishMode PublishMode + public static AppPublishMode PublishMode + { + get { - get - { #if PUBLISHMODE_SELFCONTAINED - return AppPublishMode.SelfContained; + return AppPublishMode.SelfContained; #else - return AppPublishMode.FrameworkDependent; + return AppPublishMode.FrameworkDependent; #endif - } } + } - public static AppDeploymentMode DeploymentMode => _deploymentMode.Value; + public static AppDeploymentMode DeploymentMode => _deploymentMode.Value; - /// - /// Returns the HKEY registry key used to install the current application instance. Returns null if it is a packaged or portable app instance - /// - public static RegistryKey? ApplicationInstallerRegistryHKey + /// + /// Returns the HKEY registry key used to install the current application instance. Returns null if it is a packaged or portable app instance + /// + public static RegistryKey? ApplicationInstallerRegistryHKey + { + get { - get + var registryKey = DeploymentMode switch { - var registryKey = DeploymentMode switch - { - AppDeploymentMode.PerUser => Registry.CurrentUser, - AppDeploymentMode.PerMachine => Registry.LocalMachine, - _ => null, - }; - - return registryKey; - } - } + AppDeploymentMode.PerUser => Registry.CurrentUser, + AppDeploymentMode.PerMachine => Registry.LocalMachine, + _ => null, + }; - public static AppVersionInfo VersionInfo { get; } + return registryKey; + } + } - public static JsonSerializerOptions DefaultJsonOptions { get; } + public static AppVersionInfo VersionInfo { get; } - public static string ApplicationDataPath { get; } + public static JsonSerializerOptions DefaultJsonOptions { get; } - public static string ApplicationTempPath { get; } + public static string ApplicationDataPath { get; } - public static string UserSettingsFilePath { get; } + public static string ApplicationTempPath { get; } - public static string MsalTokenCacheFilePath { get; } + public static string UserSettingsFilePath { get; } - public static string? WebView2VersionInfo { get; } + public static string MsalTokenCacheFilePath { get; } - public static bool IsWebView2RuntimeInstalled => WebView2VersionInfo is not null; + public static string? WebView2VersionInfo { get; } - public static bool IsDiagnosticLevelVerbose => UserPreferences.Current.DiagnosticLevel == DiagnosticLevelType.Verbose; + public static bool IsWebView2RuntimeInstalled => WebView2VersionInfo is not null; - public static ConcurrentDictionary Diagnostics { get; } + public static bool IsDiagnosticLevelVerbose => UserPreferences.Current.DiagnosticLevel == DiagnosticLevelType.Verbose; - public static void AddDiagnostics(string name, Exception exception, DiagnosticMessageSeverity severity = DiagnosticMessageSeverity.Error) - { - var content = exception.ToString(); - AddDiagnostics(DiagnosticMessageType.Text, name, content, severity); - } + public static ConcurrentDictionary Diagnostics { get; } - public static void AddDiagnostics(DiagnosticMessageType type, string name, string content, DiagnosticMessageSeverity severity = DiagnosticMessageSeverity.None) - { - var message = DiagnosticMessage.Create(type, severity, name, content); - - _= Diagnostics.TryAdd(message, message); - } + public static void AddDiagnostics(string name, Exception exception, DiagnosticMessageSeverity severity = DiagnosticMessageSeverity.Error) + { + var content = exception.ToString(); + AddDiagnostics(DiagnosticMessageType.Text, name, content, severity); + } - private static void AddEnvironmentDiagnosticInfo() - { - if (!IsDiagnosticLevelVerbose) - return; + public static void AddDiagnostics(DiagnosticMessageType type, string name, string content, DiagnosticMessageSeverity severity = DiagnosticMessageSeverity.None) + { + var message = DiagnosticMessage.Create(type, severity, name, content); - var targetFramework = typeof(Program).Assembly.GetCustomAttributes(typeof(TargetFrameworkAttribute), inherit: false).OfType().FirstOrDefault(); + _ = Diagnostics.TryAdd(message, message); + } - var info = new - { - SystemOSVersion = Environment.OSVersion.VersionString, - SystemProcessorCount = Environment.ProcessorCount, - ProcessId, - ProcessPath, - ProcessSessionId = SessionId, - RuntimeOSDescription = RuntimeInformation.OSDescription.ToString(), - RuntimeOSVersion = RuntimeInformation.RuntimeIdentifier, - RuntimeFrameworkDescription = RuntimeInformation.FrameworkDescription, - TargetFrameworkName = targetFramework?.FrameworkName ?? "n/a", - WebView2VersionInfo, - // - ApplicationPublishMode = PublishMode.ToString(), - ApplicationDeploymentMode = DeploymentMode.ToString(), - ApplicationVersion = VersionInfo.InformationalVersion, - ApplicationDataPath, - ApplicationTempPath, - ApplicationUserSettingsFilePath = UserSettingsFilePath, - }; + private static void AddEnvironmentDiagnosticInfo() + { + if (!IsDiagnosticLevelVerbose) + return; - AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(AppEnvironment)}.EnvironmentInfo", content: JsonSerializer.Serialize(info)); - } + var targetFramework = typeof(Program).Assembly.GetCustomAttributes(typeof(TargetFrameworkAttribute), inherit: false).OfType().FirstOrDefault(); - private static AppDeploymentMode GetDeploymentMode() + var info = new { - if (DesktopBridgeHelper.IsRunningAsMsixPackage()) - return AppDeploymentMode.Packaged; + SystemOSVersion = Environment.OSVersion.VersionString, + SystemProcessorCount = Environment.ProcessorCount, + ProcessId, + ProcessPath, + ProcessSessionId = SessionId, + RuntimeOSDescription = RuntimeInformation.OSDescription.ToString(), + RuntimeOSVersion = RuntimeInformation.RuntimeIdentifier, + RuntimeFrameworkDescription = RuntimeInformation.FrameworkDescription, + TargetFrameworkName = targetFramework?.FrameworkName ?? "n/a", + WebView2VersionInfo, + // + ApplicationPublishMode = PublishMode.ToString(), + ApplicationDeploymentMode = DeploymentMode.ToString(), + ApplicationVersion = VersionInfo.InformationalVersion, + ApplicationDataPath, + ApplicationTempPath, + ApplicationUserSettingsFilePath = UserSettingsFilePath, + }; - var hklmValueString = Registry.LocalMachine.GetStringValue(subkeyName: ApplicationRegistryKeyName, valueName: ApplicationRegistryApplicationInstallFolderValue); - if (hklmValueString is not null) - { - if (CommonHelper.AreDirectoryPathsEqual(AppContext.BaseDirectory, hklmValueString)) - return AppDeploymentMode.PerMachine; - } + AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(AppEnvironment)}.EnvironmentInfo", content: JsonSerializer.Serialize(info)); + } - var hkcuValueString = Registry.CurrentUser.GetStringValue(subkeyName: ApplicationRegistryKeyName, valueName: ApplicationRegistryApplicationInstallFolderValue); - if (hkcuValueString is not null) - { - if (CommonHelper.AreDirectoryPathsEqual(AppContext.BaseDirectory, hkcuValueString)) - return AppDeploymentMode.PerUser; - } + private static AppDeploymentMode GetDeploymentMode() + { + if (DesktopBridgeHelper.IsRunningAsMsixPackage()) + return AppDeploymentMode.Packaged; - return AppDeploymentMode.Portable; + var hklmValueString = Registry.LocalMachine.GetStringValue(subkeyName: ApplicationRegistryKeyName, valueName: ApplicationRegistryApplicationInstallFolderValue); + if (hklmValueString is not null) + { + if (CommonHelper.AreDirectoryPathsEqual(AppContext.BaseDirectory, hklmValueString)) + return AppDeploymentMode.PerMachine; } - } - public enum AppDeploymentMode - { - None = 0, - - /// - /// Portable ZIP package - /// - Portable = 1, - - /// - /// MSI package per-user installation that does not require elevated privileges to install - /// - PerUser = 2, - - /// - /// MSI package per-machine installation that requires elevated privileges to install - /// - PerMachine = 3, - - /// - /// MSIX packaged application - /// - Packaged = 4, - } + var hkcuValueString = Registry.CurrentUser.GetStringValue(subkeyName: ApplicationRegistryKeyName, valueName: ApplicationRegistryApplicationInstallFolderValue); + if (hkcuValueString is not null) + { + if (CommonHelper.AreDirectoryPathsEqual(AppContext.BaseDirectory, hkcuValueString)) + return AppDeploymentMode.PerUser; + } - public enum AppPublishMode - { - /// - /// Published as a framework-dependent application that relies on a shared system-wide - /// version of the .NET runtime. The application will not include the .NET runtime and - /// will require it to be installed on the host machine to run. - /// - FrameworkDependent = 0, - - /// - /// Published as a self-contained application that includes a private copy of the .NET runtime. - /// The application will not rely on a shared system-wide version of the .NET runtime and - /// can run on a host machine even if the .NET runtime is not installed. - /// - SelfContained = 1, + return AppDeploymentMode.Portable; } } + +public enum AppDeploymentMode +{ + None = 0, + + /// + /// Portable ZIP package + /// + Portable = 1, + + /// + /// MSI package per-user installation that does not require elevated privileges to install + /// + PerUser = 2, + + /// + /// MSI package per-machine installation that requires elevated privileges to install + /// + PerMachine = 3, + + /// + /// MSIX packaged application + /// + Packaged = 4, +} + +public enum AppPublishMode +{ + /// + /// Published as a framework-dependent application that relies on a shared system-wide + /// version of the .NET runtime. The application will not include the .NET runtime and + /// will require it to be installed on the host machine to run. + /// + FrameworkDependent = 0, + + /// + /// Published as a self-contained application that includes a private copy of the .NET runtime. + /// The application will not rely on a shared system-wide version of the .NET runtime and + /// can run on a host machine even if the .NET runtime is not installed. + /// + SelfContained = 1, +} diff --git a/src/Infrastructure/AppExceptions.cs b/src/Infrastructure/AppExceptions.cs index ea4fbf2e..daa3d478 100644 --- a/src/Infrastructure/AppExceptions.cs +++ b/src/Infrastructure/AppExceptions.cs @@ -1,223 +1,221 @@ -namespace Sqlbi.Bravo.Infrastructure +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; +using Sqlbi.Bravo.Infrastructure.Extensions; + +namespace Sqlbi.Bravo.Infrastructure; + +/// +/// Represents unexpected errors that occur during application execution. +/// +/// +/// This class does not inherit from . will be tracked in telemetry as an unhandled exception and should not be handled at the application level. +/// +[Serializable] +public class BravoUnexpectedException : Exception { - using Sqlbi.Bravo.Infrastructure.Extensions; - using System; - using System.Diagnostics.CodeAnalysis; - using System.Runtime.CompilerServices; - using System.Text.Json.Serialization; - using System.Threading.Tasks; + public BravoUnexpectedException(string message) + : base(message) + { + } /// - /// Represents unexpected errors that occur during application execution. + /// Throws an if is null. See https://github.com/dotnet/runtime/issues/48573 /// - /// - /// This class does not inherit from . will be tracked in telemetry as an unhandled exception and should not be handled at the application level. - /// - [Serializable] - public class BravoUnexpectedException : Exception + /// The reference type argument to validate as non-null. + /// The name of the parameter with which corresponds. + public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression("argument")] string? paramName = null) { - public BravoUnexpectedException(string message) - : base(message) + if (argument is null) { + ThrowUnexpectedArgumentNullException(paramName); } + } - /// - /// Throws an if is null. See https://github.com/dotnet/runtime/issues/48573 - /// - /// The reference type argument to validate as non-null. - /// The name of the parameter with which corresponds. - public static void ThrowIfNull([NotNull] object? argument, [CallerArgumentExpression("argument")] string? paramName = null) + /// + /// Throws an if is false. + /// + /// The Boolean condition to be evaluated. + /// The expression of the parameter with which corresponds. + public static void Assert([DoesNotReturnIf(false)] bool condition, [CallerArgumentExpression("condition")] string? conditionExpression = null) + { + if (condition == false) { - if (argument is null) - { - ThrowUnexpectedArgumentNullException(paramName); - } + ThrowUnexpectedInvalidOperationException(conditionExpression); } + } - /// - /// Throws an if is false. - /// - /// The Boolean condition to be evaluated. - /// The expression of the parameter with which corresponds. - public static void Assert([DoesNotReturnIf(false)] bool condition, [CallerArgumentExpression("condition")] string? conditionExpression = null) - { - if (condition == false) - { - ThrowUnexpectedInvalidOperationException(conditionExpression); - } - } + [DoesNotReturn] + private static void ThrowUnexpectedArgumentNullException(string? paramName) => throw new BravoUnexpectedArgumentNullException(paramName); - [DoesNotReturn] - private static void ThrowUnexpectedArgumentNullException(string? paramName) => throw new BravoUnexpectedArgumentNullException(paramName); + [DoesNotReturn] + private static void ThrowUnexpectedInvalidOperationException(string? conditionExpression) => throw new BravoUnexpectedInvalidOperationException($"Condition failed '{conditionExpression}'"); +} - [DoesNotReturn] - private static void ThrowUnexpectedInvalidOperationException(string? conditionExpression) => throw new BravoUnexpectedInvalidOperationException($"Condition failed '{conditionExpression}'"); +public class BravoUnexpectedPolicyViolationException : BravoUnexpectedException +{ + public BravoUnexpectedPolicyViolationException(string policyName) + : base(message: policyName) + { } +} - public class BravoUnexpectedPolicyViolationException : BravoUnexpectedException +public class BravoUnexpectedArgumentNullException : ArgumentNullException +{ + public BravoUnexpectedArgumentNullException(string? paramName) + : base(paramName) { - public BravoUnexpectedPolicyViolationException(string policyName) - : base(message: policyName) - { - } } +} - public class BravoUnexpectedArgumentNullException : ArgumentNullException +public class BravoUnexpectedInvalidOperationException : InvalidOperationException +{ + public BravoUnexpectedInvalidOperationException(string? message) + : base(message) { - public BravoUnexpectedArgumentNullException(string? paramName) - : base(paramName) - { - } } +} + +/// +/// Represents the base class for error that occurs during application execution. +/// +[Serializable] +public class BravoException : Exception +{ + public BravoProblem Problem { get; private set; } - public class BravoUnexpectedInvalidOperationException : InvalidOperationException + public string? ProblemDetail => Message.NullIfWhiteSpace(); + + public string ProblemInstance => $"{(int)Problem}"; + + public BravoException(BravoProblem problem) + : base(string.Empty) { - public BravoUnexpectedInvalidOperationException(string? message) - : base(message) - { - } + Problem = problem; } + public BravoException(BravoProblem problem, string message) + : base(message) + { + Problem = problem; + } + + public BravoException(BravoProblem problem, string message, Exception innerException) + : base(message, innerException) + { + Problem = problem; + } +} + +public enum BravoProblem +{ + [JsonPropertyName("None")] + None = 0, + /// - /// Represents the base class for error that occurs during application execution. + /// An was thrown (request aborted or user-cancelled operation). + /// Response message is not sent to the user/UI due to the aborted request. /// - [Serializable] - public class BravoException : Exception - { - public BravoProblem Problem { get; private set; } + [JsonPropertyName("OperationCancelled")] + OperationCancelled = 1, - public string? ProblemDetail => Message.NullIfWhiteSpace(); + /// + /// A connection problem arises between the server and current application + /// + [JsonPropertyName("AnalysisServicesConnectionFailed")] + AnalysisServicesConnectionFailed = 10, - public string ProblemInstance => $"{ (int)Problem }"; + /// + /// TOM database does not exists in the collection or the user does not have admin rights for it. + /// + [JsonPropertyName("TOMDatabaseDatabaseNotFound")] + TOMDatabaseDatabaseNotFound = 101, - public BravoException(BravoProblem problem) - : base(string.Empty) - { - Problem = problem; - } + /// + /// TOM database update failed while saving local changes made on the model tree to the version of the model residing in the database server. + /// + [JsonPropertyName("TOMDatabaseUpdateFailed")] + TOMDatabaseUpdateFailed = 102, - public BravoException(BravoProblem problem, string message) - : base(message) - { - Problem = problem; - } + /// + /// TOM measure update request conflict with current state of the target resource + /// + [JsonPropertyName("TOMDatabaseUpdateConflictMeasure")] + TOMDatabaseUpdateConflictMeasure = 103, - public BravoException(BravoProblem problem, string message, Exception innerException) - : base(message, innerException) - { - Problem = problem; - } - } + /// + /// TOM measure update request failed because the measure contains DaxFormatter errors + /// + [JsonPropertyName("TOMDatabaseUpdateErrorMeasure")] + TOMDatabaseUpdateErrorMeasure = 104, - public enum BravoProblem - { - [JsonPropertyName("None")] - None = 0, - - /// - /// An was thrown (request aborted or user-cancelled operation). - /// Response message is not sent to the user/UI due to the aborted request. - /// - [JsonPropertyName("OperationCancelled")] - OperationCancelled = 1, - - /// - /// A connection problem arises between the server and current application - /// - [JsonPropertyName("AnalysisServicesConnectionFailed")] - AnalysisServicesConnectionFailed = 10, - - /// - /// TOM database does not exists in the collection or the user does not have admin rights for it. - /// - [JsonPropertyName("TOMDatabaseDatabaseNotFound")] - TOMDatabaseDatabaseNotFound = 101, - - /// - /// TOM database update failed while saving local changes made on the model tree to the version of the model residing in the database server. - /// - [JsonPropertyName("TOMDatabaseUpdateFailed")] - TOMDatabaseUpdateFailed = 102, - - /// - /// TOM measure update request conflict with current state of the target resource - /// - [JsonPropertyName("TOMDatabaseUpdateConflictMeasure")] - TOMDatabaseUpdateConflictMeasure = 103, - - /// - /// TOM measure update request failed because the measure contains DaxFormatter errors - /// - [JsonPropertyName("TOMDatabaseUpdateErrorMeasure")] - TOMDatabaseUpdateErrorMeasure = 104, - - /// - /// The connection to the requested resource is not supported - /// - [JsonPropertyName("ConnectionUnsupported")] - ConnectionUnsupported = 200, - - /// - /// An error occurred while saving user settings - /// - [JsonPropertyName("UserSettingsSaveError")] - UserSettingsSaveError = 300, - - /// - /// An error occurs during token acquisition. - /// - /// - /// Exceptions in MSAL.NET are intended for app developers to troubleshoot and not for displaying to end-users - /// - [JsonPropertyName("SignInMsalExceptionOccurred")] - SignInMsalExceptionOccurred = 400, - - /// - /// An error occurred while importing the VPAX file - /// - [JsonPropertyName("VpaxFileImportError")] - VpaxFileImportError = 500, - - /// - /// An error has occurred while exporting the VPAX file - /// - [JsonPropertyName("VpaxFileExportError")] - VpaxFileExportError = 501, - - /// - /// You are not connected to the Internet - /// - [JsonPropertyName("NetworkError")] - NetworkError = 600, - - /// - /// An exception occurred while exporting data to file - /// - [JsonPropertyName("ExportDataFileError")] - ExportDataFileError = 700, - - /// - /// An exception occurred while executing the DAX template engine - /// - [JsonPropertyName("ManageDateTemplateError")] - ManageDateTemplateError = 800, - - /// - /// An exception occurred while executing the template development APIs - /// - [JsonPropertyName("TemplateDevelopmentError")] - TemplateDevelopmentError = 900, - - /// - /// An error occurred while obfuscating the VPAX file - /// - [JsonPropertyName("VpaxObfuscationError")] - VpaxObfuscationError = 1000, - - /// - /// An error occurred while deobfuscating the VPAX file - /// - [JsonPropertyName("VpaxDeobfuscationError")] - VpaxDeobfuscationError = 1001, - } + /// + /// The connection to the requested resource is not supported + /// + [JsonPropertyName("ConnectionUnsupported")] + ConnectionUnsupported = 200, + + /// + /// An error occurred while saving user settings + /// + [JsonPropertyName("UserSettingsSaveError")] + UserSettingsSaveError = 300, + + /// + /// An error occurs during token acquisition. + /// + /// + /// Exceptions in MSAL.NET are intended for app developers to troubleshoot and not for displaying to end-users + /// + [JsonPropertyName("SignInMsalExceptionOccurred")] + SignInMsalExceptionOccurred = 400, + + /// + /// An error occurred while importing the VPAX file + /// + [JsonPropertyName("VpaxFileImportError")] + VpaxFileImportError = 500, + + /// + /// An error has occurred while exporting the VPAX file + /// + [JsonPropertyName("VpaxFileExportError")] + VpaxFileExportError = 501, + + /// + /// You are not connected to the Internet + /// + [JsonPropertyName("NetworkError")] + NetworkError = 600, + + /// + /// An exception occurred while exporting data to file + /// + [JsonPropertyName("ExportDataFileError")] + ExportDataFileError = 700, + + /// + /// An exception occurred while executing the DAX template engine + /// + [JsonPropertyName("ManageDateTemplateError")] + ManageDateTemplateError = 800, + + /// + /// An exception occurred while executing the template development APIs + /// + [JsonPropertyName("TemplateDevelopmentError")] + TemplateDevelopmentError = 900, + + /// + /// An error occurred while obfuscating the VPAX file + /// + [JsonPropertyName("VpaxObfuscationError")] + VpaxObfuscationError = 1000, + + /// + /// An error occurred while deobfuscating the VPAX file + /// + [JsonPropertyName("VpaxDeobfuscationError")] + VpaxDeobfuscationError = 1001, } diff --git a/src/Infrastructure/AppInstance.cs b/src/Infrastructure/AppInstance.cs index e6bf0259..73c6f71c 100644 --- a/src/Infrastructure/AppInstance.cs +++ b/src/Infrastructure/AppInstance.cs @@ -1,166 +1,165 @@ -namespace Sqlbi.Bravo.Infrastructure +using System; +using System.Diagnostics; +using System.IO; +using System.IO.Pipes; +using System.Security.Principal; +using System.Text; +using System.Text.Json; +using System.Threading; +using Sqlbi.Bravo.Infrastructure.Configuration.Settings; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Messages; +using Sqlbi.Bravo.Infrastructure.Telemetry; + +namespace Sqlbi.Bravo.Infrastructure; + +internal class AppInstance : IDisposable { - using Sqlbi.Bravo.Infrastructure.Configuration.Settings; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Messages; - using Sqlbi.Bravo.Infrastructure.Telemetry; - using System; - using System.Diagnostics; - using System.IO; - using System.IO.Pipes; - using System.Security.Principal; - using System.Text; - using System.Text.Json; - using System.Threading; - - internal class AppInstance : IDisposable - { - private readonly bool _owned; - private readonly Mutex _mutex; - private readonly string _pipeName; - private readonly string _mutexName; + private readonly bool _owned; + private readonly Mutex _mutex; + private readonly string _pipeName; + private readonly string _mutexName; - private NamedPipeServerStream? _pipeServer; - private bool _disposed; + private NamedPipeServerStream? _pipeServer; + private bool _disposed; - public AppInstance() + public AppInstance() + { + var appId = "8D4D9F1D39F94C7789D84729480D8198"; // Do not change !! + var appName = AppEnvironment.DeploymentMode == AppDeploymentMode.Packaged ? AppEnvironment.ApplicationStoreAliasName : AppEnvironment.ApplicationName; + // Named pipes in packaged applications must use the syntax \\.\pipe\LOCAL\ for the pipe name, however, for non-windows store applications there is no such directive yet. + // See https://learn.microsoft.com/en-gb/windows/win32/api/winbase/nf-winbase-createnamedpipea + var pipeNamePrefix = AppEnvironment.DeploymentMode == AppDeploymentMode.Packaged ? "LOCAL\\" : string.Empty; + + using var identity = WindowsIdentity.GetCurrent(); + BravoUnexpectedException.ThrowIfNull(identity.Owner); // A non-null Owner is expected since GetCurrent() ifImpersonating/threadOnly argument is false + var userSid = identity.Owner.Value; // Should we use TokenLogonSid instead of TokenInformationClass.TokenOwner ? + var sessionId = AppEnvironment.SessionId; + + // 'sessionId' allows to run multiple instance - one per session - on multi-session environments such as Remote Desktop Services + // 'userSid' allows to run multiple instance under different user accounts (non-elevated) + _pipeName = $"{pipeNamePrefix}{appName}.{appId}.{sessionId}.{userSid}"; + _mutexName = $"{appName}.{appId}.{userSid}"; + _mutex = new Mutex(initiallyOwned: true, name: _mutexName, createdNew: out _owned); + + if (_owned) { - var appId = "8D4D9F1D39F94C7789D84729480D8198"; // Do not change !! - var appName = AppEnvironment.DeploymentMode == AppDeploymentMode.Packaged ? AppEnvironment.ApplicationStoreAliasName : AppEnvironment.ApplicationName; - // Named pipes in packaged applications must use the syntax \\.\pipe\LOCAL\ for the pipe name, however, for non-windows store applications there is no such directive yet. - // See https://learn.microsoft.com/en-gb/windows/win32/api/winbase/nf-winbase-createnamedpipea - var pipeNamePrefix = AppEnvironment.DeploymentMode == AppDeploymentMode.Packaged ? "LOCAL\\" : string.Empty; - - using var identity = WindowsIdentity.GetCurrent(); - BravoUnexpectedException.ThrowIfNull(identity.Owner); // A non-null Owner is expected since GetCurrent() ifImpersonating/threadOnly argument is false - var userSid = identity.Owner.Value; // Should we use TokenLogonSid instead of TokenInformationClass.TokenOwner ? - var sessionId = AppEnvironment.SessionId; - - // 'sessionId' allows to run multiple instance - one per session - on multi-session environments such as Remote Desktop Services - // 'userSid' allows to run multiple instance under different user accounts (non-elevated) - _pipeName = $"{pipeNamePrefix}{appName}.{appId}.{sessionId}.{userSid}"; - _mutexName = $"{appName}.{appId}.{userSid}"; - _mutex = new Mutex(initiallyOwned: true, name: _mutexName, createdNew: out _owned); - - if (_owned) - { - StartPipeServer(); - GC.KeepAlive(_mutex); - } + StartPipeServer(); + GC.KeepAlive(_mutex); } + } - /// - /// Determines if the current instance is the only running instance of the application or if another instance is already running - /// - /// true if the current instance is the only running instance of the application; otherwise, false - public bool IsOwned => _owned; - - /// - /// Occurs when a new (secondary) instance of the application is started and the notification is sent to the primary (owner) instance - /// - public event EventHandler? OnNewInstance; - - /// - /// Sends a message to the primary instance owner notifying it of startup arguments for the current instance - /// - public void NotifyOwner() + /// + /// Determines if the current instance is the only running instance of the application or if another instance is already running + /// + /// true if the current instance is the only running instance of the application; otherwise, false + public bool IsOwned => _owned; + + /// + /// Occurs when a new (secondary) instance of the application is started and the notification is sent to the primary (owner) instance + /// + public event EventHandler? OnNewInstance; + + /// + /// Sends a message to the primary instance owner notifying it of startup arguments for the current instance + /// + public void NotifyOwner() + { + using var pipeClient = new NamedPipeClientStream(serverName: ".", _pipeName, PipeDirection.Out); + try { - using var pipeClient = new NamedPipeClientStream(serverName: ".", _pipeName, PipeDirection.Out); - try - { - pipeClient.Connect(timeout: 5_000); - } - catch (Exception ex) when (ex is IOException || ex is TimeoutException) - { - ExceptionHelper.WriteToEventLog(ex, EventLogEntryType.Warning); - TelemetryService.Instance.TrackException(ex); - return; - } + pipeClient.Connect(timeout: 5_000); + } + catch (Exception ex) when (ex is IOException || ex is TimeoutException) + { + ExceptionHelper.WriteToEventLog(ex, EventLogEntryType.Warning); + TelemetryService.Instance.TrackException(ex); + return; + } - var startupSettings = StartupSettings.CreateFromCommandLineArguments(); - var startupMessage = AppInstanceStartupMessage.CreateFrom(startupSettings); - var json = JsonSerializer.Serialize(startupMessage); - var bytes = Encoding.Unicode.GetBytes(json).AsSpan(); + var startupSettings = StartupSettings.CreateFromCommandLineArguments(); + var startupMessage = AppInstanceStartupMessage.CreateFrom(startupSettings); + var json = JsonSerializer.Serialize(startupMessage); + var bytes = Encoding.Unicode.GetBytes(json).AsSpan(); - try - { - pipeClient.Write(bytes); - pipeClient.Flush(); - } - catch (Exception ex) when (ex is ObjectDisposedException || ex is InvalidOperationException || ex is IOException) - { - ExceptionHelper.WriteToEventLog(ex, EventLogEntryType.Warning); - TelemetryService.Instance.TrackException(ex); - return; - } + try + { + pipeClient.Write(bytes); + pipeClient.Flush(); } - - private void StartPipeServer() + catch (Exception ex) when (ex is ObjectDisposedException || ex is InvalidOperationException || ex is IOException) { - _pipeServer?.Dispose(); - _pipeServer = new NamedPipeServerStream(_pipeName, PipeDirection.In, 1, PipeTransmissionMode.Byte, PipeOptions.CurrentUserOnly); - _pipeServer.BeginWaitForConnection(OnPipeConnection, state: _pipeServer); + ExceptionHelper.WriteToEventLog(ex, EventLogEntryType.Warning); + TelemetryService.Instance.TrackException(ex); + return; } + } - private void OnPipeConnection(IAsyncResult asyncResult) - { - BravoUnexpectedException.ThrowIfNull(asyncResult.AsyncState); + private void StartPipeServer() + { + _pipeServer?.Dispose(); + _pipeServer = new NamedPipeServerStream(_pipeName, PipeDirection.In, 1, PipeTransmissionMode.Byte, PipeOptions.CurrentUserOnly); + _pipeServer.BeginWaitForConnection(OnPipeConnection, state: _pipeServer); + } - var pipeServer = (NamedPipeServerStream)asyncResult.AsyncState; - pipeServer.EndWaitForConnection(asyncResult); + private void OnPipeConnection(IAsyncResult asyncResult) + { + BravoUnexpectedException.ThrowIfNull(asyncResult.AsyncState); - using var reader = new StreamReader(pipeServer, Encoding.Unicode); - var json = reader.ReadToEnd(); + var pipeServer = (NamedPipeServerStream)asyncResult.AsyncState; + pipeServer.EndWaitForConnection(asyncResult); - var startupMessage = default(AppInstanceStartupMessage?); - try - { - startupMessage = JsonSerializer.Deserialize(json); - } - catch (JsonException) - { - // TODO: log JsonException ? - } + using var reader = new StreamReader(pipeServer, Encoding.Unicode); + var json = reader.ReadToEnd(); - OnNewInstance?.Invoke(this, new AppInstanceStartupEventArgs(startupMessage)); - StartPipeServer(); + var startupMessage = default(AppInstanceStartupMessage?); + try + { + startupMessage = JsonSerializer.Deserialize(json); + } + catch (JsonException) + { + // TODO: log JsonException ? } - #region IDisposable + OnNewInstance?.Invoke(this, new AppInstanceStartupEventArgs(startupMessage)); + StartPipeServer(); + } + + #region IDisposable - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + if (!_disposed) { - if (!_disposed) + if (disposing) { - if (disposing) - { - if (_owned) - _mutex.ReleaseMutex(); + if (_owned) + _mutex.ReleaseMutex(); - _pipeServer?.Dispose(); - _mutex.Dispose(); - } - - _disposed = true; + _pipeServer?.Dispose(); + _mutex.Dispose(); } - } - public void Dispose() - { - Dispose(disposing: true); - GC.SuppressFinalize(this); + _disposed = true; } - - #endregion } - internal class AppInstanceStartupEventArgs : EventArgs + public void Dispose() { - public AppInstanceStartupEventArgs(AppInstanceStartupMessage? message) - { - Message = message; - } + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + #endregion +} - public AppInstanceStartupMessage? Message { get; } +internal class AppInstanceStartupEventArgs : EventArgs +{ + public AppInstanceStartupEventArgs(AppInstanceStartupMessage? message) + { + Message = message; } + + public AppInstanceStartupMessage? Message { get; } } diff --git a/src/Infrastructure/AppVersionInfo.cs b/src/Infrastructure/AppVersionInfo.cs index e17ca6ff..0ad29709 100644 --- a/src/Infrastructure/AppVersionInfo.cs +++ b/src/Infrastructure/AppVersionInfo.cs @@ -1,36 +1,35 @@ -namespace Sqlbi.Bravo.Infrastructure +namespace Sqlbi.Bravo.Infrastructure; + +/// +/// Exposes the application version, stamped at build time by Nerdbank.GitVersioning from version.json. +/// +internal sealed class AppVersionInfo { - /// - /// Exposes the application version, stamped at build time by Nerdbank.GitVersioning from version.json. - /// - internal sealed class AppVersionInfo + public AppVersionInfo() { - public AppVersionInfo() - { - var build = ThisAssembly.AssemblyFileVersion; + var build = ThisAssembly.AssemblyFileVersion; - Build = build; - Version = System.Version.Parse(build).ToString(3); - InformationalVersion = ThisAssembly.AssemblyInformationalVersion; - } + Build = build; + Version = System.Version.Parse(build).ToString(3); + InformationalVersion = ThisAssembly.AssemblyInformationalVersion; + } - /// - /// Gets the full four-part assembly file version Major.Minor.Patch.Height, where the fourth field is the - /// git-height build counter. Intended for diagnostics only (e.g. telemetry) - never used for update comparisons - /// or shown to users; use instead. - /// - public string Build { get; } + /// + /// Gets the full four-part assembly file version Major.Minor.Patch.Height, where the fourth field is the + /// git-height build counter. Intended for diagnostics only (e.g. telemetry) - never used for update comparisons + /// or shown to users; use instead. + /// + public string Build { get; } - /// - /// Gets the three-part Semantic Version Major.Minor.Patch. This is the canonical application version: - /// shown to users and used to compare versions when checking for updates. - /// - public string Version { get; } + /// + /// Gets the three-part Semantic Version Major.Minor.Patch. This is the canonical application version: + /// shown to users and used to compare versions when checking for updates. + /// + public string Version { get; } - /// - /// Gets the informational version: the version with build metadata (the git commit id) appended, - /// e.g. 1.2.3.45+0a1b2c3d4e. Intended for diagnostics. - /// - public string InformationalVersion { get; } - } + /// + /// Gets the informational version: the version with build metadata (the git commit id) appended, + /// e.g. 1.2.3.45+0a1b2c3d4e. Intended for diagnostics. + /// + public string InformationalVersion { get; } } diff --git a/src/Infrastructure/AppWindow.Designer.cs b/src/Infrastructure/AppWindow.Designer.cs index 4eb86790..be397138 100644 --- a/src/Infrastructure/AppWindow.Designer.cs +++ b/src/Infrastructure/AppWindow.Designer.cs @@ -1,4 +1,5 @@ namespace Sqlbi.Bravo.Infrastructure + { partial class AppWindow { @@ -62,4 +63,4 @@ private void InitializeComponent() private Microsoft.Web.WebView2.WinForms.WebView2 webView; } -} \ No newline at end of file +} diff --git a/src/Infrastructure/AppWindow.cs b/src/Infrastructure/AppWindow.cs index f605e683..e311404c 100644 --- a/src/Infrastructure/AppWindow.cs +++ b/src/Infrastructure/AppWindow.cs @@ -1,26 +1,33 @@ -namespace Sqlbi.Bravo.Infrastructure +using System; +using System.Diagnostics; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Windows.Forms; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Web.WebView2.Core; +using Microsoft.Web.WebView2.WinForms; +using Sqlbi.Bravo.Infrastructure.Configuration; +using Sqlbi.Bravo.Infrastructure.Configuration.Settings; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Messages; +using Sqlbi.Bravo.Infrastructure.Policies; +using Sqlbi.Bravo.Infrastructure.Services; +using Sqlbi.Bravo.Infrastructure.Telemetry; +using Sqlbi.Bravo.Infrastructure.Windows.Interop; +using Sqlbi.Bravo.Models; + +namespace Sqlbi.Bravo.Infrastructure; + +internal partial class AppWindow : Form { - using Microsoft.AspNetCore.Http; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Options; - using Microsoft.Web.WebView2.Core; - using Microsoft.Web.WebView2.WinForms; - using Sqlbi.Bravo.Infrastructure.Configuration; - using Sqlbi.Bravo.Infrastructure.Configuration.Settings; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Messages; - using Sqlbi.Bravo.Infrastructure.Policies; - using Sqlbi.Bravo.Infrastructure.Services; - using Sqlbi.Bravo.Infrastructure.Telemetry; - using Sqlbi.Bravo.Infrastructure.Windows.Interop; - using Sqlbi.Bravo.Models; - using System.Drawing; - using System.Windows.Forms; - - internal partial class AppWindow : Form - { - private const string WindowExternalWebMessageCallbackScript = @" + private const string WindowExternalWebMessageCallbackScript = @" window.external = { sendMessage: function(message) { window.chrome.webview.postMessage(message); @@ -31,325 +38,324 @@ internal partial class AppWindow : Form }); } };"; - public static SynchronizationContext? UISynchronizationContext { get; set; } + public static SynchronizationContext? UISynchronizationContext { get; set; } - private readonly AppInstance _instance; - private readonly IServerAddressProvider _serverAddressProvider; - private readonly IOptions _startupSettingsOptionsAccessor; - private readonly WebView2ProxyAuthHandler _proxyAuthHandler; - private readonly Color _startupThemeColor; - private readonly IPolicies _policies; + private readonly AppInstance _instance; + private readonly IServerAddressProvider _serverAddressProvider; + private readonly IOptions _startupSettingsOptionsAccessor; + private readonly WebView2ProxyAuthHandler _proxyAuthHandler; + private readonly Color _startupThemeColor; + private readonly IPolicies _policies; - public AppWindow(IServiceProvider services, AppInstance instance) - { - _instance = instance; - _serverAddressProvider = services.GetRequiredService(); - _startupSettingsOptionsAccessor = services.GetRequiredService>(); - _policies = services.GetRequiredService(); - _proxyAuthHandler = new WebView2ProxyAuthHandler(WebProxyWrapper.Current); - _startupThemeColor = ThemeHelper.ShouldUseDarkMode(UserPreferences.Current.Theme) ? AppEnvironment.ThemeColorDark : AppEnvironment.ThemeColorLight; - - UISynchronizationContext = new WindowsFormsSynchronizationContext(); - InitializeComponent(); - InitializeWebViewAsync(); - - // How does the window manager decide where to place a newly-created window ? https://devblogs.microsoft.com/oldnewthing/20121126-00/?p=5993 - StartPosition = FormStartPosition.WindowsDefaultBounds; - } + public AppWindow(IServiceProvider services, AppInstance instance) + { + _instance = instance; + _serverAddressProvider = services.GetRequiredService(); + _startupSettingsOptionsAccessor = services.GetRequiredService>(); + _policies = services.GetRequiredService(); + _proxyAuthHandler = new WebView2ProxyAuthHandler(WebProxyWrapper.Current); + _startupThemeColor = ThemeHelper.ShouldUseDarkMode(UserPreferences.Current.Theme) ? AppEnvironment.ThemeColorDark : AppEnvironment.ThemeColorLight; + + UISynchronizationContext = new WindowsFormsSynchronizationContext(); + InitializeComponent(); + InitializeWebViewAsync(); + + // How does the window manager decide where to place a newly-created window ? https://devblogs.microsoft.com/oldnewthing/20121126-00/?p=5993 + StartPosition = FormStartPosition.WindowsDefaultBounds; + } - private WebView2 WebView => webView; + private WebView2 WebView => webView; - private async void InitializeWebViewAsync() + private async void InitializeWebViewAsync() + { + // + // Feature-detecting to test whether the installed Runtime supports recently added APIs + // https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/versioning#feature-detecting-to-test-whether-the-installed-runtime-supports-recently-added-apis + // + // ICoreWebView2_2 - SDK >= 1.0.705.50 - Runtime >= 86.0.616.0 + // ICoreWebView2_10 - SDK >= 1.0.1150.38 - Runtime >= 99.0.1150.38 + // ICoreWebView2Settings3 - SDK >= 1.0.864.35 - Runtime >= 91.0.864.35 + // ICoreWebView2Settings4 - SDK >= 1.0.902.49 - Runtime >= 92.0.902.49 + // ICoreWebView2Settings5 - SDK >= 1.0.902.49 - Runtime >= 92.0.902.49 + // ICoreWebView2Settings6 - SDK >= 1.0.992.28 - Runtime >= 94.0.992.31 + // ICoreWebView2Controller2 - SDK >= 1.0.774.44 - Runtime >= 89.0.774.44 + // + // How to test a specific runtime version: + // + // - winget (not all versions are available) + // winget show --id=Microsoft.EdgeWebView2Runtime --versions + // winget install --id=Microsoft.EdgeWebView2Runtime --version 95.0.1020.53 --architecture x64 + // - Download installer from https://www.catalog.update.microsoft.com/Search.aspx?q=WebView2 + // uncompress microsoftedgestandaloneinstallerx64_.exe + // uncompress MicrosoftEdge_X64_.exe.{} + // create webview environment and pass the folder path in then 'browserExecutableFolder' arguments => .\microsoftedgestandaloneinstallerx64_\MicrosoftEdge_X64_\MSEDGE\Chrome-bin\ + + WebView.Visible = false; + + var options = new CoreWebView2EnvironmentOptions(additionalBrowserArguments: null, language: null, targetCompatibleBrowserVersion: null, allowSingleSignOnUsingOSPrimaryAccount: false); { - // - // Feature-detecting to test whether the installed Runtime supports recently added APIs - // https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/versioning#feature-detecting-to-test-whether-the-installed-runtime-supports-recently-added-apis - // - // ICoreWebView2_2 - SDK >= 1.0.705.50 - Runtime >= 86.0.616.0 - // ICoreWebView2_10 - SDK >= 1.0.1150.38 - Runtime >= 99.0.1150.38 - // ICoreWebView2Settings3 - SDK >= 1.0.864.35 - Runtime >= 91.0.864.35 - // ICoreWebView2Settings4 - SDK >= 1.0.902.49 - Runtime >= 92.0.902.49 - // ICoreWebView2Settings5 - SDK >= 1.0.902.49 - Runtime >= 92.0.902.49 - // ICoreWebView2Settings6 - SDK >= 1.0.992.28 - Runtime >= 94.0.992.31 - // ICoreWebView2Controller2 - SDK >= 1.0.774.44 - Runtime >= 89.0.774.44 - // - // How to test a specific runtime version: - // - // - winget (not all versions are available) - // winget show --id=Microsoft.EdgeWebView2Runtime --versions - // winget install --id=Microsoft.EdgeWebView2Runtime --version 95.0.1020.53 --architecture x64 - // - Download installer from https://www.catalog.update.microsoft.com/Search.aspx?q=WebView2 - // uncompress microsoftedgestandaloneinstallerx64_.exe - // uncompress MicrosoftEdge_X64_.exe.{} - // create webview environment and pass the folder path in then 'browserExecutableFolder' arguments => .\microsoftedgestandaloneinstallerx64_\MicrosoftEdge_X64_\MSEDGE\Chrome-bin\ - - WebView.Visible = false; - - var options = new CoreWebView2EnvironmentOptions(additionalBrowserArguments: null, language: null, targetCompatibleBrowserVersion: null, allowSingleSignOnUsingOSPrimaryAccount: false); - { - /* ICoreWebView2EnvironmentOptions */ options.AdditionalBrowserArguments = WebView2Helper.GetProxyArguments(UserPreferences.Current.Proxy, WebProxyWrapper.Current.DefaultSystemProxy); - - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Text, name: $"{ nameof(AppWindow) }.{ nameof(InitializeWebViewAsync) }.{ nameof(options.AdditionalBrowserArguments) }", content: options.AdditionalBrowserArguments); - } - var environment = await CoreWebView2Environment.CreateAsync(browserExecutableFolder: null, userDataFolder: AppEnvironment.ApplicationTempPath, options); - { - //environment.BrowserProcessExited - } - await WebView.EnsureCoreWebView2Async(environment); + /* ICoreWebView2EnvironmentOptions */ options.AdditionalBrowserArguments = WebView2Helper.GetProxyArguments(UserPreferences.Current.Proxy, WebProxyWrapper.Current.DefaultSystemProxy); + + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Text, name: $"{nameof(AppWindow)}.{nameof(InitializeWebViewAsync)}.{nameof(options.AdditionalBrowserArguments)}", content: options.AdditionalBrowserArguments); + } + var environment = await CoreWebView2Environment.CreateAsync(browserExecutableFolder: null, userDataFolder: AppEnvironment.ApplicationTempPath, options); + { + //environment.BrowserProcessExited + } + await WebView.EnsureCoreWebView2Async(environment); #if DEBUG - var isDebug = true; + var isDebug = true; #else - var isDebug = false; + var isDebug = false; #endif - /* ICoreWebView2Controller2 */ WebView2Helper.TryAndIgnoreUnsupportedError(() => WebView.DefaultBackgroundColor = _startupThemeColor); - /* ICoreWebView2Controller4 */ // WebView2Helper.TryAndIgnoreUnsupportedError(() => WebView.AllowExternalDrop = true); // Commented out because the default value is true - /* ICoreWebView2Settings3 */ WebView2Helper.TryAndIgnoreUnsupportedError(() => WebView.CoreWebView2.Settings.AreBrowserAcceleratorKeysEnabled = isDebug); - /* ICoreWebView2Settings */ WebView.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = isDebug; - /* ICoreWebView2Settings */ WebView.CoreWebView2.Settings.AreDefaultContextMenusEnabled = isDebug; - /* ICoreWebView2Settings */ WebView.CoreWebView2.Settings.AreDevToolsEnabled = isDebug; - /* ICoreWebView2Settings */ WebView.CoreWebView2.Settings.AreHostObjectsAllowed = false; - /* ICoreWebView2Settings */ WebView.CoreWebView2.Settings.IsWebMessageEnabled = true; - /* ICoreWebView2Settings */ WebView.CoreWebView2.Settings.IsScriptEnabled = true; - /* ICoreWebView2Settings */ WebView.CoreWebView2.Settings.IsStatusBarEnabled = false; - /* ICoreWebView2Settings4 */ WebView2Helper.TryAndIgnoreUnsupportedError(() => WebView.CoreWebView2.Settings.IsPasswordAutosaveEnabled = false); - /* ICoreWebView2Settings4 */ WebView2Helper.TryAndIgnoreUnsupportedError(() => WebView.CoreWebView2.Settings.IsGeneralAutofillEnabled = false); - /* ICoreWebView2Settings5 */ WebView2Helper.TryAndIgnoreUnsupportedError(() => WebView.CoreWebView2.Settings.IsPinchZoomEnabled = false); - /* ICoreWebView2Settings6 */ WebView2Helper.TryAndIgnoreUnsupportedError(() => WebView.CoreWebView2.Settings.IsSwipeNavigationEnabled = false); - - //WebView.CoreWebView2.OpenDevToolsWindow(); - //WebView.CoreWebView2.OpenTaskManagerWindow(); - //WebView.CoreWebView2.NavigationStarting += OnWebViewNavigationStarting; - //WebView.CoreWebView2.NavigationCompleted += OnWebViewNavigationCompleted; - //WebView.CoreWebView2.ContentLoading += OnWebViewContentLoading; - //WebView.CoreWebView2.WebMessageReceived += OnWebViewWebWebMessageReceived; - //WebView.CoreWebView2.WebResourceResponseReceived += OnWebViewWebResourceResponseReceived; - - /* ICoreWebView2_2 */ WebView.CoreWebView2.DOMContentLoaded += OnWebViewDOMContentLoaded; - /* ICoreWebView2 */ WebView.CoreWebView2.WebResourceRequested += OnWebViewWebResourceRequested; - /* ICoreWebView2 */ WebView.CoreWebView2.PermissionRequested += OnWebViewPermissionRequested; - /* ICoreWebView2_10 */ WebView2Helper.TryAndIgnoreUnsupportedError(() => WebView.CoreWebView2.BasicAuthenticationRequested += OnWebViewBasicAuthenticationRequested); - - /* ICoreWebView2 */ await WebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(WindowExternalWebMessageCallbackScript); - /* ICoreWebView2 */ WebView.CoreWebView2.AddWebResourceRequestedFilter("*", CoreWebView2WebResourceContext.All); - // Use a virtual host name to serve local content over HTTPS, avoiding 'file:' URL security origin restrictions. - // The '.example' TLD is reserved by RFC 6761 and guaranteed never to be registered, preventing domain collisions. - /* ICoreWebView2_3 */ WebView.CoreWebView2.SetVirtualHostNameToFolderMapping("bravo.example", "wwwroot", CoreWebView2HostResourceAccessKind.Allow); - /* ICoreWebView2 */ WebView.CoreWebView2.Navigate("https://bravo.example/index.html"); - - // TODO: Consider allowing users to open DevTools for troubleshooting (e.g. by pressing F12 or via a context menu) - } + /* ICoreWebView2Controller2 */ WebView2Helper.TryAndIgnoreUnsupportedError(() => WebView.DefaultBackgroundColor = _startupThemeColor); + /* ICoreWebView2Controller4 */ // WebView2Helper.TryAndIgnoreUnsupportedError(() => WebView.AllowExternalDrop = true); // Commented out because the default value is true + /* ICoreWebView2Settings3 */ WebView2Helper.TryAndIgnoreUnsupportedError(() => WebView.CoreWebView2.Settings.AreBrowserAcceleratorKeysEnabled = isDebug); + /* ICoreWebView2Settings */ WebView.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = isDebug; + /* ICoreWebView2Settings */ WebView.CoreWebView2.Settings.AreDefaultContextMenusEnabled = isDebug; + /* ICoreWebView2Settings */ WebView.CoreWebView2.Settings.AreDevToolsEnabled = isDebug; + /* ICoreWebView2Settings */ WebView.CoreWebView2.Settings.AreHostObjectsAllowed = false; + /* ICoreWebView2Settings */ WebView.CoreWebView2.Settings.IsWebMessageEnabled = true; + /* ICoreWebView2Settings */ WebView.CoreWebView2.Settings.IsScriptEnabled = true; + /* ICoreWebView2Settings */ WebView.CoreWebView2.Settings.IsStatusBarEnabled = false; + /* ICoreWebView2Settings4 */ WebView2Helper.TryAndIgnoreUnsupportedError(() => WebView.CoreWebView2.Settings.IsPasswordAutosaveEnabled = false); + /* ICoreWebView2Settings4 */ WebView2Helper.TryAndIgnoreUnsupportedError(() => WebView.CoreWebView2.Settings.IsGeneralAutofillEnabled = false); + /* ICoreWebView2Settings5 */ WebView2Helper.TryAndIgnoreUnsupportedError(() => WebView.CoreWebView2.Settings.IsPinchZoomEnabled = false); + /* ICoreWebView2Settings6 */ WebView2Helper.TryAndIgnoreUnsupportedError(() => WebView.CoreWebView2.Settings.IsSwipeNavigationEnabled = false); + + //WebView.CoreWebView2.OpenDevToolsWindow(); + //WebView.CoreWebView2.OpenTaskManagerWindow(); + //WebView.CoreWebView2.NavigationStarting += OnWebViewNavigationStarting; + //WebView.CoreWebView2.NavigationCompleted += OnWebViewNavigationCompleted; + //WebView.CoreWebView2.ContentLoading += OnWebViewContentLoading; + //WebView.CoreWebView2.WebMessageReceived += OnWebViewWebWebMessageReceived; + //WebView.CoreWebView2.WebResourceResponseReceived += OnWebViewWebResourceResponseReceived; + + /* ICoreWebView2_2 */ WebView.CoreWebView2.DOMContentLoaded += OnWebViewDOMContentLoaded; + /* ICoreWebView2 */ WebView.CoreWebView2.WebResourceRequested += OnWebViewWebResourceRequested; + /* ICoreWebView2 */ WebView.CoreWebView2.PermissionRequested += OnWebViewPermissionRequested; + /* ICoreWebView2_10 */ WebView2Helper.TryAndIgnoreUnsupportedError(() => WebView.CoreWebView2.BasicAuthenticationRequested += OnWebViewBasicAuthenticationRequested); + + /* ICoreWebView2 */ await WebView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(WindowExternalWebMessageCallbackScript); + /* ICoreWebView2 */ WebView.CoreWebView2.AddWebResourceRequestedFilter("*", CoreWebView2WebResourceContext.All); + // Use a virtual host name to serve local content over HTTPS, avoiding 'file:' URL security origin restrictions. + // The '.example' TLD is reserved by RFC 6761 and guaranteed never to be registered, preventing domain collisions. + /* ICoreWebView2_3 */ WebView.CoreWebView2.SetVirtualHostNameToFolderMapping("bravo.example", "wwwroot", CoreWebView2HostResourceAccessKind.Allow); + /* ICoreWebView2 */ WebView.CoreWebView2.Navigate("https://bravo.example/index.html"); + + // TODO: Consider allowing users to open DevTools for troubleshooting (e.g. by pressing F12 or via a context menu) + } - protected override void WndProc(ref Message message) + protected override void WndProc(ref Message message) + { + switch (message.Msg) { - switch (message.Msg) - { - // Form.StyleChanged event detects all theme change except for Aero color changes - // The Aero color change triggers the WM_DWMCOLORIZATIONCOLORCHANGED message - case (int)WindowMessage.WM_DWMCOLORIZATIONCOLORCHANGED: - case (int)WindowMessage.WM_DWMCOMPOSITIONCHANGED: - case (int)WindowMessage.WM_THEMECHANGED: - if (UserPreferences.Current.Theme == ThemeType.Auto) - { - ThemeHelper.ChangeTheme(message.HWnd, ThemeType.Auto); - } - break; - } - - base.WndProc(ref message); + // Form.StyleChanged event detects all theme change except for Aero color changes + // The Aero color change triggers the WM_DWMCOLORIZATIONCOLORCHANGED message + case (int)WindowMessage.WM_DWMCOLORIZATIONCOLORCHANGED: + case (int)WindowMessage.WM_DWMCOMPOSITIONCHANGED: + case (int)WindowMessage.WM_THEMECHANGED: + if (UserPreferences.Current.Theme == ThemeType.Auto) + { + ThemeHelper.ChangeTheme(message.HWnd, ThemeType.Auto); + } + break; } - private void OnFormLoad(object? sender, EventArgs e) - { - ThemeHelper.InitializeTheme(Handle, UserPreferences.Current.Theme); + base.WndProc(ref message); + } - var titleVersionHidden = Microsoft.Win32.Registry.CurrentUser.GetBoolValue(subkeyName: AppEnvironment.ApplicationRegistryKeyName, valueName: AppEnvironment.ApplicationRegistryApplicationTitleVersionHiddenValue); + private void OnFormLoad(object? sender, EventArgs e) + { + ThemeHelper.InitializeTheme(Handle, UserPreferences.Current.Theme); - Text = titleVersionHidden ? AppEnvironment.ApplicationMainWindowTitle : AppEnvironment.ApplicationMainWindowTitle.AppendApplicationVersion(); - BackgroundImageLayout = ImageLayout.Center; - BackColor = _startupThemeColor; + var titleVersionHidden = Microsoft.Win32.Registry.CurrentUser.GetBoolValue(subkeyName: AppEnvironment.ApplicationRegistryKeyName, valueName: AppEnvironment.ApplicationRegistryApplicationTitleVersionHiddenValue); - CenterToScreen(); + Text = titleVersionHidden ? AppEnvironment.ApplicationMainWindowTitle : AppEnvironment.ApplicationMainWindowTitle.AppendApplicationVersion(); + BackgroundImageLayout = ImageLayout.Center; + BackColor = _startupThemeColor; - _instance.OnNewInstance += OnNewInstanceRestoreFormWindowToForeground; - } + CenterToScreen(); - private void OnFormClosed(object? sender, FormClosedEventArgs e) - { - _instance.OnNewInstance -= OnNewInstanceRestoreFormWindowToForeground; - _instance.OnNewInstance -= OnNewInstanceSendStartupWebMessage; - } - - private void OnWebViewDOMContentLoaded(object? sender, CoreWebView2DOMContentLoadedEventArgs e) - { - WebViewLog(message: $"::OnWebViewDOMContentLoaded({ e.NavigationId })"); + _instance.OnNewInstance += OnNewInstanceRestoreFormWindowToForeground; + } - if (WebView.Visible == false) - { - WebView.Visible = true; - BackgroundImage = null; - SendAppStartupWebMessage(); + private void OnFormClosed(object? sender, FormClosedEventArgs e) + { + _instance.OnNewInstance -= OnNewInstanceRestoreFormWindowToForeground; + _instance.OnNewInstance -= OnNewInstanceSendStartupWebMessage; + } - _instance.OnNewInstance += OnNewInstanceSendStartupWebMessage; - } - } + private void OnWebViewDOMContentLoaded(object? sender, CoreWebView2DOMContentLoadedEventArgs e) + { + WebViewLog(message: $"::OnWebViewDOMContentLoaded({e.NavigationId})"); - private void OnWebViewPermissionRequested(object? sender, CoreWebView2PermissionRequestedEventArgs e) + if (WebView.Visible == false) { - WebViewLog(message: $"::OnWebViewPermissionRequested({ e.PermissionKind }|{ e.State })"); + WebView.Visible = true; + BackgroundImage = null; + SendAppStartupWebMessage(); - if (e.PermissionKind == CoreWebView2PermissionKind.ClipboardRead) - e.State = CoreWebView2PermissionState.Allow; - } - - private void OnWebViewBasicAuthenticationRequested(object? sender, CoreWebView2BasicAuthenticationRequestedEventArgs e) - { - if (_proxyAuthHandler.TryHandle(e)) - return; - - // Proxy authentication not handled (untrusted proxy or credentials unavailable). - // Do not cancel the request — allow WebView's native auth dialog so the user can provide or deny credentials. - e.Cancel = false; - - //var deferral = e.GetDeferral(); - - //SynchronizationContext.Current?.Post((_) => - //{ - // using (deferral) - // { - // var credentialOptions = new CredentialDialogOptions(caption: "Authentication request", message: $"Authentication request from { e.Uri }\r\nChallenge: { e.Challenge }") - // { - // HwndParent = Handle - // }; - - // var credential = CredentialDialog.PromptForCredentials(credentialOptions); - // if (credential is not null) - // { - // e.Response.UserName = credential.UserName; - // e.Response.Password = credential.Password; - // } - // else - // { - // e.Cancel = true; - // } - // } - //}, state: null); + _instance.OnNewInstance += OnNewInstanceSendStartupWebMessage; } + } - private void OnWebViewNavigationStarting(object? sender, CoreWebView2NavigationStartingEventArgs e) - { - WebViewLog(message: $"::OnWebViewNavigationStarting({ e.NavigationId }|{ e.Uri })"); - } + private void OnWebViewPermissionRequested(object? sender, CoreWebView2PermissionRequestedEventArgs e) + { + WebViewLog(message: $"::OnWebViewPermissionRequested({e.PermissionKind}|{e.State})"); - private void OnWebViewNavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e) - { - WebViewLog(message: $"::OnWebViewNavigationCompleted({ e.NavigationId }|{ e.IsSuccess }|{ e.WebErrorStatus })"); - } + if (e.PermissionKind == CoreWebView2PermissionKind.ClipboardRead) + e.State = CoreWebView2PermissionState.Allow; + } - private void OnWebViewContentLoading(object? sender, CoreWebView2ContentLoadingEventArgs e) - { - WebViewLog(message: $"::OnWebViewContentLoading({ e.NavigationId }|{ e.IsErrorPage })"); - } + private void OnWebViewBasicAuthenticationRequested(object? sender, CoreWebView2BasicAuthenticationRequestedEventArgs e) + { + if (_proxyAuthHandler.TryHandle(e)) + return; + + // Proxy authentication not handled (untrusted proxy or credentials unavailable). + // Do not cancel the request — allow WebView's native auth dialog so the user can provide or deny credentials. + e.Cancel = false; + + //var deferral = e.GetDeferral(); + + //SynchronizationContext.Current?.Post((_) => + //{ + // using (deferral) + // { + // var credentialOptions = new CredentialDialogOptions(caption: "Authentication request", message: $"Authentication request from { e.Uri }\r\nChallenge: { e.Challenge }") + // { + // HwndParent = Handle + // }; + + // var credential = CredentialDialog.PromptForCredentials(credentialOptions); + // if (credential is not null) + // { + // e.Response.UserName = credential.UserName; + // e.Response.Password = credential.Password; + // } + // else + // { + // e.Cancel = true; + // } + // } + //}, state: null); + } - private void OnWebViewWebResourceRequested(object? sender, CoreWebView2WebResourceRequestedEventArgs e) - { - //WebViewLog(message: $"::OnWebViewWebResourceRequested({ e.ResourceContext }|{ e.Request.Uri })"); + private void OnWebViewNavigationStarting(object? sender, CoreWebView2NavigationStartingEventArgs e) + { + WebViewLog(message: $"::OnWebViewNavigationStarting({e.NavigationId}|{e.Uri})"); + } - if (e.ResourceContext == CoreWebView2WebResourceContext.Script && e.Request.Uri.EqualsI("app://config.js")) - { - var content = GetConfigJs(); - e.Response = WebView.CoreWebView2.Environment.CreateWebResourceResponse(content, StatusCodes.Status200OK, "OK", "Content-Type: text/javascript"); - } - } + private void OnWebViewNavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e) + { + WebViewLog(message: $"::OnWebViewNavigationCompleted({e.NavigationId}|{e.IsSuccess}|{e.WebErrorStatus})"); + } - private void OnWebViewWebWebMessageReceived(object? sender, CoreWebView2WebMessageReceivedEventArgs e) - { - var messageString = e.TryGetWebMessageAsString(); + private void OnWebViewContentLoading(object? sender, CoreWebView2ContentLoadingEventArgs e) + { + WebViewLog(message: $"::OnWebViewContentLoading({e.NavigationId}|{e.IsErrorPage})"); + } - WebViewLog(message: $"::OnWebViewWebWebMessageReceived({ e.Source }|{ messageString })"); - } + private void OnWebViewWebResourceRequested(object? sender, CoreWebView2WebResourceRequestedEventArgs e) + { + //WebViewLog(message: $"::OnWebViewWebResourceRequested({ e.ResourceContext }|{ e.Request.Uri })"); - private void OnWebViewWebResourceResponseReceived(object? sender, CoreWebView2WebResourceResponseReceivedEventArgs e) + if (e.ResourceContext == CoreWebView2WebResourceContext.Script && e.Request.Uri.EqualsI("app://config.js")) { - WebViewLog(message: $"::OnWebViewWebResourceResponseReceived({ e.Response.StatusCode }{ e.Response.ReasonPhrase }|{ e.Request.Uri })"); + var content = GetConfigJs(); + e.Response = WebView.CoreWebView2.Environment.CreateWebResourceResponse(content, StatusCodes.Status200OK, "OK", "Content-Type: text/javascript"); } + } + + private void OnWebViewWebWebMessageReceived(object? sender, CoreWebView2WebMessageReceivedEventArgs e) + { + var messageString = e.TryGetWebMessageAsString(); - private void OnNewInstanceRestoreFormWindowToForeground(object? sender, AppInstanceStartupEventArgs _) + WebViewLog(message: $"::OnWebViewWebWebMessageReceived({e.Source}|{messageString})"); + } + + private void OnWebViewWebResourceResponseReceived(object? sender, CoreWebView2WebResourceResponseReceivedEventArgs e) + { + WebViewLog(message: $"::OnWebViewWebResourceResponseReceived({e.Response.StatusCode}{e.Response.ReasonPhrase}|{e.Request.Uri})"); + } + + private void OnNewInstanceRestoreFormWindowToForeground(object? sender, AppInstanceStartupEventArgs _) + { + ProcessHelper.InvokeOnUIThread(this, () => { - ProcessHelper.InvokeOnUIThread(this, () => + if (WindowState == FormWindowState.Minimized) { - if (WindowState == FormWindowState.Minimized) - { - User32.ShowWindow(Handle, User32.SW_RESTORE); - } + User32.ShowWindow(Handle, User32.SW_RESTORE); + } - User32.SetForegroundWindow(Handle); - }); - } + User32.SetForegroundWindow(Handle); + }); + } - private void OnNewInstanceSendStartupWebMessage(object? sender, AppInstanceStartupEventArgs e) + private void OnNewInstanceSendStartupWebMessage(object? sender, AppInstanceStartupEventArgs e) + { + if (e.Message?.IsEmpty == false) { - if (e.Message?.IsEmpty == false) + ProcessHelper.InvokeOnUIThread(this, () => { - ProcessHelper.InvokeOnUIThread(this, () => - { - var webMessageString = e.Message.ToWebMessageString(); - WebView.CoreWebView2.PostWebMessageAsString(webMessageString); - }); - } + var webMessageString = e.Message.ToWebMessageString(); + WebView.CoreWebView2.PostWebMessageAsString(webMessageString); + }); } + } - private MemoryStream GetConfigJs() + private MemoryStream GetConfigJs() + { + var config = new { - var config = new - { #if DEBUG - debug = true, + debug = true, #endif - address = _serverAddressProvider.GetListeningAddress(), - token = AppEnvironment.ApiAuthenticationToken, - version = AppEnvironment.VersionInfo.Version, - options = BravoOptions.CreateFromUserPreferences(), - policies = _policies, - culture = new - { - ietfLanguageTag = CultureInfo.CurrentCulture.IetfLanguageTag, - twoLetterISOLanguageName = CultureInfo.CurrentCulture.TwoLetterISOLanguageName - }, - telemetry = new - { - connectionString = TelemetrySessionInfo.ConnectionString, - contextComponentVersion = TelemetrySessionInfo.ComponentVersion, - contextSessionId = TelemetrySessionInfo.SessionId, - contextUserId = TelemetrySessionInfo.UserId, - globalProperties = TelemetrySessionInfo.GlobalProperties - }, - }; + address = _serverAddressProvider.GetListeningAddress(), + token = AppEnvironment.ApiAuthenticationToken, + version = AppEnvironment.VersionInfo.Version, + options = BravoOptions.CreateFromUserPreferences(), + policies = _policies, + culture = new + { + ietfLanguageTag = CultureInfo.CurrentCulture.IetfLanguageTag, + twoLetterISOLanguageName = CultureInfo.CurrentCulture.TwoLetterISOLanguageName + }, + telemetry = new + { + connectionString = TelemetrySessionInfo.ConnectionString, + contextComponentVersion = TelemetrySessionInfo.ComponentVersion, + contextSessionId = TelemetrySessionInfo.SessionId, + contextUserId = TelemetrySessionInfo.UserId, + globalProperties = TelemetrySessionInfo.GlobalProperties + }, + }; - var script = $@"var CONFIG = { JsonSerializer.Serialize(config, options: new JsonSerializerOptions(JsonSerializerDefaults.Web)) };"; + var script = $@"var CONFIG = {JsonSerializer.Serialize(config, options: new JsonSerializerOptions(JsonSerializerDefaults.Web))};"; - return new MemoryStream(Encoding.UTF8.GetBytes(script)); - } + return new MemoryStream(Encoding.UTF8.GetBytes(script)); + } - private void SendAppStartupWebMessage() + private void SendAppStartupWebMessage() + { + var startupSettings = _startupSettingsOptionsAccessor.Value; + if (!startupSettings.IsEmpty) { - var startupSettings = _startupSettingsOptionsAccessor.Value; - if (!startupSettings.IsEmpty) - { - var startupMessage = AppInstanceStartupMessage.CreateFrom(startupSettings); - var startupMessageString = startupMessage.ToWebMessageString(); + var startupMessage = AppInstanceStartupMessage.CreateFrom(startupSettings); + var startupMessageString = startupMessage.ToWebMessageString(); - WebView.CoreWebView2.PostWebMessageAsString(startupMessageString); - } + WebView.CoreWebView2.PostWebMessageAsString(startupMessageString); } + } - [Conditional("DEBUG")] - private void WebViewLog(string message) - { - WebView.CoreWebView2.ExecuteScriptAsync($"console.log('[WEBVIEW]{ message }');"); + [Conditional("DEBUG")] + private void WebViewLog(string message) + { + WebView.CoreWebView2.ExecuteScriptAsync($"console.log('[WEBVIEW]{message}');"); - //if (AppEnvironment.IsDiagnosticLevelVerbose) - // AppEnvironment.AddDiagnostics(DiagnosticMessageType.Text, name: $"{ nameof(AppWindow) }.{ nameof(WebView2) }", content: message); - } + //if (AppEnvironment.IsDiagnosticLevelVerbose) + // AppEnvironment.AddDiagnostics(DiagnosticMessageType.Text, name: $"{ nameof(AppWindow) }.{ nameof(WebView2) }", content: message); } } diff --git a/src/Infrastructure/AppWindowSubclass.cs b/src/Infrastructure/AppWindowSubclass.cs index ecdb7253..f47dcbd5 100644 --- a/src/Infrastructure/AppWindowSubclass.cs +++ b/src/Infrastructure/AppWindowSubclass.cs @@ -1,101 +1,100 @@ -namespace Sqlbi.Bravo.Infrastructure +using System; +using Sqlbi.Bravo.Infrastructure.Configuration; +using Sqlbi.Bravo.Infrastructure.Configuration.Settings; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Windows; +using Sqlbi.Bravo.Infrastructure.Windows.Interop; + +namespace Sqlbi.Bravo.Infrastructure; + +internal class AppWindowSubclass : WindowSubclass { - using Sqlbi.Bravo.Infrastructure.Configuration; - using Sqlbi.Bravo.Infrastructure.Configuration.Settings; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Windows; - using Sqlbi.Bravo.Infrastructure.Windows.Interop; - using System; - - internal class AppWindowSubclass : WindowSubclass - { - private readonly IntPtr TRUE = new(1); // Message handled, do not call the next handler in the subclass chain + private readonly IntPtr TRUE = new(1); // Message handled, do not call the next handler in the subclass chain - /// - /// Try to install a WndProc subclass callback to hook messages sent to the selected window - /// - public static AppWindowSubclass Hook(IntPtr hWnd) => new(hWnd); + /// + /// Try to install a WndProc subclass callback to hook messages sent to the selected window + /// + public static AppWindowSubclass Hook(IntPtr hWnd) => new(hWnd); - private AppWindowSubclass(IntPtr hWnd) - : base(hWnd) - { - } + private AppWindowSubclass(IntPtr hWnd) + : base(hWnd) + { + } - protected override IntPtr WndProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam, IntPtr uIdSubclass, IntPtr dwRefData) + protected override IntPtr WndProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam, IntPtr uIdSubclass, IntPtr dwRefData) + { + switch (uMsg) { - switch (uMsg) + //case (uint)WindowMessage.WM_COPYDATA: + // { + // if (HandleMsgWmCopyData(hWnd, copydataPtr: lParam)) + // return TRUE; + // } + // break; + case (uint)WindowMessage.WM_THEMECHANGED: { - //case (uint)WindowMessage.WM_COPYDATA: - // { - // if (HandleMsgWmCopyData(hWnd, copydataPtr: lParam)) - // return TRUE; - // } - // break; - case (uint)WindowMessage.WM_THEMECHANGED: - { - if (HandleMsgWmThemeChanged(hWnd)) - return TRUE; - } - break; + if (HandleMsgWmThemeChanged(hWnd)) + return TRUE; } - - return base.WndProc(hWnd, uMsg, wParam, lParam, uIdSubclass, dwRefData); + break; } - //private bool HandleMsgWmCopyData(IntPtr hWnd, IntPtr copydataPtr) - //{ - // if (_window.Minimized) - // { - // // Restore original size and position only if the window is minimized, otherwise keeps the current position - // _ = User32.ShowWindow(hWnd, User32.SW_RESTORE); - // } - - // // Regardless of the current state, try to brings into the foreground and activates the window - // _ = User32.SetForegroundWindow(hWnd); - - // try - // { - // var copyDataObject = Marshal.PtrToStructure(copydataPtr, typeof(User32.COPYDATASTRUCT)); - // if (copyDataObject is User32.COPYDATASTRUCT copyData && copyData.cbData != 0) - // { - // if (AppEnvironment.IsDiagnosticLevelVerbose) - // AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{ nameof(AppWindowSubclass) }.{ nameof(HandleMsgWmCopyData) }", content: copyData.lpData); - - // var startupMessage = JsonSerializer.Deserialize(json: copyData.lpData); - // if (startupMessage?.IsEmpty == false) - // { - // var webMessageString = startupMessage.ToWebMessageString(); - // _window.SendWebMessage(webMessageString); - // } - - // // Here we return true because the WM_COPYDATA message has been received and processed, regardless of whether startupMessage is empty or not - // return true; - // } - // } - // catch (Exception ex) - // { - // var exceptionMessage = UnknownWebMessage.CreateFrom(ex); - // var exceptionMessageString = exceptionMessage.AsString; - - // _window.SendWebMessage(exceptionMessageString); - - // if (AppEnvironment.IsDiagnosticLevelVerbose) - // AppEnvironment.AddDiagnostics(name: $"{ nameof(AppWindowSubclass) }.{ nameof(HandleMsgWmCopyData) }", ex); - // } - - // return false; - //} - - private static bool HandleMsgWmThemeChanged(IntPtr hWnd) - { - if (UserPreferences.Current.Theme == ThemeType.Auto) - { - ThemeHelper.ChangeTheme(hWnd, ThemeType.Auto); - } + return base.WndProc(hWnd, uMsg, wParam, lParam, uIdSubclass, dwRefData); + } + + //private bool HandleMsgWmCopyData(IntPtr hWnd, IntPtr copydataPtr) + //{ + // if (_window.Minimized) + // { + // // Restore original size and position only if the window is minimized, otherwise keeps the current position + // _ = User32.ShowWindow(hWnd, User32.SW_RESTORE); + // } + + // // Regardless of the current state, try to brings into the foreground and activates the window + // _ = User32.SetForegroundWindow(hWnd); + + // try + // { + // var copyDataObject = Marshal.PtrToStructure(copydataPtr, typeof(User32.COPYDATASTRUCT)); + // if (copyDataObject is User32.COPYDATASTRUCT copyData && copyData.cbData != 0) + // { + // if (AppEnvironment.IsDiagnosticLevelVerbose) + // AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{ nameof(AppWindowSubclass) }.{ nameof(HandleMsgWmCopyData) }", content: copyData.lpData); + + // var startupMessage = JsonSerializer.Deserialize(json: copyData.lpData); + // if (startupMessage?.IsEmpty == false) + // { + // var webMessageString = startupMessage.ToWebMessageString(); + // _window.SendWebMessage(webMessageString); + // } + + // // Here we return true because the WM_COPYDATA message has been received and processed, regardless of whether startupMessage is empty or not + // return true; + // } + // } + // catch (Exception ex) + // { + // var exceptionMessage = UnknownWebMessage.CreateFrom(ex); + // var exceptionMessageString = exceptionMessage.AsString; - // Here we always return true to avoid that the WM_THEMECHANGED message is sent to the Photino native WndProc - // This is due to the fact that the ThemeHelper class adds a custom non-client area color handler - return true; + // _window.SendWebMessage(exceptionMessageString); + + // if (AppEnvironment.IsDiagnosticLevelVerbose) + // AppEnvironment.AddDiagnostics(name: $"{ nameof(AppWindowSubclass) }.{ nameof(HandleMsgWmCopyData) }", ex); + // } + + // return false; + //} + + private static bool HandleMsgWmThemeChanged(IntPtr hWnd) + { + if (UserPreferences.Current.Theme == ThemeType.Auto) + { + ThemeHelper.ChangeTheme(hWnd, ThemeType.Auto); } + + // Here we always return true to avoid that the WM_THEMECHANGED message is sent to the Photino native WndProc + // This is due to the fact that the ThemeHelper class adds a custom non-client area color handler + return true; } -} \ No newline at end of file +} diff --git a/src/Infrastructure/Authentication/AppAuthenticationHandler.cs b/src/Infrastructure/Authentication/AppAuthenticationHandler.cs index 1b62d412..3d3d6955 100644 --- a/src/Infrastructure/Authentication/AppAuthenticationHandler.cs +++ b/src/Infrastructure/Authentication/AppAuthenticationHandler.cs @@ -1,56 +1,55 @@ -namespace Sqlbi.Bravo.Infrastructure.Authentication +using System; +using System.Net; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using Sqlbi.Bravo.Controllers; + +namespace Sqlbi.Bravo.Infrastructure.Authentication; + +internal class AppAuthenticationHandler : AuthenticationHandler { - using Microsoft.AspNetCore.Authentication; - using Microsoft.Extensions.Logging; - using Microsoft.Extensions.Options; - using Microsoft.Net.Http.Headers; - using Sqlbi.Bravo.Controllers; - using System; - using System.Net; - using System.Security.Claims; - using System.Text.Encodings.Web; - using System.Threading.Tasks; - - internal class AppAuthenticationHandler : AuthenticationHandler + public AppAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) + : base(options, logger, encoder) { - public AppAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder) - : base(options, logger, encoder) - { - } + } + + protected override Task HandleAuthenticateAsync() + { + // RemoteIpAddress is null when Kestrel is behind a reverse proxy or the connection is not a TCP connection (like a Unix domain socket) + var isLoopbackRequest = Context.Connection.RemoteIpAddress is not null && IPAddress.IsLoopback(Context.Connection.RemoteIpAddress); - protected override Task HandleAuthenticateAsync() + if (isLoopbackRequest && Request.Headers.TryGetValue(HeaderNames.Authorization, out var token)) { - // RemoteIpAddress is null when Kestrel is behind a reverse proxy or the connection is not a TCP connection (like a Unix domain socket) - var isLoopbackRequest = Context.Connection.RemoteIpAddress is not null && IPAddress.IsLoopback(Context.Connection.RemoteIpAddress); - - if (isLoopbackRequest && Request.Headers.TryGetValue(HeaderNames.Authorization, out var token)) + var authenticated = AppEnvironment.ApiAuthenticationToken.Equals(token); + if (authenticated == false) { - var authenticated = AppEnvironment.ApiAuthenticationToken.Equals(token); - if (authenticated == false) - { - if (Request.Path.StartsWithSegments(TemplateDevelopmentController.ControllerPathSegment)) - { - authenticated = AppEnvironment.ApiAuthenticationTokenTemplateDevelopment.Equals(token); - } + if (Request.Path.StartsWithSegments(TemplateDevelopmentController.ControllerPathSegment)) + { + authenticated = AppEnvironment.ApiAuthenticationTokenTemplateDevelopment.Equals(token); } - - if (authenticated) + } + + if (authenticated) + { + var claims = new[] { - var claims = new[] - { - new Claim(ClaimTypes.NameIdentifier, $@"{ Environment.UserDomainName }\{ Environment.UserName }"), - new Claim(ClaimTypes.Name, Environment.UserName) - }; + new Claim(ClaimTypes.NameIdentifier, $@"{ Environment.UserDomainName }\{ Environment.UserName }"), + new Claim(ClaimTypes.Name, Environment.UserName) + }; - var identity = new ClaimsIdentity(claims, authenticationType: nameof(AppAuthenticationHandler)); - var principal = new ClaimsPrincipal(identity); - var ticket = new AuthenticationTicket(principal, authenticationScheme: Scheme.Name); + var identity = new ClaimsIdentity(claims, authenticationType: nameof(AppAuthenticationHandler)); + var principal = new ClaimsPrincipal(identity); + var ticket = new AuthenticationTicket(principal, authenticationScheme: Scheme.Name); - return Task.FromResult(AuthenticateResult.Success(ticket)); - } + return Task.FromResult(AuthenticateResult.Success(ticket)); } - - return Task.FromResult(AuthenticateResult.Fail("Authorization failed")); } + + return Task.FromResult(AuthenticateResult.Fail("Authorization failed")); } } diff --git a/src/Infrastructure/Authentication/AppAuthenticationSchemeOptions.cs b/src/Infrastructure/Authentication/AppAuthenticationSchemeOptions.cs index 3d69b07e..4a0fa024 100644 --- a/src/Infrastructure/Authentication/AppAuthenticationSchemeOptions.cs +++ b/src/Infrastructure/Authentication/AppAuthenticationSchemeOptions.cs @@ -1,8 +1,7 @@ using Microsoft.AspNetCore.Authentication; -namespace Sqlbi.Bravo.Infrastructure.Authentication +namespace Sqlbi.Bravo.Infrastructure.Authentication; + +internal class AppAuthenticationSchemeOptions : AuthenticationSchemeOptions { - internal class AppAuthenticationSchemeOptions : AuthenticationSchemeOptions - { - } } diff --git a/src/Infrastructure/Configuration/Settings/ProxySettings.cs b/src/Infrastructure/Configuration/Settings/ProxySettings.cs index b7e7c6c8..067d13cf 100644 --- a/src/Infrastructure/Configuration/Settings/ProxySettings.cs +++ b/src/Infrastructure/Configuration/Settings/ProxySettings.cs @@ -1,142 +1,141 @@ -namespace Sqlbi.Bravo.Infrastructure.Configuration.Settings +using System; +using System.Linq; +using System.Net; +using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; +using System.Text.RegularExpressions; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Security; + +namespace Sqlbi.Bravo.Infrastructure.Configuration.Settings; + +public class ProxySettings { - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Security; - using System; - using System.Linq; - using System.Net; - using System.Runtime.CompilerServices; - using System.Text.Json.Serialization; - using System.Text.RegularExpressions; - - public class ProxySettings + [JsonPropertyName("type")] + public ProxyType Type { get; set; } = ProxyType.System; + + /// + /// Indicate whether the system credentials of the application are sent with requests. + /// https://docs.microsoft.com/en-us/power-bi/connect-data/desktop-troubleshooting-sign-in#using-default-system-credentials-for-web-proxy + /// + [JsonPropertyName("useDefaultCredentials")] + public bool UseDefaultCredentials { get; set; } = true; + + /// + /// The address of the proxy server. + /// + [JsonPropertyName("address")] + public string? Address { get; set; } + + /// + /// Indicates whether to bypass the proxy server for local addresses. The default value is true. + /// + [JsonPropertyName("bypassOnLocal")] + public bool BypassOnLocal { get; set; } = true; + + /// + /// An array of addresses that do not use the proxy server + /// + [JsonPropertyName("bypassList")] + public string? BypassList { get; set; } + + [MethodImpl(MethodImplOptions.NoOptimization)] + public bool Validate(bool throwOnError = true) { - [JsonPropertyName("type")] - public ProxyType Type { get; set; } = ProxyType.System; - - /// - /// Indicate whether the system credentials of the application are sent with requests. - /// https://docs.microsoft.com/en-us/power-bi/connect-data/desktop-troubleshooting-sign-in#using-default-system-credentials-for-web-proxy - /// - [JsonPropertyName("useDefaultCredentials")] - public bool UseDefaultCredentials { get; set; } = true; - - /// - /// The address of the proxy server. - /// - [JsonPropertyName("address")] - public string? Address { get; set; } - - /// - /// Indicates whether to bypass the proxy server for local addresses. The default value is true. - /// - [JsonPropertyName("bypassOnLocal")] - public bool BypassOnLocal { get; set; } = true; - - /// - /// An array of addresses that do not use the proxy server - /// - [JsonPropertyName("bypassList")] - public string? BypassList { get; set; } - - [MethodImpl(MethodImplOptions.NoOptimization)] - public bool Validate(bool throwOnError = true) + try { - try + var bypassList = BypassList?.Split(";", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (bypassList?.Length > 0) { - var bypassList = BypassList?.Split(";", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - if (bypassList?.Length > 0) + foreach (var bypassListItem in bypassList) { - foreach (var bypassListItem in bypassList) - { - _ = new Regex(bypassListItem, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); - } + _ = new Regex(bypassListItem, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); } - - if (Address is not null) - { - if (Address.Contains(Uri.SchemeDelimiter)) - { - _ = new Uri(Address); - } - else - { - _ = new Uri($"{ Uri.UriSchemeHttp }{ Uri.SchemeDelimiter }{ Address }"); - } - } - - return true; } - catch - { - if (throwOnError) - throw; - - return false; - } - } - internal ICredentials? GetCredentials() - { - if (UseDefaultCredentials == false) + if (Address is not null) { - if (CredentialManager.TryGetCredential(targetName: AppEnvironment.CredentialManagerProxyCredentialName, out var genericCredential)) + if (Address.Contains(Uri.SchemeDelimiter)) + { + _ = new Uri(Address); + } + else { - var credentials = genericCredential.ToNetworkCredential(); - return credentials; + _ = new Uri($"{Uri.UriSchemeHttp}{Uri.SchemeDelimiter}{Address}"); } } - return CredentialCache.DefaultCredentials; + return true; } - - internal static string[] GetSafeBypassList(string? bypassListString, bool includeLoopback) + catch { - var bypassList = bypassListString?.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - var safeBypassList = GetSafeBypassList(bypassList, includeLoopback); + if (throwOnError) + throw; - return safeBypassList; + return false; } + } - internal static string[] GetSafeBypassList(string[]? bypassList, bool includeLoopback) + internal ICredentials? GetCredentials() + { + if (UseDefaultCredentials == false) { - if (bypassList is null) - bypassList = Array.Empty(); - - var safeBypassList = bypassList.ToList(); - _ = safeBypassList.RemoveAll(NetworkHelper.LoopbackProxyBypassRule.EqualsTI); // To prevent traffic to localhost from being sent through a proxy - - if (includeLoopback) + if (CredentialManager.TryGetCredential(targetName: AppEnvironment.CredentialManagerProxyCredentialName, out var genericCredential)) { - // Include loopback addresses to avoid routing local WebAPI traffic through a web proxy - // Applied even though it would not be necessary since the browser applies implicit bypass rules - https://chromium.googlesource.com/chromium/src/+/HEAD/net/docs/proxy.md#implicit-bypass-rules - //-- - // !!! Keep these rules at the end of the string/list, this is because sorting can matter when using a subtractive rule, as the rules will be evaluated in a left to right order - // -- - safeBypassList.Add("{0}".FormatInvariant(IPAddress.Loopback)); - safeBypassList.Add("{0}".FormatInvariant(IPAddress.IPv6Loopback)); // IPv6 literals must not be bracketed + var credentials = genericCredential.ToNetworkCredential(); + return credentials; } - - return safeBypassList.ToArray(); } + + return CredentialCache.DefaultCredentials; } - public enum ProxyType + internal static string[] GetSafeBypassList(string? bypassListString, bool includeLoopback) { - /// - /// Specifies not to use a Proxy, even if the system is otherwise configured to use one. It overrides and ignore any other proxy settings that are provided - /// - None = 0, - - /// - /// Specifies to try and automatically detect the system proxy configuration. This is the default value - /// - System = 1, - - /// - /// Specifies to use a custom proxy configuration and applies all other proxy settings that are provided - /// - Custom = 2, + var bypassList = bypassListString?.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + var safeBypassList = GetSafeBypassList(bypassList, includeLoopback); + + return safeBypassList; } + + internal static string[] GetSafeBypassList(string[]? bypassList, bool includeLoopback) + { + if (bypassList is null) + bypassList = Array.Empty(); + + var safeBypassList = bypassList.ToList(); + _ = safeBypassList.RemoveAll(NetworkHelper.LoopbackProxyBypassRule.EqualsTI); // To prevent traffic to localhost from being sent through a proxy + + if (includeLoopback) + { + // Include loopback addresses to avoid routing local WebAPI traffic through a web proxy + // Applied even though it would not be necessary since the browser applies implicit bypass rules - https://chromium.googlesource.com/chromium/src/+/HEAD/net/docs/proxy.md#implicit-bypass-rules + //-- + // !!! Keep these rules at the end of the string/list, this is because sorting can matter when using a subtractive rule, as the rules will be evaluated in a left to right order + // -- + safeBypassList.Add("{0}".FormatInvariant(IPAddress.Loopback)); + safeBypassList.Add("{0}".FormatInvariant(IPAddress.IPv6Loopback)); // IPv6 literals must not be bracketed + } + + return safeBypassList.ToArray(); + } +} + +public enum ProxyType +{ + /// + /// Specifies not to use a Proxy, even if the system is otherwise configured to use one. It overrides and ignore any other proxy settings that are provided + /// + None = 0, + + /// + /// Specifies to try and automatically detect the system proxy configuration. This is the default value + /// + System = 1, + + /// + /// Specifies to use a custom proxy configuration and applies all other proxy settings that are provided + /// + Custom = 2, } diff --git a/src/Infrastructure/Configuration/Settings/StartupSettings.cs b/src/Infrastructure/Configuration/Settings/StartupSettings.cs index c7dcd071..ace85e29 100644 --- a/src/Infrastructure/Configuration/Settings/StartupSettings.cs +++ b/src/Infrastructure/Configuration/Settings/StartupSettings.cs @@ -1,131 +1,129 @@ -namespace Sqlbi.Bravo.Infrastructure.Configuration.Settings +using System; +using System.CommandLine; +using System.CommandLine.Parsing; +using System.Diagnostics; +using System.Linq; +using System.Text.Json.Serialization; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Helpers; + +namespace Sqlbi.Bravo.Infrastructure.Configuration.Settings; + +internal class StartupSettings { - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Helpers; - using System; - using System.CommandLine; - using System.CommandLine.Parsing; - using System.Diagnostics; - using System.Linq; - using System.Text.Json.Serialization; - - internal class StartupSettings - { - [JsonPropertyName("isEmpty")] - public bool IsEmpty { get; set; } + [JsonPropertyName("isEmpty")] + public bool IsEmpty { get; set; } - [JsonPropertyName("externalTool")] - public bool IsExternalTool { get; set; } + [JsonPropertyName("externalTool")] + public bool IsExternalTool { get; set; } - [JsonPropertyName("serverName")] - public string? ArgumentServerName { get; set; } + [JsonPropertyName("serverName")] + public string? ArgumentServerName { get; set; } - [JsonPropertyName("databaseName")] - public string? ArgumentDatabaseName { get; set; } + [JsonPropertyName("databaseName")] + public string? ArgumentDatabaseName { get; set; } - [JsonPropertyName("commandLineErrors")] - public string[]? CommandLineErrors { get; set; } + [JsonPropertyName("commandLineErrors")] + public string[]? CommandLineErrors { get; set; } - [JsonIgnore] - public bool IsPBIDesktopExternalTool => IsExternalTool && AppEnvironment.PBIDesktopProcessName.Equals(ParentProcessName, StringComparison.OrdinalIgnoreCase); + [JsonIgnore] + public bool IsPBIDesktopExternalTool => IsExternalTool && AppEnvironment.PBIDesktopProcessName.Equals(ParentProcessName, StringComparison.OrdinalIgnoreCase); - [JsonIgnore] - public int? ParentProcessId { get; set; } + [JsonIgnore] + public int? ParentProcessId { get; set; } - [JsonIgnore] - public string? ParentProcessName { get; set; } + [JsonIgnore] + public string? ParentProcessName { get; set; } - //[JsonIgnore] - //public IntPtr ParentProcessMainWindowHandle { get; set; } + //[JsonIgnore] + //public IntPtr ParentProcessMainWindowHandle { get; set; } - [JsonIgnore] - public string? ParentProcessMainWindowTitle { get; set; } + [JsonIgnore] + public string? ParentProcessMainWindowTitle { get; set; } - public static StartupSettings CreateFromCommandLineArguments() + public static StartupSettings CreateFromCommandLineArguments() + { + var settings = new StartupSettings(); { - var settings = new StartupSettings(); - { - settings.FromCommandLineArguments(); - } - - return settings; + settings.FromCommandLineArguments(); } + + return settings; } +} - internal static class StartupSettingsExtensions +internal static class StartupSettingsExtensions +{ + public static void FromCommandLineArguments(this StartupSettings settings) { - public static void FromCommandLineArguments(this StartupSettings settings) + // Skip the first command line arg as the first element in the array contains the file name of the executing program. + // If the file name is not available, the first element is equal to String.Empty. + // In.NET 5 and later versions, for single-file publishing, the first element is the name of the host executable. + var args = Environment.GetCommandLineArgs().Skip(1).ToArray(); + if (args.Length == 0) { - // Skip the first command line arg as the first element in the array contains the file name of the executing program. - // If the file name is not available, the first element is equal to String.Empty. - // In.NET 5 and later versions, for single-file publishing, the first element is the name of the host executable. - var args = Environment.GetCommandLineArgs().Skip(1).ToArray(); - if (args.Length == 0) - { - // No args provided - settings.IsEmpty = true; - return; - } + // No args provided + settings.IsEmpty = true; + return; + } - var serverOption = new Option("--server"); - serverOption.AddAlias("-s"); - serverOption.IsRequired = true; - serverOption.Description = "Server name"; - serverOption.Arity = ArgumentArity.ExactlyOne; + var serverOption = new Option("--server"); + serverOption.AddAlias("-s"); + serverOption.IsRequired = true; + serverOption.Description = "Server name"; + serverOption.Arity = ArgumentArity.ExactlyOne; - var databaseOption = new Option("--database"); - databaseOption.AddAlias("-d"); - databaseOption.IsRequired = true; - databaseOption.Description = "Database name"; - databaseOption.Arity = ArgumentArity.ExactlyOne; + var databaseOption = new Option("--database"); + databaseOption.AddAlias("-d"); + databaseOption.IsRequired = true; + databaseOption.Description = "Database name"; + databaseOption.Arity = ArgumentArity.ExactlyOne; - var parentProcessIdOption = new Option("--ppid"); - parentProcessIdOption.IsRequired = false; - parentProcessIdOption.Description = "Parent process ID"; - parentProcessIdOption.Arity = ArgumentArity.ExactlyOne; + var parentProcessIdOption = new Option("--ppid"); + parentProcessIdOption.IsRequired = false; + parentProcessIdOption.Description = "Parent process ID"; + parentProcessIdOption.Arity = ArgumentArity.ExactlyOne; - var command = new RootCommand - { - serverOption, - databaseOption, - parentProcessIdOption - }; + var command = new RootCommand + { + serverOption, + databaseOption, + parentProcessIdOption + }; - var parseResult = command.Parse(args); + var parseResult = command.Parse(args); - var serverOptionResult = parseResult.FindResultFor(serverOption); - if (serverOptionResult is not null && serverOptionResult.ErrorMessage is null) - settings.ArgumentServerName = parseResult.GetValueForOption(serverOption); + var serverOptionResult = parseResult.FindResultFor(serverOption); + if (serverOptionResult is not null && serverOptionResult.ErrorMessage is null) + settings.ArgumentServerName = parseResult.GetValueForOption(serverOption); - var databaseOptionResult = parseResult.FindResultFor(databaseOption); - if (databaseOptionResult is not null && databaseOptionResult.ErrorMessage is null) - settings.ArgumentDatabaseName = parseResult.GetValueForOption(databaseOption); + var databaseOptionResult = parseResult.FindResultFor(databaseOption); + if (databaseOptionResult is not null && databaseOptionResult.ErrorMessage is null) + settings.ArgumentDatabaseName = parseResult.GetValueForOption(databaseOption); - settings.IsExternalTool = parseResult.HasOption(serverOption) || parseResult.HasOption(databaseOption); - settings.CommandLineErrors = parseResult.Errors.Select((e) => e.Message).ToArray(); + settings.IsExternalTool = parseResult.HasOption(serverOption) || parseResult.HasOption(databaseOption); + settings.CommandLineErrors = parseResult.Errors.Select((e) => e.Message).ToArray(); + + Process? parentProcess = null; + { + if (AppEnvironment.DeploymentMode == AppDeploymentMode.Packaged && parseResult.HasOption(parentProcessIdOption)) + { + var parentProcessId = parseResult.GetValueForOption(parentProcessIdOption); + parentProcess = ProcessHelper.SafeGetProcessById(parentProcessId); + } + else + { + parentProcess = ProcessHelper.GetParentProcess(); + } - Process? parentProcess = null; + if (parentProcess is not null) { - if (AppEnvironment.DeploymentMode == AppDeploymentMode.Packaged && parseResult.HasOption(parentProcessIdOption)) - { - var parentProcessId = parseResult.GetValueForOption(parentProcessIdOption); - parentProcess = ProcessHelper.SafeGetProcessById(parentProcessId); - } - else - { - parentProcess = ProcessHelper.GetParentProcess(); - } - - if (parentProcess is not null) - { - settings.ParentProcessId = parentProcess.Id; - settings.ParentProcessName = parentProcess.ProcessName; - //settings.ParentProcessMainWindowHandle = parentProcess.MainWindowHandle; - settings.ParentProcessMainWindowTitle = settings.IsPBIDesktopExternalTool ? parentProcess.GetPBIDesktopMainWindowTitle() : parentProcess.GetMainWindowTitle(); - } + settings.ParentProcessId = parentProcess.Id; + settings.ParentProcessName = parentProcess.ProcessName; + //settings.ParentProcessMainWindowHandle = parentProcess.MainWindowHandle; + settings.ParentProcessMainWindowTitle = settings.IsPBIDesktopExternalTool ? parentProcess.GetPBIDesktopMainWindowTitle() : parentProcess.GetMainWindowTitle(); } - parentProcess?.Dispose(); } + parentProcess?.Dispose(); } } diff --git a/src/Infrastructure/Configuration/Settings/UserSettings.cs b/src/Infrastructure/Configuration/Settings/UserSettings.cs index 06a1dcaa..e13f3ea2 100644 --- a/src/Infrastructure/Configuration/Settings/UserSettings.cs +++ b/src/Infrastructure/Configuration/Settings/UserSettings.cs @@ -1,101 +1,100 @@ -namespace Sqlbi.Bravo.Infrastructure.Configuration.Settings -{ - using System.Text.Json; - using System.Text.Json.Serialization; - - public interface IUserSettings - { - bool TelemetryEnabled { get; set; } - - DiagnosticLevelType DiagnosticLevel { get; set; } +using System.Text.Json; +using System.Text.Json.Serialization; - UpdateChannelType UpdateChannel { get; set; } +namespace Sqlbi.Bravo.Infrastructure.Configuration.Settings; - bool UpdateCheckEnabled { get; set; } +public interface IUserSettings +{ + bool TelemetryEnabled { get; set; } - ThemeType Theme { get; set; } + DiagnosticLevelType DiagnosticLevel { get; set; } - ProxySettings? Proxy { get; set; } + UpdateChannelType UpdateChannel { get; set; } - bool UseSystemBrowserForAuthentication { get; set; } + bool UpdateCheckEnabled { get; set; } - bool CustomTemplatesEnabled { get; set; } + ThemeType Theme { get; set; } - JsonElement? CustomOptions { get; set; } - } + ProxySettings? Proxy { get; set; } - public class UserSettings : IUserSettings - { - public const bool DefaultTelemetryEnabled = true; - public const DiagnosticLevelType DefaultDiagnosticLevel = DiagnosticLevelType.None; - public const UpdateChannelType DefaultUpdateChannel = UpdateChannelType.Stable; - public const bool DefaultUpdateCheckEnabled = true; - public const ThemeType DefaultTheme = ThemeType.Auto; - public const bool DefaultUseSystemBrowserForAuthentication = false; - public const bool DefaultCustomTemplatesEnabled = true; + bool UseSystemBrowserForAuthentication { get; set; } - [JsonPropertyName("telemetryEnabled")] - public bool TelemetryEnabled { get; set; } = DefaultTelemetryEnabled; + bool CustomTemplatesEnabled { get; set; } - [JsonPropertyName("diagnosticLevel")] - public DiagnosticLevelType DiagnosticLevel { get; set; } = DefaultDiagnosticLevel; + JsonElement? CustomOptions { get; set; } +} - [JsonPropertyName("updateChannel")] - public UpdateChannelType UpdateChannel { get; set; } = DefaultUpdateChannel; +public class UserSettings : IUserSettings +{ + public const bool DefaultTelemetryEnabled = true; + public const DiagnosticLevelType DefaultDiagnosticLevel = DiagnosticLevelType.None; + public const UpdateChannelType DefaultUpdateChannel = UpdateChannelType.Stable; + public const bool DefaultUpdateCheckEnabled = true; + public const ThemeType DefaultTheme = ThemeType.Auto; + public const bool DefaultUseSystemBrowserForAuthentication = false; + public const bool DefaultCustomTemplatesEnabled = true; - [JsonPropertyName("updateCheckEnabled")] - public bool UpdateCheckEnabled { get; set; } = DefaultUpdateCheckEnabled; + [JsonPropertyName("telemetryEnabled")] + public bool TelemetryEnabled { get; set; } = DefaultTelemetryEnabled; - [JsonPropertyName("theme")] - public ThemeType Theme { get; set; } = DefaultTheme; + [JsonPropertyName("diagnosticLevel")] + public DiagnosticLevelType DiagnosticLevel { get; set; } = DefaultDiagnosticLevel; - [JsonPropertyName("proxy")] - public ProxySettings? Proxy { get; set; } + [JsonPropertyName("updateChannel")] + public UpdateChannelType UpdateChannel { get; set; } = DefaultUpdateChannel; - [JsonPropertyName("useSystemBrowserForAuthentication")] - public bool UseSystemBrowserForAuthentication { get; set; } = DefaultUseSystemBrowserForAuthentication; + [JsonPropertyName("updateCheckEnabled")] + public bool UpdateCheckEnabled { get; set; } = DefaultUpdateCheckEnabled; - [JsonPropertyName("customTemplatesEnabled")] - public bool CustomTemplatesEnabled { get; set; } = DefaultCustomTemplatesEnabled; + [JsonPropertyName("theme")] + public ThemeType Theme { get; set; } = DefaultTheme; - [JsonPropertyName("customOptions")] - public JsonElement? CustomOptions { get; set; } - } + [JsonPropertyName("proxy")] + public ProxySettings? Proxy { get; set; } - public enum ThemeType - { - Auto = 0, - Light = 1, - Dark = 2 - } + [JsonPropertyName("useSystemBrowserForAuthentication")] + public bool UseSystemBrowserForAuthentication { get; set; } = DefaultUseSystemBrowserForAuthentication; - public enum UpdateChannelType - { - /// - /// (Default) Stable builds are the best ones to use, they are a result of the code being built in Canary, tested in Dev and bug fixed in Beta - /// - Stable = 0, + [JsonPropertyName("customTemplatesEnabled")] + public bool CustomTemplatesEnabled { get; set; } = DefaultCustomTemplatesEnabled; - ///// - ///// Beta channel is the best build to get if you’re interested in being the first one to know about upcoming features - ///// - //Beta = 1, + [JsonPropertyName("customOptions")] + public JsonElement? CustomOptions { get; set; } +} - /// - /// Dev build will carry the improvements made to the application and tested by the developers, it’s still not recommended to use it because it can have bugs - /// - Dev = 2, +public enum ThemeType +{ + Auto = 0, + Light = 1, + Dark = 2 +} - ///// - ///// Canary build carries features that are released as soon as they’re built and are not tested or used - ///// - //Canary = 3, - } +public enum UpdateChannelType +{ + /// + /// (Default) Stable builds are the best ones to use, they are a result of the code being built in Canary, tested in Dev and bug fixed in Beta + /// + Stable = 0, + + ///// + ///// Beta channel is the best build to get if you’re interested in being the first one to know about upcoming features + ///// + //Beta = 1, + + /// + /// Dev build will carry the improvements made to the application and tested by the developers, it’s still not recommended to use it because it can have bugs + /// + Dev = 2, + + ///// + ///// Canary build carries features that are released as soon as they’re built and are not tested or used + ///// + //Canary = 3, +} - public enum DiagnosticLevelType - { - None = 0, - Basic = 1, - Verbose = 2 - } +public enum DiagnosticLevelType +{ + None = 0, + Basic = 1, + Verbose = 2 } diff --git a/src/Infrastructure/Configuration/StartupConfiguration.cs b/src/Infrastructure/Configuration/StartupConfiguration.cs index f5ecfcca..747e3d88 100644 --- a/src/Infrastructure/Configuration/StartupConfiguration.cs +++ b/src/Infrastructure/Configuration/StartupConfiguration.cs @@ -1,74 +1,74 @@ -namespace Sqlbi.Bravo.Infrastructure.Configuration -{ - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Services; - using System.IO; - using System.Net.Http; - using System.Runtime; +using System; +using System.IO; +using System.Net.Http; +using System.Runtime; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Services; + +namespace Sqlbi.Bravo.Infrastructure.Configuration; - internal static class StartupConfiguration +internal static class StartupConfiguration +{ + public static void Configure() { - public static void Configure() - { - ApplicationConfiguration.Initialize(); + ApplicationConfiguration.Initialize(); - //if (AppEnvironment.IsOSVersionUnsupported) - //{ - // MessageDialog.Show(heading: "Unsupported Windows OS version", text: "This application is only supported on Windows 10 series operating systems version 1809 (build 17763) or higher."); - // Environment.Exit(NativeMethods.NO_ERROR); - //} + //if (AppEnvironment.IsOSVersionUnsupported) + //{ + // MessageDialog.Show(heading: "Unsupported Windows OS version", text: "This application is only supported on Windows 10 series operating systems version 1809 (build 17763) or higher."); + // Environment.Exit(NativeMethods.NO_ERROR); + //} - ConfigureProxy(); - ConfigureDirectories(); - ConfigureMulticoreJit(); + ConfigureProxy(); + ConfigureDirectories(); + ConfigureMulticoreJit(); - WebView2Helper.EnsureRuntimeIsInstalled(); - } + WebView2Helper.EnsureRuntimeIsInstalled(); + } - private static void ConfigureProxy() - { - HttpClient.DefaultProxy = WebProxyWrapper.Current; - } + private static void ConfigureProxy() + { + HttpClient.DefaultProxy = WebProxyWrapper.Current; + } - private static void ConfigureDirectories() - { - Directory.SetCurrentDirectory(AppContext.BaseDirectory); - Directory.CreateDirectory(AppEnvironment.ApplicationDataPath); - Directory.CreateDirectory(AppEnvironment.ApplicationTempPath); - } + private static void ConfigureDirectories() + { + Directory.SetCurrentDirectory(AppContext.BaseDirectory); + Directory.CreateDirectory(AppEnvironment.ApplicationDataPath); + Directory.CreateDirectory(AppEnvironment.ApplicationTempPath); + } - //private static void ConfigureProcessDpiAwareness() - //{ - // var windows8Version = new Version(6, 3, 0); // win 8.1 (build number 9600) added support for per monitor dpi - // var windows10Version = new Version(10, 0, 15063); // Windows 10 version 1703 (build number 15063) added support for per monitor dpi v2 - // var environmentOSVersion = Environment.OSVersion.Version; + //private static void ConfigureProcessDpiAwareness() + //{ + // var windows8Version = new Version(6, 3, 0); // win 8.1 (build number 9600) added support for per monitor dpi + // var windows10Version = new Version(10, 0, 15063); // Windows 10 version 1703 (build number 15063) added support for per monitor dpi v2 + // var environmentOSVersion = Environment.OSVersion.Version; - // if (environmentOSVersion >= windows8Version) - // { - // if (environmentOSVersion >= windows10Version) - // { - // _ = User32.SetProcessDpiAwarenessContext(User32.DPI_AWARENESS_CONTEXT.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + // if (environmentOSVersion >= windows8Version) + // { + // if (environmentOSVersion >= windows10Version) + // { + // _ = User32.SetProcessDpiAwarenessContext(User32.DPI_AWARENESS_CONTEXT.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); - // // Applications running at a DPI_AWARENESS_CONTEXT of DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 automatically scale their non-client areas by default. - // // Here we don't need to call User32.EnableNonClientDpiScaling function. - // } - // else - // { - // _ = User32.SetProcessDpiAwareness(User32.PROCESS_DPI_AWARENESS.PROCESS_PER_MONITOR_DPI_AWARE); - // // TODO: need to call User32.EnableNonClientDpiScaling function, see comment above - // } - // } - // else - // { - // _ = User32.SetProcessDPIAware(); - // // TODO: need to call User32.EnableNonClientDpiScaling function, see comment above - // } - //} + // // Applications running at a DPI_AWARENESS_CONTEXT of DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 automatically scale their non-client areas by default. + // // Here we don't need to call User32.EnableNonClientDpiScaling function. + // } + // else + // { + // _ = User32.SetProcessDpiAwareness(User32.PROCESS_DPI_AWARENESS.PROCESS_PER_MONITOR_DPI_AWARE); + // // TODO: need to call User32.EnableNonClientDpiScaling function, see comment above + // } + // } + // else + // { + // _ = User32.SetProcessDPIAware(); + // // TODO: need to call User32.EnableNonClientDpiScaling function, see comment above + // } + //} - private static void ConfigureMulticoreJit() - { - ProfileOptimization.SetProfileRoot(AppEnvironment.ApplicationDataPath); - ProfileOptimization.StartProfile(".jitprofile"); - } + private static void ConfigureMulticoreJit() + { + ProfileOptimization.SetProfileRoot(AppEnvironment.ApplicationDataPath); + ProfileOptimization.StartProfile(".jitprofile"); } } diff --git a/src/Infrastructure/Configuration/UserPreferences.cs b/src/Infrastructure/Configuration/UserPreferences.cs index 0b78be3d..82f66fa9 100644 --- a/src/Infrastructure/Configuration/UserPreferences.cs +++ b/src/Infrastructure/Configuration/UserPreferences.cs @@ -1,116 +1,115 @@ -namespace Sqlbi.Bravo.Infrastructure.Configuration +using System; +using System.Diagnostics; +using System.IO; +using System.Text.Json; +using Sqlbi.Bravo.Infrastructure.Configuration.Settings; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Helpers; + +namespace Sqlbi.Bravo.Infrastructure.Configuration; + +internal static class UserPreferences { - using Sqlbi.Bravo.Infrastructure.Configuration.Settings; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Helpers; - using System; - using System.Diagnostics; - using System.IO; - using System.Text.Json; + private static readonly JsonSerializerOptions _serializationOptions; + private static readonly Lazy _settings; - internal static class UserPreferences + static UserPreferences() { - private static readonly JsonSerializerOptions _serializationOptions; - private static readonly Lazy _settings; + _settings = new Lazy(CreateInstance, isThreadSafe: true); + _serializationOptions = new() + { + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true, + WriteIndented = true, + }; + } + + public static UserSettings Current => _settings.Value; - static UserPreferences() + public static void Save() + { + try { - _settings = new Lazy(CreateInstance, isThreadSafe: true); - _serializationOptions = new() - { - ReadCommentHandling = JsonCommentHandling.Skip, - AllowTrailingCommas = true, - WriteIndented = true, - }; + var settingsString = JsonSerializer.Serialize(Current, _serializationOptions); + File.WriteAllText(AppEnvironment.UserSettingsFilePath, settingsString); } + catch (Exception ex) + { + throw new BravoException(BravoProblem.UserSettingsSaveError, ex.Message, ex); + } + } - public static UserSettings Current => _settings.Value; - - public static void Save() + private static UserSettings CreateInstance() + { + try { - try - { - var settingsString = JsonSerializer.Serialize(Current, _serializationOptions); - File.WriteAllText(AppEnvironment.UserSettingsFilePath, settingsString); - } - catch (Exception ex) - { - throw new BravoException(BravoProblem.UserSettingsSaveError, ex.Message, ex); - } + var settings = CreateInstanceFromFile(); + if (settings is not null) + return settings; + } + catch (Exception ex) + { + ExceptionHelper.WriteToEventLog(ex, EventLogEntryType.Warning, throwOnError: false); } - private static UserSettings CreateInstance() + // Creation from file failed, the file is corrupted, does not exists or it's empty. + var defaultSettings = new UserSettings(); { try { - var settings = CreateInstanceFromFile(); - if (settings is not null) - return settings; + UpdateFromRegistry(defaultSettings); } catch (Exception ex) { ExceptionHelper.WriteToEventLog(ex, EventLogEntryType.Warning, throwOnError: false); } - - // Creation from file failed, the file is corrupted, does not exists or it's empty. - var defaultSettings = new UserSettings(); - { - try - { - UpdateFromRegistry(defaultSettings); - } - catch (Exception ex) - { - ExceptionHelper.WriteToEventLog(ex, EventLogEntryType.Warning, throwOnError: false); - } - } - return defaultSettings; } + return defaultSettings; + } - private static UserSettings? CreateInstanceFromFile() + private static UserSettings? CreateInstanceFromFile() + { + if (File.Exists(AppEnvironment.UserSettingsFilePath)) { - if (File.Exists(AppEnvironment.UserSettingsFilePath)) - { - var settingsString = File.ReadAllText(AppEnvironment.UserSettingsFilePath); - var settings = JsonSerializer.Deserialize(settingsString, _serializationOptions); + var settingsString = File.ReadAllText(AppEnvironment.UserSettingsFilePath); + var settings = JsonSerializer.Deserialize(settingsString, _serializationOptions); - // Validation - if (settings is not null) + // Validation + if (settings is not null) + { + var validProxy = settings.Proxy?.Validate(throwOnError: false); + if (validProxy == false) { - var validProxy = settings.Proxy?.Validate(throwOnError: false); - if (validProxy == false) - { - settings.Proxy = null; - } - - if (!Enum.IsDefined(typeof(ThemeType), (int)settings.Theme)) - { - settings.Theme = ThemeType.Auto; - } + settings.Proxy = null; } - return settings; + if (!Enum.IsDefined(typeof(ThemeType), (int)settings.Theme)) + { + settings.Theme = ThemeType.Auto; + } } - return null; + return settings; } - private static void UpdateFromRegistry(UserSettings settings) + return null; + } + + private static void UpdateFromRegistry(UserSettings settings) + { + var registryKey = AppEnvironment.ApplicationInstallerRegistryHKey; + if (registryKey is not null) { - var registryKey = AppEnvironment.ApplicationInstallerRegistryHKey; - if (registryKey is not null) + var valueString = registryKey.GetStringValue(subkeyName: AppEnvironment.ApplicationRegistryKeyName, valueName: AppEnvironment.ApplicationRegistryApplicationTelemetryEnabledValue); + if (valueString is not null) { - var valueString = registryKey.GetStringValue(subkeyName: AppEnvironment.ApplicationRegistryKeyName, valueName: AppEnvironment.ApplicationRegistryApplicationTelemetryEnabledValue); - if (valueString is not null) + if (int.TryParse(valueString, out var intValue)) + { + settings.TelemetryEnabled = Convert.ToBoolean(intValue); + } + else { - if (int.TryParse(valueString, out var intValue)) - { - settings.TelemetryEnabled = Convert.ToBoolean(intValue); - } - else - { - settings.TelemetryEnabled = false; - } + settings.TelemetryEnabled = false; } } } diff --git a/src/Infrastructure/Extensions/CommonExtensions.cs b/src/Infrastructure/Extensions/CommonExtensions.cs index 1ed85e35..f783cf22 100644 --- a/src/Infrastructure/Extensions/CommonExtensions.cs +++ b/src/Infrastructure/Extensions/CommonExtensions.cs @@ -1,55 +1,54 @@ -namespace Sqlbi.Bravo.Infrastructure.Extensions -{ - using System; - using System.Text.Json; +using System; +using System.Text.Json; + +namespace Sqlbi.Bravo.Infrastructure.Extensions; - internal static class EnumExtensions +internal static class EnumExtensions +{ + public static TEnum? TryParseTo(this Enum? value) where TEnum : struct, Enum { - public static TEnum? TryParseTo(this Enum? value) where TEnum : struct, Enum + if (value is not null) { - if (value is not null) - { - var valueString = value.ToString(); - var valueEnum = TryParseTo(valueString); + var valueString = value.ToString(); + var valueEnum = TryParseTo(valueString); - return valueEnum; - } - - return null; + return valueEnum; } - public static TEnum? TryParseTo(this string? value) where TEnum : struct, Enum + return null; + } + + public static TEnum? TryParseTo(this string? value) where TEnum : struct, Enum + { + if (value is not null) { - if (value is not null) + if (Enum.TryParse(value, ignoreCase: true, out var valueEnum)) { - if (Enum.TryParse(value, ignoreCase: true, out var valueEnum)) - { - return valueEnum; - } + return valueEnum; } - - return null; } - public static TEnum? TryParseTo(this int? value) where TEnum : struct, Enum + return null; + } + + public static TEnum? TryParseTo(this int? value) where TEnum : struct, Enum + { + if (value is not null) { - if (value is not null) + if (Enum.IsDefined(typeof(TEnum), value.Value)) { - if (Enum.IsDefined(typeof(TEnum), value.Value)) - { - var @enum = (TEnum)Enum.ToObject(typeof(TEnum), value.Value); - return @enum; - } + var @enum = (TEnum)Enum.ToObject(typeof(TEnum), value.Value); + return @enum; } - - return null; } - public static T? JsonClone(this T value) - { - var json = JsonSerializer.Serialize(value); - var instance = JsonSerializer.Deserialize(json); - return instance; - } + return null; + } + + public static T? JsonClone(this T value) + { + var json = JsonSerializer.Serialize(value); + var instance = JsonSerializer.Deserialize(json); + return instance; } } diff --git a/src/Infrastructure/Extensions/DataReaderExtensions.cs b/src/Infrastructure/Extensions/DataReaderExtensions.cs index 9b1c2a5f..128d2f0f 100644 --- a/src/Infrastructure/Extensions/DataReaderExtensions.cs +++ b/src/Infrastructure/Extensions/DataReaderExtensions.cs @@ -1,19 +1,18 @@ -namespace Sqlbi.Bravo.Infrastructure.Extensions -{ - using System.Collections.Generic; - using System; - using System.Data; +using System; +using System.Collections.Generic; +using System.Data; + +namespace Sqlbi.Bravo.Infrastructure.Extensions; - internal static class DataReaderExtensions +internal static class DataReaderExtensions +{ + public static IEnumerable Select(this IDataReader reader, Func selector) { - public static IEnumerable Select(this IDataReader reader, Func selector) + while (reader.Read()) { - while (reader.Read()) - { - yield return selector(reader); - } - - reader.Close(); + yield return selector(reader); } + + reader.Close(); } } diff --git a/src/Infrastructure/Extensions/EnumerableExtensions.cs b/src/Infrastructure/Extensions/EnumerableExtensions.cs index fd4c2d64..934240d3 100644 --- a/src/Infrastructure/Extensions/EnumerableExtensions.cs +++ b/src/Infrastructure/Extensions/EnumerableExtensions.cs @@ -1,31 +1,30 @@ -namespace Sqlbi.Bravo.Infrastructure.Extensions -{ - using System; - using System.Collections.Generic; - using System.Data; - using System.Linq; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; + +namespace Sqlbi.Bravo.Infrastructure.Extensions; - internal static class EnumerableExtensions +internal static class EnumerableExtensions +{ + public static IEnumerable<(T item, int index)> WithIndex(this IEnumerable source) { - public static IEnumerable<(T item, int index)> WithIndex(this IEnumerable source) - { - return source.Select((item, index) => (item, index)); - } + return source.Select((item, index) => (item, index)); + } - public static bool IsEmpty(this IEnumerable? source) - { - if (source is null) - return true; + public static bool IsEmpty(this IEnumerable? source) + { + if (source is null) + return true; - return !source.Any(); - } + return !source.Any(); + } - public static void ForEach(this IEnumerable source, Action action) + public static void ForEach(this IEnumerable source, Action action) + { + foreach (T item in source) { - foreach (T item in source) - { - action(item); - } + action(item); } } } diff --git a/src/Infrastructure/Extensions/FileDialogExtensions.cs b/src/Infrastructure/Extensions/FileDialogExtensions.cs index 7a6bacac..f46654bd 100644 --- a/src/Infrastructure/Extensions/FileDialogExtensions.cs +++ b/src/Infrastructure/Extensions/FileDialogExtensions.cs @@ -1,7 +1,7 @@ -namespace Sqlbi.Bravo.Infrastructure.Extensions; - +using System.Windows.Forms; using Sqlbi.Bravo.Infrastructure.Helpers; -using System.Windows.Forms; + +namespace Sqlbi.Bravo.Infrastructure.Extensions; internal static class FileDialogExtensions { diff --git a/src/Infrastructure/Extensions/HostingExtensions.cs b/src/Infrastructure/Extensions/HostingExtensions.cs index 9d343a46..ebc91b47 100644 --- a/src/Infrastructure/Extensions/HostingExtensions.cs +++ b/src/Infrastructure/Extensions/HostingExtensions.cs @@ -1,262 +1,261 @@ -namespace Sqlbi.Bravo.Infrastructure.Extensions +using System; +using System.Diagnostics; +using System.IO; +using System.Net.Sockets; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using Hellang.Middleware.ProblemDetails; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Identity.Client; +using Sqlbi.Bravo.Infrastructure.Authentication; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Telemetry; +using Sqlbi.Bravo.Models; +using AMO = Microsoft.AnalysisServices; + +namespace Sqlbi.Bravo.Infrastructure.Extensions; + +internal static class HostingExtensions { - using Hellang.Middleware.ProblemDetails; - using Microsoft.AspNetCore.Authentication; - using Microsoft.AspNetCore.Hosting; - using Microsoft.AspNetCore.Http; - using Microsoft.AspNetCore.Mvc; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.DependencyInjection.Extensions; - using Microsoft.Extensions.Options; - using Microsoft.Identity.Client; - using Sqlbi.Bravo.Infrastructure.Authentication; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Telemetry; - using Sqlbi.Bravo.Models; - using System.Net.Sockets; - using System.Reflection; - using System.Text.Json; - using System.Text.Json.Serialization; - using AMO = Microsoft.AnalysisServices; - - internal static class HostingExtensions + public static IMvcBuilder AddAndConfigureControllers(this IServiceCollection services) { - public static IMvcBuilder AddAndConfigureControllers(this IServiceCollection services) + var mvcBuilder = services.AddControllers(); + + mvcBuilder.AddJsonOptions((jsonOptions) => { - var mvcBuilder = services.AddControllers(); + jsonOptions.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + }); - mvcBuilder.AddJsonOptions((jsonOptions) => - { - jsonOptions.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; - }); + mvcBuilder.ConfigureApiBehaviorOptions((apiOptions) => + { + var defaultFactory = apiOptions.InvalidModelStateResponseFactory; - mvcBuilder.ConfigureApiBehaviorOptions((apiOptions) => + apiOptions.InvalidModelStateResponseFactory = (context) => { - var defaultFactory = apiOptions.InvalidModelStateResponseFactory; + var content = JsonSerializer.Serialize(new ValidationProblemDetails(context.ModelState)); + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(MvcCoreMvcBuilderExtensions.ConfigureApiBehaviorOptions)}.{nameof(apiOptions.InvalidModelStateResponseFactory)}", content, DiagnosticMessageSeverity.Error); - apiOptions.InvalidModelStateResponseFactory = (context) => - { - var content = JsonSerializer.Serialize(new ValidationProblemDetails(context.ModelState)); - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{ nameof(MvcCoreMvcBuilderExtensions.ConfigureApiBehaviorOptions) }.{ nameof(apiOptions.InvalidModelStateResponseFactory) }", content, DiagnosticMessageSeverity.Error); + // Invoke the defaultFactory delegate to preserve the default behavior + return defaultFactory(context); + }; + }); - // Invoke the defaultFactory delegate to preserve the default behavior - return defaultFactory(context); - }; - }); + return mvcBuilder; + } - return mvcBuilder; - } + public static IServiceCollection AddAndConfigureAuthorization(this IServiceCollection services) + { + services.AddAuthorization((options) => + { + // Use the default policy since there is no need to use a custom policy + + //options.AddPolicy(policyName, (builder) => + //{ + // builder.RequireAssertion((context) => + // { + // if (context.Resource is HttpContext httpContext) + // { + // if (httpContext.Request.Headers.TryGetValue(HeaderNames.Authorization, out var token)) + // { + // return AppConstants.ApiAuthenticationToken.Equals(token); + // } + + // //var controller = httpContext.GetEndpoint()?.Metadata.GetMetadata(); + // //if (controller is not null) + // //{ + // //} + // } + + // return false; + // }); + //}); + }); + + return services; + } + + public static IServiceCollection AddAndConfigureAuthentication(this IServiceCollection services) + { + // AspNetCore data protection errors (from telemetry) + // -- + // System.Security.Cryptography.CryptographicException: An exception occurred while trying to decrypt the element. + // System.Security.Cryptography.CryptographicException: Key {KeyId:B} is ineligible to be the default key because its {MethodName} method failed. + // System.Security.Cryptography.CryptographicException: An exception occurred while processing the key element '{Element}'. + // --- + + // We are using a custom authentication scheme that doesn't need data protection APIs. + // Currently it seems to be not possible to add authentication without adding data protection services + // This can be worked around by replicating the code from AddAuthentication() without the call to AddDataProtection() + // See https://github.com/dotnet/aspnetcore/issues/43624 + + // var builder = services.AddAuthentication(defaultScheme: AppEnvironment.ApiAuthenticationSchema); + + services.AddAuthenticationCore(); + //services.AddDataProtection(); + services.AddWebEncoders(); + services.TryAddSingleton(TimeProvider.System); + + var builder = new AuthenticationBuilder(services); + services.Configure((options) => options.DefaultScheme = AppEnvironment.ApiAuthenticationSchema); + builder.AddScheme(AppEnvironment.ApiAuthenticationSchema, (options) => options.Validate()); + + return services; + } - public static IServiceCollection AddAndConfigureAuthorization(this IServiceCollection services) + public static IServiceCollection AddAndConfigureCors(this IServiceCollection services) + { + services.AddCors((options) => { - services.AddAuthorization((options) => + options.AddDefaultPolicy((policy) => { - // Use the default policy since there is no need to use a custom policy - - //options.AddPolicy(policyName, (builder) => - //{ - // builder.RequireAssertion((context) => - // { - // if (context.Resource is HttpContext httpContext) - // { - // if (httpContext.Request.Headers.TryGetValue(HeaderNames.Authorization, out var token)) - // { - // return AppConstants.ApiAuthenticationToken.Equals(token); - // } - - // //var controller = httpContext.GetEndpoint()?.Metadata.GetMetadata(); - // //if (controller is not null) - // //{ - // //} - // } - - // return false; - // }); - //}); + policy.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin(); }); + }); - return services; - } + return services; + } - public static IServiceCollection AddAndConfigureAuthentication(this IServiceCollection services) + public static IServiceCollection AddAndConfigureProblemDetails(this IServiceCollection services) + { + services.AddProblemDetails((options) => { - // AspNetCore data protection errors (from telemetry) - // -- - // System.Security.Cryptography.CryptographicException: An exception occurred while trying to decrypt the element. - // System.Security.Cryptography.CryptographicException: Key {KeyId:B} is ineligible to be the default key because its {MethodName} method failed. - // System.Security.Cryptography.CryptographicException: An exception occurred while processing the key element '{Element}'. - // --- + // We include the details of the exception so that the UI can dispaly it and the user can view/copy/paste the stack trace of the exception + options.IncludeExceptionDetails = (context, exception) => true; - // We are using a custom authentication scheme that doesn't need data protection APIs. - // Currently it seems to be not possible to add authentication without adding data protection services - // This can be worked around by replicating the code from AddAuthentication() without the call to AddDataProtection() - // See https://github.com/dotnet/aspnetcore/issues/43624 + options.Map((context, exception) => + { + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(name: nameof(BravoException), exception); - // var builder = services.AddAuthentication(defaultScheme: AppEnvironment.ApiAuthenticationSchema); + context.RequestServices.GetRequiredService().TrackException(exception); - services.AddAuthenticationCore(); - //services.AddDataProtection(); - services.AddWebEncoders(); - services.TryAddSingleton(TimeProvider.System); + var problemDetailsFactory = context.RequestServices.GetRequiredService(); + var problemDetails = problemDetailsFactory.CreateProblemDetails(context, + statusCode: StatusCodes.Status400BadRequest, + detail: exception.ProblemDetail, + instance: exception.ProblemInstance); - var builder = new AuthenticationBuilder(services); - services.Configure((options) => options.DefaultScheme = AppEnvironment.ApiAuthenticationSchema); - builder.AddScheme(AppEnvironment.ApiAuthenticationSchema, (options) => options.Validate()); + return problemDetails; + }); - return services; - } + options.Map((context, exception) => + { + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(name: nameof(MsalException), exception); - public static IServiceCollection AddAndConfigureCors(this IServiceCollection services) - { - services.AddCors((options) => + context.RequestServices.GetRequiredService().TrackException(exception); + + var problemDetailsFactory = context.RequestServices.GetRequiredService(); + var problemDetails = problemDetailsFactory.CreateProblemDetails(context, + statusCode: StatusCodes.Status400BadRequest, + detail: exception.ErrorCode, + instance: $"{(int)BravoProblem.SignInMsalExceptionOccurred}"); + + return problemDetails; + }); + + options.Map((context, exception) => exception.IsOrHasInner(), mapping: (context, exception) => { - options.AddDefaultPolicy((policy) => - { - policy.AllowAnyMethod().AllowAnyHeader().AllowAnyOrigin(); - }); + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(name: $"{nameof(AMO.AsClientException)}::{nameof(MsalException)}", exception); + + context.RequestServices.GetRequiredService().TrackException(exception); + + var msalException = exception.Find(); BravoUnexpectedException.ThrowIfNull(msalException); + var problemDetailsFactory = context.RequestServices.GetRequiredService(); + var problemDetails = problemDetailsFactory.CreateProblemDetails(context, + statusCode: StatusCodes.Status400BadRequest, + detail: msalException.ErrorCode, + instance: $"{(int)BravoProblem.SignInMsalExceptionOccurred}"); + + return problemDetails; }); - return services; - } + options.Map((context, exception) => + { + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(name: nameof(AMO.ConnectionException), exception); - public static IServiceCollection AddAndConfigureProblemDetails(this IServiceCollection services) - { - services.AddProblemDetails((options) => + context.RequestServices.GetRequiredService().TrackException(exception); + + var problemDetailsFactory = context.RequestServices.GetRequiredService(); + var problemDetails = problemDetailsFactory.CreateProblemDetails(context, + statusCode: StatusCodes.Status400BadRequest, + detail: exception.Message, + instance: $"{(int)BravoProblem.AnalysisServicesConnectionFailed}"); + + return problemDetails; + }); + + options.Map((context, exception) => + { + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(name: nameof(OperationCanceledException), exception); + + context.RequestServices.GetRequiredService().TrackException(exception); + + var problemDetailsFactory = context.RequestServices.GetRequiredService(); + var problemDetails = problemDetailsFactory.CreateProblemDetails(context, + statusCode: StatusCodes.Status400BadRequest, + detail: exception.Message, + instance: $"{(int)BravoProblem.OperationCancelled}"); + + return problemDetails; + }); + + // Do not change the Map<> order, this is because a SocketException can exists as an inner exception also for other types of exceptions (i.e. AMO.ConnectionException) + // This can result in a misleading message like a AMO.ConnectionException reported as a SocketException (NetworkError instead of AnalysisServicesConnectionFailed) + options.Map(predicate: (context, exception) => exception.IsOrHasInner(), mapping: (context, exception) => { - // We include the details of the exception so that the UI can dispaly it and the user can view/copy/paste the stack trace of the exception - options.IncludeExceptionDetails = (context, exception) => true; + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(name: $"{nameof(Exception)}::{nameof(SocketException)}", exception); - options.Map((context, exception) => - { - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(name: nameof(BravoException), exception); - - context.RequestServices.GetRequiredService().TrackException(exception); + context.RequestServices.GetRequiredService().TrackException(exception); - var problemDetailsFactory = context.RequestServices.GetRequiredService(); - var problemDetails = problemDetailsFactory.CreateProblemDetails(context, - statusCode: StatusCodes.Status400BadRequest, - detail: exception.ProblemDetail, - instance: exception.ProblemInstance); - - return problemDetails; - }); - - options.Map((context, exception) => - { - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(name: nameof(MsalException), exception); - - context.RequestServices.GetRequiredService().TrackException(exception); - - var problemDetailsFactory = context.RequestServices.GetRequiredService(); - var problemDetails = problemDetailsFactory.CreateProblemDetails(context, - statusCode: StatusCodes.Status400BadRequest, - detail: exception.ErrorCode, - instance: $"{ (int)BravoProblem.SignInMsalExceptionOccurred }"); - - return problemDetails; - }); - - options.Map((context, exception) => exception.IsOrHasInner(), mapping: (context, exception) => - { - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(name: $"{nameof(AMO.AsClientException)}::{nameof(MsalException)}", exception); - - context.RequestServices.GetRequiredService().TrackException(exception); - - var msalException = exception.Find(); BravoUnexpectedException.ThrowIfNull(msalException); - var problemDetailsFactory = context.RequestServices.GetRequiredService(); - var problemDetails = problemDetailsFactory.CreateProblemDetails(context, - statusCode: StatusCodes.Status400BadRequest, - detail: msalException.ErrorCode, - instance: $"{ (int)BravoProblem.SignInMsalExceptionOccurred }"); - - return problemDetails; - }); - - options.Map((context, exception) => - { - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(name: nameof(AMO.ConnectionException), exception); - - context.RequestServices.GetRequiredService().TrackException(exception); - - var problemDetailsFactory = context.RequestServices.GetRequiredService(); - var problemDetails = problemDetailsFactory.CreateProblemDetails(context, - statusCode: StatusCodes.Status400BadRequest, - detail: exception.Message, - instance: $"{ (int)BravoProblem.AnalysisServicesConnectionFailed }"); - - return problemDetails; - }); - - options.Map((context, exception) => - { - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(name: nameof(OperationCanceledException), exception); - - context.RequestServices.GetRequiredService().TrackException(exception); - - var problemDetailsFactory = context.RequestServices.GetRequiredService(); - var problemDetails = problemDetailsFactory.CreateProblemDetails(context, - statusCode: StatusCodes.Status400BadRequest, - detail: exception.Message, - instance: $"{ (int)BravoProblem.OperationCancelled }"); - - return problemDetails; - }); - - // Do not change the Map<> order, this is because a SocketException can exists as an inner exception also for other types of exceptions (i.e. AMO.ConnectionException) - // This can result in a misleading message like a AMO.ConnectionException reported as a SocketException (NetworkError instead of AnalysisServicesConnectionFailed) - options.Map(predicate: (context, exception) => exception.IsOrHasInner(), mapping: (context, exception) => - { - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(name: $"{nameof(Exception)}::{nameof(SocketException)}", exception); - - context.RequestServices.GetRequiredService().TrackException(exception); - - var socketException = exception.Find(); BravoUnexpectedException.ThrowIfNull(socketException); - var problemDetailsFactory = context.RequestServices.GetRequiredService(); - var problemDetails = problemDetailsFactory.CreateProblemDetails(context, - statusCode: StatusCodes.Status400BadRequest, - detail: $"[{ socketException.SocketErrorCode }] { exception.Message }", - instance: $"{ (int)BravoProblem.NetworkError }"); - - return problemDetails; - }); + var socketException = exception.Find(); BravoUnexpectedException.ThrowIfNull(socketException); + var problemDetailsFactory = context.RequestServices.GetRequiredService(); + var problemDetails = problemDetailsFactory.CreateProblemDetails(context, + statusCode: StatusCodes.Status400BadRequest, + detail: $"[{socketException.SocketErrorCode}] {exception.Message}", + instance: $"{(int)BravoProblem.NetworkError}"); - // Because exceptions are handled polymorphically, this will act as a "catch all" mapping, which is why it's added last - options.Map(mapping: (context, exception) => - { - AppEnvironment.AddDiagnostics(name: "UnhandledException", exception); + return problemDetails; + }); - context.RequestServices.GetRequiredService().TrackException(exception); + // Because exceptions are handled polymorphically, this will act as a "catch all" mapping, which is why it's added last + options.Map(mapping: (context, exception) => + { + AppEnvironment.AddDiagnostics(name: "UnhandledException", exception); - return StatusCodeProblemDetails.Create(StatusCodes.Status500InternalServerError); - }); + context.RequestServices.GetRequiredService().TrackException(exception); + + return StatusCodeProblemDetails.Create(StatusCodes.Status500InternalServerError); }); + }); - return services; - } + return services; + } - public static IServiceCollection AddAndConfigureSwaggerGen(this IServiceCollection services) + public static IServiceCollection AddAndConfigureSwaggerGen(this IServiceCollection services) + { + services.AddSwaggerGen((options) => { - services.AddSwaggerGen((options) => + options.CustomSchemaIds((type) => type.ToString()); + + // Include xml comments only if we are not debugging the MSIX packaged application, this is because a wrong path is generated for xml files + if ((Debugger.IsAttached && AppEnvironment.DeploymentMode == AppDeploymentMode.Packaged) == false) { - options.CustomSchemaIds((type) => type.ToString()); - - // Include xml comments only if we are not debugging the MSIX packaged application, this is because a wrong path is generated for xml files - if ((Debugger.IsAttached && AppEnvironment.DeploymentMode == AppDeploymentMode.Packaged) == false) - { - var xmlFile = $"{ Assembly.GetExecutingAssembly().GetName().Name }.xml"; - var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); - options.IncludeXmlComments(xmlPath, includeControllerXmlComments: true); - } - }); + var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; + var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); + options.IncludeXmlComments(xmlPath, includeControllerXmlComments: true); + } + }); - return services; - } + return services; } } diff --git a/src/Infrastructure/Extensions/MsalExceptionExtensions.cs b/src/Infrastructure/Extensions/MsalExceptionExtensions.cs index 46e9a636..be9ce463 100644 --- a/src/Infrastructure/Extensions/MsalExceptionExtensions.cs +++ b/src/Infrastructure/Extensions/MsalExceptionExtensions.cs @@ -1,14 +1,13 @@ -namespace Sqlbi.Bravo.Infrastructure.Extensions -{ - using Microsoft.Identity.Client; +using Microsoft.Identity.Client; + +namespace Sqlbi.Bravo.Infrastructure.Extensions; - internal static class MsalExceptionExtensions - { - /// - /// Returns if the exception represents the user canceling - /// an interactive authentication prompt (e.g. closing the sign-in window). - /// - public static bool IsAuthenticationCanceled(this MsalException exception) - => exception.ErrorCode == MsalError.AuthenticationCanceledError; - } +internal static class MsalExceptionExtensions +{ + /// + /// Returns if the exception represents the user canceling + /// an interactive authentication prompt (e.g. closing the sign-in window). + /// + public static bool IsAuthenticationCanceled(this MsalException exception) + => exception.ErrorCode == MsalError.AuthenticationCanceledError; } diff --git a/src/Infrastructure/Extensions/ProcessExtensions.cs b/src/Infrastructure/Extensions/ProcessExtensions.cs index c7fd4e44..2840462d 100644 --- a/src/Infrastructure/Extensions/ProcessExtensions.cs +++ b/src/Infrastructure/Extensions/ProcessExtensions.cs @@ -1,131 +1,130 @@ -namespace Sqlbi.Bravo.Infrastructure.Extensions +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Management; +using System.Runtime.InteropServices; +using System.Text; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Windows.Interop; + +namespace Sqlbi.Bravo.Infrastructure.Extensions; + +internal static class ProcessExtensions { - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Windows.Interop; - using System; - using System.Collections.Generic; - using System.ComponentModel; - using System.Diagnostics; - using System.Management; - using System.Runtime.InteropServices; - using System.Text; - - internal static class ProcessExtensions + //[DebuggerStepThrough] + [Obsolete("Use WMI query")] + public static Process? InteropGetParent(this Process process) { - //[DebuggerStepThrough] - [Obsolete("Use WMI query")] - public static Process? InteropGetParent(this Process process) + Ntdll.PROCESS_BASIC_INFORMATION processInformation; + int? retval; + try { - Ntdll.PROCESS_BASIC_INFORMATION processInformation; - int? retval; - try - { - retval = Ntdll.NtQueryInformationProcess(process.Handle, Ntdll.PROCESSINFOCLASS.ProcessBasicInformation, out processInformation, processInformationLength: (uint)Marshal.SizeOf(typeof(Ntdll.PROCESS_BASIC_INFORMATION)), returnLength: out _); - } - catch (Win32Exception ex) when (ex.ErrorCode == -2147467259) // System.ComponentModel.Win32Exception {"Access is denied."} - { - return null; - } - - if (retval.Value == (int)Ntdll.NTSTATUS.STATUS_SUCCESS) - { - var parentProcessId = (int)(uint)processInformation.InheritedFromUniqueProcessId; - var parentProcess = ProcessHelper.SafeGetProcessById(parentProcessId); - - return parentProcess; - } + retval = Ntdll.NtQueryInformationProcess(process.Handle, Ntdll.PROCESSINFOCLASS.ProcessBasicInformation, out processInformation, processInformationLength: (uint)Marshal.SizeOf(typeof(Ntdll.PROCESS_BASIC_INFORMATION)), returnLength: out _); + } + catch (Win32Exception ex) when (ex.ErrorCode == -2147467259) // System.ComponentModel.Win32Exception {"Access is denied."} + { + return null; + } - // TODO: How NTSTATUS codes are translated into Win32 errors ? - // throw new Win32Exception(retval); + if (retval.Value == (int)Ntdll.NTSTATUS.STATUS_SUCCESS) + { + var parentProcessId = (int)(uint)processInformation.InheritedFromUniqueProcessId; + var parentProcess = ProcessHelper.SafeGetProcessById(parentProcessId); - return null; + return parentProcess; } - public static IEnumerable GetChildrenPIDs(this Process process, string? childProcessImageName = null) + // TODO: How NTSTATUS codes are translated into Win32 errors ? + // throw new Win32Exception(retval); + + return null; + } + + public static IEnumerable GetChildrenPIDs(this Process process, string? childProcessImageName = null) + { + // ManagementObjectSearcher.Get() raises a System.InvalidCastException when executed on the current thread, this regardless of the apartment state of the current thread (which is STA) + // + // System.InvalidCastException "Specified cast is not valid." + // at System.StubHelpers.InterfaceMarshaler.ConvertToNative(Object objSrc, IntPtr itfMT, IntPtr classMT, Int32 flags) + // at System.Management.SecuredIWbemServicesHandler.ExecQuery_(String strQueryLanguage, String strQuery, Int32 lFlags, IWbemContext pCtx, IEnumWbemClassObject& ppEnum) + // at System.Management.ManagementObjectSearcher.Get() + + var pids = new List(); + + ProcessHelper.RunOnSTAThread(GetImpl); + + return pids; + + void GetImpl() { - // ManagementObjectSearcher.Get() raises a System.InvalidCastException when executed on the current thread, this regardless of the apartment state of the current thread (which is STA) - // - // System.InvalidCastException "Specified cast is not valid." - // at System.StubHelpers.InterfaceMarshaler.ConvertToNative(Object objSrc, IntPtr itfMT, IntPtr classMT, Int32 flags) - // at System.Management.SecuredIWbemServicesHandler.ExecQuery_(String strQueryLanguage, String strQuery, Int32 lFlags, IWbemContext pCtx, IEnumWbemClassObject& ppEnum) - // at System.Management.ManagementObjectSearcher.Get() - - var pids = new List(); - - ProcessHelper.RunOnSTAThread(GetImpl); - - return pids; - - void GetImpl() - { - var queryString = $"SELECT ProcessId FROM Win32_Process WHERE ParentProcessId = { process.Id } AND SessionId = { AppEnvironment.SessionId }"; + var queryString = $"SELECT ProcessId FROM Win32_Process WHERE ParentProcessId = {process.Id} AND SessionId = {AppEnvironment.SessionId}"; - if (childProcessImageName is not null) - queryString += $" AND Name = '{ childProcessImageName }'"; + if (childProcessImageName is not null) + queryString += $" AND Name = '{childProcessImageName}'"; - using var searcher = new ManagementObjectSearcher(queryString); - using var collection = searcher.Get(); + using var searcher = new ManagementObjectSearcher(queryString); + using var collection = searcher.Get(); - foreach (var @object in collection) + foreach (var @object in collection) + { + if (@object is not null) { - if (@object is not null) - { - var processId = (int)(uint)@object.GetPropertyValue("ProcessId"); - pids.Add(processId); - } + var processId = (int)(uint)@object.GetPropertyValue("ProcessId"); + pids.Add(processId); } } } + } - public static string GetMainWindowTitle(this Process process) - { - if (process.MainWindowTitle.Length > 0) - return process.MainWindowTitle; - - var builder = new StringBuilder(capacity: 1000); + public static string GetMainWindowTitle(this Process process) + { + if (process.MainWindowTitle.Length > 0) + return process.MainWindowTitle; + + var builder = new StringBuilder(capacity: 1000); - foreach (ProcessThread thread in process.Threads) + foreach (ProcessThread thread in process.Threads) + { + User32.EnumThreadWindows(thread.Id, (hWnd, lParam) => { - User32.EnumThreadWindows(thread.Id, (hWnd, lParam) => + if (User32.IsWindowVisible(hWnd)) { - if (User32.IsWindowVisible(hWnd)) - { - User32.SendMessage(hWnd, WindowMessage.WM_GETTEXT, builder.Capacity, builder); + User32.SendMessage(hWnd, WindowMessage.WM_GETTEXT, builder.Capacity, builder); - var windowTitle = builder.ToString(); - if (windowTitle.Length > 0) - return false; - } - - return true; - }, - IntPtr.Zero); + var windowTitle = builder.ToString(); + if (windowTitle.Length > 0) + return false; + } - if (builder.Length > 0) - break; - } + return true; + }, + IntPtr.Zero); - return builder.ToString(); + if (builder.Length > 0) + break; } - public static string? GetPBIDesktopMainWindowTitle(this Process process) - { - var windowTitle = process.GetMainWindowTitle(); + return builder.ToString(); + } + + public static string? GetPBIDesktopMainWindowTitle(this Process process) + { + var windowTitle = process.GetMainWindowTitle(); - if (windowTitle.IsNullOrWhiteSpace()) - return null; // PBIDesktop process is starting and/or the SSAS instance is not yet started and/or the model is not yet fully loaded + if (windowTitle.IsNullOrWhiteSpace()) + return null; // PBIDesktop process is starting and/or the SSAS instance is not yet started and/or the model is not yet fully loaded - foreach (var suffix in AppEnvironment.PBIDesktopMainWindowTitleSuffixes) + foreach (var suffix in AppEnvironment.PBIDesktopMainWindowTitleSuffixes) + { + var index = windowTitle.LastIndexOf(suffix); + if (index >= 0) { - var index = windowTitle.LastIndexOf(suffix); - if (index >= 0) - { - windowTitle = windowTitle[..index]; - break; - } + windowTitle = windowTitle[..index]; + break; } - - return windowTitle; } + + return windowTitle; } } diff --git a/src/Infrastructure/Extensions/RegistryExtensions.cs b/src/Infrastructure/Extensions/RegistryExtensions.cs index 533c1f85..e50dea2d 100644 --- a/src/Infrastructure/Extensions/RegistryExtensions.cs +++ b/src/Infrastructure/Extensions/RegistryExtensions.cs @@ -1,56 +1,55 @@ -namespace Sqlbi.Bravo.Infrastructure.Extensions +using Microsoft.Win32; + +namespace Sqlbi.Bravo.Infrastructure.Extensions; + +internal static class RegistryExtensions { - using Microsoft.Win32; + public static bool GetBoolValue(this RegistryKey registryKey, string subkeyName, string valueName) + { + var valueInt = GetIntValue(registryKey, subkeyName, valueName); + return valueInt == 1; + } - internal static class RegistryExtensions + public static int? GetIntValue(this RegistryKey registryKey, string subkeyName, string valueName) { - public static bool GetBoolValue(this RegistryKey registryKey, string subkeyName, string valueName) + var (value, valueKind) = GetRegistryValue(registryKey, subkeyName, valueName); + + if (value is not null && valueKind == RegistryValueKind.DWord) { - var valueInt = GetIntValue(registryKey, subkeyName, valueName); - return valueInt == 1; + var valueInt = (int)value; + return valueInt; } - public static int? GetIntValue(this RegistryKey registryKey, string subkeyName, string valueName) - { - var (value, valueKind) = GetRegistryValue(registryKey, subkeyName, valueName); + return null; + } - if (value is not null && valueKind == RegistryValueKind.DWord) - { - var valueInt = (int)value; - return valueInt; - } + public static string? GetStringValue(this RegistryKey registryKey, string subkeyName, string valueName) + { + var (value, valueKind) = GetRegistryValue(registryKey, subkeyName, valueName); - return null; + if (value is not null && valueKind == RegistryValueKind.String) + { + var valueString = (string)value; + return valueString; } - public static string? GetStringValue(this RegistryKey registryKey, string subkeyName, string valueName) - { - var (value, valueKind) = GetRegistryValue(registryKey, subkeyName, valueName); - - if (value is not null && valueKind == RegistryValueKind.String) - { - var valueString = (string)value; - return valueString; - } + return null; + } - return null; - } + private static (object? Value, RegistryValueKind ValueKind) GetRegistryValue(RegistryKey registryKey, string subkeyName, string valueName) + { + using var registrySubKey = registryKey.OpenSubKey(subkeyName); - private static (object? Value, RegistryValueKind ValueKind) GetRegistryValue(RegistryKey registryKey, string subkeyName, string valueName) + if (registrySubKey is not null) { - using var registrySubKey = registryKey.OpenSubKey(subkeyName); - - if (registrySubKey is not null) + var value = registrySubKey.GetValue(valueName, defaultValue: null, RegistryValueOptions.DoNotExpandEnvironmentNames); + if (value is not null) { - var value = registrySubKey.GetValue(valueName, defaultValue: null, RegistryValueOptions.DoNotExpandEnvironmentNames); - if (value is not null) - { - var valueKind = registrySubKey.GetValueKind(valueName); - return (value, valueKind); - } + var valueKind = registrySubKey.GetValueKind(valueName); + return (value, valueKind); } - - return (null, RegistryValueKind.None); } + + return (null, RegistryValueKind.None); } -} \ No newline at end of file +} diff --git a/src/Infrastructure/Extensions/StringExtensions.cs b/src/Infrastructure/Extensions/StringExtensions.cs index 8bc6b540..1719867b 100644 --- a/src/Infrastructure/Extensions/StringExtensions.cs +++ b/src/Infrastructure/Extensions/StringExtensions.cs @@ -1,251 +1,249 @@ -namespace Sqlbi.Bravo.Infrastructure.Extensions +using System; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; +using Sqlbi.Bravo.Models.FormatDax; + +namespace Sqlbi.Bravo.Infrastructure.Extensions; + +internal static class StringExtensions { - using Sqlbi.Bravo.Models.FormatDax; - using System; - using System.Diagnostics.CodeAnalysis; - using System.Globalization; - using System.IO; - using System.Linq; - using System.Runtime.CompilerServices; - using System.Text; - using System.Text.RegularExpressions; - - internal static class StringExtensions - { - private static Regex? _invalidFileNameCharsRegex; - private static Regex? _invalidPathCharsRegex; - - public static string AppendApplicationVersion(this string value) - { - return $"{value} - v{AppEnvironment.VersionInfo.Version}"; - } - - /// - /// Convert the old .NET JavaScriptSerializer/DataContractJsonSerializer date format "/Date(1617810719887)/" to - /// - /// Json date string in format "/Date(1617810719887)/" - public static DateTimeOffset? ToDateTimeOffset(this string value) - { - var regex = new Regex("^\\/Date\\(([0-9]+)\\)\\/$"); + private static Regex? _invalidFileNameCharsRegex; + private static Regex? _invalidPathCharsRegex; - var match = regex.Match(value); - if (match.Success) - { - var seconds = long.Parse(match.Groups[1].Value); - return DateTimeOffset.FromUnixTimeMilliseconds(seconds); - } + public static string AppendApplicationVersion(this string value) + { + return $"{value} - v{AppEnvironment.VersionInfo.Version}"; + } - return null; - } + /// + /// Convert the old .NET JavaScriptSerializer/DataContractJsonSerializer date format "/Date(1617810719887)/" to + /// + /// Json date string in format "/Date(1617810719887)/" + public static DateTimeOffset? ToDateTimeOffset(this string value) + { + var regex = new Regex("^\\/Date\\(([0-9]+)\\)\\/$"); - public static string? NullIfEmpty(this string? value) + var match = regex.Match(value); + if (match.Success) { - return string.IsNullOrEmpty(value) ? null : value; + var seconds = long.Parse(match.Groups[1].Value); + return DateTimeOffset.FromUnixTimeMilliseconds(seconds); } - public static string? NullIfWhiteSpace(this string? value) - { - return string.IsNullOrWhiteSpace(value) ? null : value; - } + return null; + } - public static bool IsNullOrEmpty([NotNullWhen(false)] this string? value) - { - return string.IsNullOrEmpty(value); - } + public static string? NullIfEmpty(this string? value) + { + return string.IsNullOrEmpty(value) ? null : value; + } - public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? value) - { - return string.IsNullOrWhiteSpace(value); - } + public static string? NullIfWhiteSpace(this string? value) + { + return string.IsNullOrWhiteSpace(value) ? null : value; + } - public static bool IsEmptyOrWhiteSpace([NotNullWhen(false)] this string? value) - { - return value is not null && string.IsNullOrWhiteSpace(value); - } + public static bool IsNullOrEmpty([NotNullWhen(false)] this string? value) + { + return string.IsNullOrEmpty(value); + } - public static string FormatInvariant(this string format, params object?[] args) - { - return string.Format(CultureInfo.InvariantCulture, format, args); - } + public static bool IsNullOrWhiteSpace([NotNullWhen(false)] this string? value) + { + return string.IsNullOrWhiteSpace(value); + } - public static string ToFileDialogFilterString(this string? filter) - { - if (filter.IsNullOrWhiteSpace()) - filter = " |*.*"; - - var stringBuilder = new StringBuilder(filter); - stringBuilder.Replace('|', '\0'); - stringBuilder.Append('\0'); - stringBuilder.Append('\0'); - return stringBuilder.ToString(); - } + public static bool IsEmptyOrWhiteSpace([NotNullWhen(false)] this string? value) + { + return value is not null && string.IsNullOrWhiteSpace(value); + } - public static bool ContainsInvalidPathChars(this string path) - { - if (path is null) - return false; + public static string FormatInvariant(this string format, params object?[] args) + { + return string.Format(CultureInfo.InvariantCulture, format, args); + } - var indexOfAny = path.IndexOfAny(Path.GetInvalidPathChars()); - return indexOfAny != -1; - } + public static string ToFileDialogFilterString(this string? filter) + { + if (filter.IsNullOrWhiteSpace()) + filter = " |*.*"; + + var stringBuilder = new StringBuilder(filter); + stringBuilder.Replace('|', '\0'); + stringBuilder.Append('\0'); + stringBuilder.Append('\0'); + return stringBuilder.ToString(); + } - public static string ReplaceInvalidPathChars(this string path, string replacement = "_") - { - if (_invalidPathCharsRegex is null) - { - var pattern = Regex.Escape(new string(Path.GetInvalidPathChars())); - _invalidPathCharsRegex = new($"[{ pattern }]"); - } + public static bool ContainsInvalidPathChars(this string path) + { + if (path is null) + return false; - path = _invalidPathCharsRegex.Replace(path, replacement); - return path; - } + var indexOfAny = path.IndexOfAny(Path.GetInvalidPathChars()); + return indexOfAny != -1; + } - public static string ReplaceInvalidFileNameChars(this string path, string replacement = "_") + public static string ReplaceInvalidPathChars(this string path, string replacement = "_") + { + if (_invalidPathCharsRegex is null) { - if (_invalidFileNameCharsRegex is null) - { - // Not necessary to include GetInvalidPathChars(), the illegal file name char list contains the illegal path char list - var pattern = Regex.Escape(new string(Path.GetInvalidFileNameChars())); - _invalidFileNameCharsRegex = new($"[{ pattern }]"); - } - - path = _invalidFileNameCharsRegex.Replace(path, replacement); - return path; + var pattern = Regex.Escape(new string(Path.GetInvalidPathChars())); + _invalidPathCharsRegex = new($"[{pattern}]"); } - public static bool EqualsI(this string? current, string? value) - { - return current?.Equals(value, StringComparison.OrdinalIgnoreCase) ?? false; - } + path = _invalidPathCharsRegex.Replace(path, replacement); + return path; + } - public static bool EqualsTI(this string? current, string? value) + public static string ReplaceInvalidFileNameChars(this string path, string replacement = "_") + { + if (_invalidFileNameCharsRegex is null) { - return EqualsI(current, value?.Trim()); + // Not necessary to include GetInvalidPathChars(), the illegal file name char list contains the illegal path char list + var pattern = Regex.Escape(new string(Path.GetInvalidFileNameChars())); + _invalidFileNameCharsRegex = new($"[{pattern}]"); } - public static bool EndsWithI(this string? current, string value) - { - return current?.EndsWith(value, StringComparison.OrdinalIgnoreCase) ?? false; - } + path = _invalidFileNameCharsRegex.Replace(path, replacement); + return path; + } + + public static bool EqualsI(this string? current, string? value) + { + return current?.Equals(value, StringComparison.OrdinalIgnoreCase) ?? false; + } - public static string? GetDaxName(this string? fullyQualifiedName) + public static bool EqualsTI(this string? current, string? value) + { + return EqualsI(current, value?.Trim()); + } + + public static bool EndsWithI(this string? current, string value) + { + return current?.EndsWith(value, StringComparison.OrdinalIgnoreCase) ?? false; + } + + public static string? GetDaxName(this string? fullyQualifiedName) + { + if (fullyQualifiedName is not null) { - if (fullyQualifiedName is not null) + var firstIndex = fullyQualifiedName.IndexOf('['); + if (firstIndex != -1) { - var firstIndex = fullyQualifiedName.IndexOf('['); - if (firstIndex != -1) + var lastIndex = fullyQualifiedName.LastIndexOf(']'); + if (lastIndex != -1) { - var lastIndex = fullyQualifiedName.LastIndexOf(']'); - if (lastIndex != -1) + if (++firstIndex < lastIndex) // start of the range is inclusive, math notation is [start..end[ { - if (++firstIndex < lastIndex) // start of the range is inclusive, math notation is [start..end[ - { - var objectName = fullyQualifiedName[firstIndex..lastIndex]; - return objectName; - } + var objectName = fullyQualifiedName[firstIndex..lastIndex]; + return objectName; } } } - - return null; } - /// - /// Remove initial CR/LF or SPACE/CR/LF after the last non-empty character of the expression. - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static (string? Expression, DaxLineBreakStyle LineBreakStyle) NormalizeDax(this string? expression) - { - // Ignore minor differences in DAX Formatter https://github.com/sql-bi/Bravo/issues/86 - // We ignore differences in measures caused by initial CR/LF or spaces/CR/LF after the last non-empty character of the formula. - // This way, we do not report as "to be formatted" a measure that differs only for initial and final CR/LF/spaces - - var lineBreakStyle = DaxLineBreakStyle.None; - - if (expression?.Length > 0) - { - // Replace all occurrences of CRLF with LF since this is the default EOL character in SSAS - expression = expression.Replace("\r\n", "\n"); + return null; + } - if (!expression.StartsWith("\n\n")) - { - // remove a single EOL leading character, if any - if (expression.Length > 0 && expression[0] == '\n') - { - expression = expression[1..]; - lineBreakStyle = DaxLineBreakStyle.InitialLineBreak; - } - } + /// + /// Remove initial CR/LF or SPACE/CR/LF after the last non-empty character of the expression. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static (string? Expression, DaxLineBreakStyle LineBreakStyle) NormalizeDax(this string? expression) + { + // Ignore minor differences in DAX Formatter https://github.com/sql-bi/Bravo/issues/86 + // We ignore differences in measures caused by initial CR/LF or spaces/CR/LF after the last non-empty character of the formula. + // This way, we do not report as "to be formatted" a measure that differs only for initial and final CR/LF/spaces - //expression = expression.TrimEnd('\n', ' '); + var lineBreakStyle = DaxLineBreakStyle.None; - if (!expression.EndsWith("\n\n")) - { - // remove a single EOL trailing character, if any - if (expression.Length > 0 && expression[^1] == '\n') - expression = expression[..^1]; - } + if (expression?.Length > 0) + { + // Replace all occurrences of CRLF with LF since this is the default EOL character in SSAS + expression = expression.Replace("\r\n", "\n"); - if (!expression.EndsWith(" ")) + if (!expression.StartsWith("\n\n")) + { + // remove a single EOL leading character, if any + if (expression.Length > 0 && expression[0] == '\n') { - // remove a single SPACE trailing character, if any - if (expression.Length > 0 && expression[^1] == ' ') - expression = expression[..^1]; + expression = expression[1..]; + lineBreakStyle = DaxLineBreakStyle.InitialLineBreak; } } - return (expression, lineBreakStyle); + //expression = expression.TrimEnd('\n', ' '); + + if (!expression.EndsWith("\n\n")) + { + // remove a single EOL trailing character, if any + if (expression.Length > 0 && expression[^1] == '\n') + expression = expression[..^1]; + } + + if (!expression.EndsWith(" ")) + { + // remove a single SPACE trailing character, if any + if (expression.Length > 0 && expression[^1] == ' ') + expression = expression[..^1]; + } } - public static bool IsAutoDateTimePrivateTableName(this string? tableName) + return (expression, lineBreakStyle); + } + + public static bool IsAutoDateTimePrivateTableName(this string? tableName) + { + if (tableName is not null) { - if (tableName is not null) + var localTableIndex = tableName.IndexOf("LocalDateTable_"); + if (localTableIndex == 0) { - var localTableIndex = tableName.IndexOf("LocalDateTable_"); - if (localTableIndex == 0) - { - var guidString = tableName.Remove(localTableIndex, "LocalDateTable_".Length); - var isGuid = Guid.TryParse(guidString, out _); - return isGuid; - } - - var templateTableIndex = tableName.IndexOf("DateTableTemplate_"); - if (templateTableIndex == 0) - { - var guidString = tableName.Remove(templateTableIndex, "DateTableTemplate_".Length); - var isGuid = Guid.TryParse(guidString, out _); - return isGuid; - } + var guidString = tableName.Remove(localTableIndex, "LocalDateTable_".Length); + var isGuid = Guid.TryParse(guidString, out _); + return isGuid; } - return false; + var templateTableIndex = tableName.IndexOf("DateTableTemplate_"); + if (templateTableIndex == 0) + { + var guidString = tableName.Remove(templateTableIndex, "DateTableTemplate_".Length); + var isGuid = Guid.TryParse(guidString, out _); + return isGuid; + } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static string? ApplyLineBreakStyle(this string? expression, DaxLineBreakStyle lineBreakStyle) + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string? ApplyLineBreakStyle(this string? expression, DaxLineBreakStyle lineBreakStyle) + { + if (expression?.Length > 0) { - if (expression?.Length > 0) + switch (lineBreakStyle) { - switch (lineBreakStyle) + case DaxLineBreakStyle.None: + { + if (expression[0] == '\n') + expression = expression[1..]; + } + break; + case DaxLineBreakStyle.InitialLineBreak: { - case DaxLineBreakStyle.None: - { - if (expression[0] == '\n') - expression = expression[1..]; - } - break; - case DaxLineBreakStyle.InitialLineBreak: - { - expression = '\n' + expression; - } - break; - default: - throw new BravoUnexpectedInvalidOperationException($"Unhandled { nameof(DaxLineBreakStyle) } value ({ lineBreakStyle })"); + expression = '\n' + expression; } + break; + default: + throw new BravoUnexpectedInvalidOperationException($"Unhandled {nameof(DaxLineBreakStyle)} value ({lineBreakStyle})"); } - - return expression; } + + return expression; } } diff --git a/src/Infrastructure/Extensions/TabularModelExtensions.cs b/src/Infrastructure/Extensions/TabularModelExtensions.cs index c44d7287..349d22ec 100644 --- a/src/Infrastructure/Extensions/TabularModelExtensions.cs +++ b/src/Infrastructure/Extensions/TabularModelExtensions.cs @@ -1,219 +1,218 @@ -namespace Sqlbi.Bravo.Infrastructure.Extensions -{ - using Microsoft.AnalysisServices; - using Sqlbi.Bravo.Infrastructure.Helpers; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Text.Json.Nodes; - using TOM = Microsoft.AnalysisServices.Tabular; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Nodes; +using Microsoft.AnalysisServices; +using Sqlbi.Bravo.Infrastructure.Helpers; +using TOM = Microsoft.AnalysisServices.Tabular; + +namespace Sqlbi.Bravo.Infrastructure.Extensions; - internal static class ModelOperationResultExtensions +internal static class ModelOperationResultExtensions +{ + public static string? ToMessageString(this TOM.ModelOperationResult operationResult) { - public static string? ToMessageString(this TOM.ModelOperationResult operationResult) - { - var xmlaResults = operationResult.XmlaResults; - if (xmlaResults is null) - return null; + var xmlaResults = operationResult.XmlaResults; + if (xmlaResults is null) + return null; - var descriptions = xmlaResults.OfType() - .SelectMany((r) => r.Messages.OfType()) - .Select((m) => m.Description) - .ToArray(); + var descriptions = xmlaResults.OfType() + .SelectMany((r) => r.Messages.OfType()) + .Select((m) => m.Description) + .ToArray(); - return string.Join(Environment.NewLine, descriptions); - } + return string.Join(Environment.NewLine, descriptions); + } - public static void ThrowOnError(this TOM.ModelOperationResult operationResult) + public static void ThrowOnError(this TOM.ModelOperationResult operationResult) + { + if (operationResult.XmlaResults?.ContainsErrors == true) { - if (operationResult.XmlaResults?.ContainsErrors == true) - { - var message = operationResult.ToMessageString(); - throw new BravoException(BravoProblem.TOMDatabaseUpdateFailed, message!); - } + var message = operationResult.ToMessageString(); + throw new BravoException(BravoProblem.TOMDatabaseUpdateFailed, message!); } } +} - internal static class TabularExtension +internal static class TabularExtension +{ + public static bool IsQueryable(this TOM.ObjectState state) { - public static bool IsQueryable(this TOM.ObjectState state) - { - return state == TOM.ObjectState.Ready || state == TOM.ObjectState.NoData; - } + return state == TOM.ObjectState.Ready || state == TOM.ObjectState.NoData; } +} - internal static class ColumnExtension +internal static class ColumnExtension +{ + public static bool IsQueryable(this TOM.Column column) { - public static bool IsQueryable(this TOM.Column column) - { - var isQueryable = column.State.IsQueryable(); - return isQueryable; - } + var isQueryable = column.State.IsQueryable(); + return isQueryable; } +} - internal static class TableExtension +internal static class TableExtension +{ + public static bool IsQueryable(this TOM.Table table) { - public static bool IsQueryable(this TOM.Table table) - { - var isQueryable = table.Columns.All((column) => column.IsQueryable()); - return isQueryable; - } + var isQueryable = table.Columns.All((column) => column.IsQueryable()); + return isQueryable; + } - public static TOM.PartitionSourceType GetSourceType(this TOM.Table table) - { - return table.Partitions.FirstOrDefault()?.SourceType ?? TOM.PartitionSourceType.None; - } + public static TOM.PartitionSourceType GetSourceType(this TOM.Table table) + { + return table.Partitions.FirstOrDefault()?.SourceType ?? TOM.PartitionSourceType.None; + } - public static bool IsCalculatedOrCalculationGroup(this TOM.Table table) - { - var sourceType = GetSourceType(table); - return sourceType == TOM.PartitionSourceType.Calculated || sourceType == TOM.PartitionSourceType.CalculationGroup; - } + public static bool IsCalculatedOrCalculationGroup(this TOM.Table table) + { + var sourceType = GetSourceType(table); + return sourceType == TOM.PartitionSourceType.Calculated || sourceType == TOM.PartitionSourceType.CalculationGroup; + } - public static bool IsCalculated(this TOM.Table table) - { - var sourceType = GetSourceType(table); - return sourceType == TOM.PartitionSourceType.Calculated; - } + public static bool IsCalculated(this TOM.Table table) + { + var sourceType = GetSourceType(table); + return sourceType == TOM.PartitionSourceType.Calculated; + } - public static bool IsImported(this TOM.Table table) - { - var sourceType = GetSourceType(table); - return sourceType == TOM.PartitionSourceType.M || sourceType == TOM.PartitionSourceType.Query || sourceType == TOM.PartitionSourceType.PolicyRange; - } + public static bool IsImported(this TOM.Table table) + { + var sourceType = GetSourceType(table); + return sourceType == TOM.PartitionSourceType.M || sourceType == TOM.PartitionSourceType.Query || sourceType == TOM.PartitionSourceType.PolicyRange; } +} - internal static class TableCollectionExtensions +internal static class TableCollectionExtensions +{ + public static IEnumerable FindByAnnotation(this IEnumerable tables, string annotationName, string annotationValue) { - public static IEnumerable FindByAnnotation(this IEnumerable tables, string annotationName, string annotationValue) + foreach (var table in tables) { - foreach (var table in tables) + var annotation = table.Annotations.Find(annotationName); + if (annotation?.Value.Equals(annotationValue) == true) { - var annotation = table.Annotations.Find(annotationName); - if (annotation?.Value.Equals(annotationValue) == true) - { - yield return table; - } + yield return table; } } + } - public static TOM.Measure? FindMeasure(this TOM.TableCollection tables, string tableName, string measureName) + public static TOM.Measure? FindMeasure(this TOM.TableCollection tables, string tableName, string measureName) + { + var table = tables.Find(tableName); + if (table is not null) { - var table = tables.Find(tableName); - if (table is not null) - { - var measure = table.Measures.Find(measureName); - return measure; - } - - return null; + var measure = table.Measures.Find(measureName); + return measure; } + + return null; } +} - internal static class DatabaseExtensions +internal static class DatabaseExtensions +{ + public static string? GetETag(this TOM.Database database, bool refresh = true) { - public static string? GetETag(this TOM.Database database, bool refresh = true) + if (refresh) { - if (refresh) - { - database.Refresh(full: false); - } - - var etag = TabularModelHelper.GetDatabaseETag(database.Name, database.Version, database.LastUpdate); - return etag; + database.Refresh(full: false); } + + var etag = TabularModelHelper.GetDatabaseETag(database.Name, database.Version, database.LastUpdate); + return etag; } +} - internal static class ServerExtension +internal static class ServerExtension +{ + public static bool IsPowerBIDesktop(this TOM.Server server) { - public static bool IsPowerBIDesktop(this TOM.Server server) + if (server.IsPowerBIOnPremis()) { - if (server.IsPowerBIOnPremis()) - { - return server.ServerMode == ServerMode.SharePoint; - } - - return false; + return server.ServerMode == ServerMode.SharePoint; } - public static bool IsPowerBIDesktopReportServer(this TOM.Server server) - { - if (server.IsPowerBIOnPremis()) - { - return server.ServerMode != ServerMode.SharePoint; - } + return false; + } - return false; + public static bool IsPowerBIDesktopReportServer(this TOM.Server server) + { + if (server.IsPowerBIOnPremis()) + { + return server.ServerMode != ServerMode.SharePoint; } - public static bool IsPowerBIService(this TOM.Server server) - { - if (server.CompatibilityMode == CompatibilityMode.PowerBI) - { - return server.ServerLocation == ServerLocation.Azure; - } + return false; + } - return false; + public static bool IsPowerBIService(this TOM.Server server) + { + if (server.CompatibilityMode == CompatibilityMode.PowerBI) + { + return server.ServerLocation == ServerLocation.Azure; } - public static bool IsPowerBIOnPremis(this TOM.Server server) - { - if (server.CompatibilityMode == CompatibilityMode.PowerBI) - { - return server.ServerLocation == ServerLocation.OnPremise; - } + return false; + } - return false; + public static bool IsPowerBIOnPremis(this TOM.Server server) + { + if (server.CompatibilityMode == CompatibilityMode.PowerBI) + { + return server.ServerLocation == ServerLocation.OnPremise; } - public static bool IsSQLServerAnalisysServices(this TOM.Server server) - { - if (server.CompatibilityMode == CompatibilityMode.AnalysisServices) - { - return server.ServerLocation == ServerLocation.OnPremise; - } + return false; + } - return false; + public static bool IsSQLServerAnalisysServices(this TOM.Server server) + { + if (server.CompatibilityMode == CompatibilityMode.AnalysisServices) + { + return server.ServerLocation == ServerLocation.OnPremise; } - public static bool IsAzureAnalisysServices(this TOM.Server server) - { - if (server.CompatibilityMode == CompatibilityMode.AnalysisServices) - { - return server.ServerLocation == ServerLocation.Azure; - } + return false; + } - return false; + public static bool IsAzureAnalisysServices(this TOM.Server server) + { + if (server.CompatibilityMode == CompatibilityMode.AnalysisServices) + { + return server.ServerLocation == ServerLocation.Azure; } - public static JsonObject SerializeDiagnosticProperties(this TOM.Server server) - { - //var options = new TOM.SerializeOptions - //{ - // IgnoreChildren = true, - // IgnoreTimestamps = true, - // IgnoreInferredObjects = true, - // IgnoreInferredProperties = true, - //}; + return false; + } - var databases = new JsonObject(); + public static JsonObject SerializeDiagnosticProperties(this TOM.Server server) + { + //var options = new TOM.SerializeOptions + //{ + // IgnoreChildren = true, + // IgnoreTimestamps = true, + // IgnoreInferredObjects = true, + // IgnoreInferredProperties = true, + //}; - foreach (TOM.Database database in server.Databases) - { - //var jsonString = TOM.JsonSerializer.SerializeDatabase(database, options); - //var jsonNode = JsonNode.Parse(jsonString); + var databases = new JsonObject(); - var properties = new JsonObject(new[] - { - KeyValuePair.Create(nameof(database.ID), database.ID), - KeyValuePair.Create(nameof(database.Name), database.Name), - KeyValuePair.Create(nameof(database.CompatibilityLevel), database.CompatibilityLevel), - }); + foreach (TOM.Database database in server.Databases) + { + //var jsonString = TOM.JsonSerializer.SerializeDatabase(database, options); + //var jsonNode = JsonNode.Parse(jsonString); - databases.Add(database.ID, properties); - } + var properties = new JsonObject(new[] + { + KeyValuePair.Create(nameof(database.ID), database.ID), + KeyValuePair.Create(nameof(database.Name), database.Name), + KeyValuePair.Create(nameof(database.CompatibilityLevel), database.CompatibilityLevel), + }); - return databases; + databases.Add(database.ID, properties); } + + return databases; } } diff --git a/src/Infrastructure/Helpers/CommonHelper.cs b/src/Infrastructure/Helpers/CommonHelper.cs index 078dbf20..314124b2 100644 --- a/src/Infrastructure/Helpers/CommonHelper.cs +++ b/src/Infrastructure/Helpers/CommonHelper.cs @@ -1,165 +1,164 @@ -namespace Sqlbi.Bravo.Infrastructure.Helpers +using System; +using System.IO; +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using Sqlbi.Bravo.Infrastructure.Configuration.Settings; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Windows.Interop; +using Sqlbi.Bravo.Models; + +namespace Sqlbi.Bravo.Infrastructure.Helpers; + +internal static class CommonHelper { - using Sqlbi.Bravo.Infrastructure.Configuration.Settings; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Windows.Interop; - using Sqlbi.Bravo.Models; - using System; - using System.IO; - using System.Net.Http; - using System.Text.Json; - using System.Threading; - using System.Threading.Tasks; - using System.Windows.Forms; - - internal static class CommonHelper + public static string? ChangeUriScheme(string? uriString, string scheme, bool ignorePort = false) { - public static string? ChangeUriScheme(string? uriString, string scheme, bool ignorePort = false) + if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri)) { - if (Uri.TryCreate(uriString, UriKind.Absolute, out var uri)) + var uriBuilder = new UriBuilder(uri) { - var uriBuilder = new UriBuilder(uri) - { - Scheme = scheme - }; - - if (ignorePort) - { - uriBuilder.Port = -1; - } - - return uriBuilder.Uri.AbsoluteUri; + Scheme = scheme + }; + + if (ignorePort) + { + uriBuilder.Port = -1; } - return null; + return uriBuilder.Uri.AbsoluteUri; } - public static User32.KeyState GetKeyState(Keys key) + return null; + } + + public static User32.KeyState GetKeyState(Keys key) + { + var state = User32.KeyState.None; { - var state = User32.KeyState.None; - { - var retval = User32.GetKeyState((int)key); + var retval = User32.GetKeyState((int)key); - if ((retval & 0x8000) == 0x8000) - state |= User32.KeyState.Down; + if ((retval & 0x8000) == 0x8000) + state |= User32.KeyState.Down; - if ((retval & 1) == 1) - state |= User32.KeyState.Toggled; - } - return state; + if ((retval & 1) == 1) + state |= User32.KeyState.Toggled; } + return state; + } - public static bool IsKeyDown(Keys key) - { - var state = GetKeyState(key); + public static bool IsKeyDown(Keys key) + { + var state = GetKeyState(key); - return state.HasFlag(User32.KeyState.Down); - } + return state.HasFlag(User32.KeyState.Down); + } - public static bool AreDirectoryPathsEqual(string path1, string path2) - { - var normalizedPath1 = NormalizeDirectoryPath(path1); - var normalizedPath2 = NormalizeDirectoryPath(path2); + public static bool AreDirectoryPathsEqual(string path1, string path2) + { + var normalizedPath1 = NormalizeDirectoryPath(path1); + var normalizedPath2 = NormalizeDirectoryPath(path2); - var equals = normalizedPath1.EqualsI(normalizedPath2); - return equals; - } + var equals = normalizedPath1.EqualsI(normalizedPath2); + return equals; + } - public static string NormalizeDirectoryPath(string path) - { - var normalizedPath = path; + public static string NormalizeDirectoryPath(string path) + { + var normalizedPath = path; - normalizedPath = normalizedPath.Trim(); - normalizedPath = Path.TrimEndingDirectorySeparator(normalizedPath); - normalizedPath = new DirectoryInfo(normalizedPath).FullName; + normalizedPath = normalizedPath.Trim(); + normalizedPath = Path.TrimEndingDirectorySeparator(normalizedPath); + normalizedPath = new DirectoryInfo(normalizedPath).FullName; - return normalizedPath; - } + return normalizedPath; + } - public static string NormalizeUriString(string uriString) - { - var uri = new Uri(uriString, UriKind.Absolute); - return uri.AbsoluteUri; - } + public static string NormalizeUriString(string uriString) + { + var uri = new Uri(uriString, UriKind.Absolute); + return uri.AbsoluteUri; + } - public static string? GetFileRelativePath(string relativeTo, string filePath) - { - var relativePath = Path.GetRelativePath(relativeTo, filePath); - var directoryName = Path.GetDirectoryName(relativePath); + public static string? GetFileRelativePath(string relativeTo, string filePath) + { + var relativePath = Path.GetRelativePath(relativeTo, filePath); + var directoryName = Path.GetDirectoryName(relativePath); - return directoryName; - } + return directoryName; + } - public async static Task CheckForUpdateAsync(UpdateChannelType updateChannel, CancellationToken cancellationToken) + public async static Task CheckForUpdateAsync(UpdateChannelType updateChannel, CancellationToken cancellationToken) + { + var channelPath = updateChannel switch { - var channelPath = updateChannel switch - { - UpdateChannelType.Stable => "bravo-public", - UpdateChannelType.Dev => "bravo-internal", - _ => throw new BravoUnexpectedInvalidOperationException($"Unhandled { nameof(UpdateChannelType) } value ({ updateChannel })") - }; + UpdateChannelType.Stable => "bravo-public", + UpdateChannelType.Dev => "bravo-internal", + _ => throw new BravoUnexpectedInvalidOperationException($"Unhandled {nameof(UpdateChannelType)} value ({updateChannel})") + }; - using var httpClient = new HttpClient(); - var requestUri = $"https://bravorelease.blob.core.windows.net/{ channelPath }/currentversion.json?nocache={ DateTimeOffset.Now.ToUnixTimeSeconds() }"; - var json = await httpClient.GetStringAsync(requestUri, cancellationToken).ConfigureAwait(false); + using var httpClient = new HttpClient(); + var requestUri = $"https://bravorelease.blob.core.windows.net/{channelPath}/currentversion.json?nocache={DateTimeOffset.Now.ToUnixTimeSeconds()}"; + var json = await httpClient.GetStringAsync(requestUri, cancellationToken).ConfigureAwait(false); - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(CommonHelper)}.{nameof(CheckForUpdateAsync)}", content: json); + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(CommonHelper)}.{nameof(CheckForUpdateAsync)}", content: json); - using var document = JsonDocument.Parse(json); - var rootElement = document.RootElement; + using var document = JsonDocument.Parse(json); + var rootElement = document.RootElement; - var version = Version.Parse(rootElement.GetProperty("version").GetString()!) - .ToString(3); // Versioning is SemVer-based: discard a 4th (build) digit if present - var isNewerVersion = Version.Parse(version) > Version.Parse(AppEnvironment.VersionInfo.Version); - var downloadUrl = GetDownloadUrl(rootElement.GetProperty("download").GetString()!); - var changelogUrl = rootElement.GetProperty("changelog").GetString()!; + var version = Version.Parse(rootElement.GetProperty("version").GetString()!) + .ToString(3); // Versioning is SemVer-based: discard a 4th (build) digit if present + var isNewerVersion = Version.Parse(version) > Version.Parse(AppEnvironment.VersionInfo.Version); + var downloadUrl = GetDownloadUrl(rootElement.GetProperty("download").GetString()!); + var changelogUrl = rootElement.GetProperty("changelog").GetString()!; + + return new BravoUpdate + { + UpdateChannel = updateChannel, + IsNewerVersion = isNewerVersion, + Version = version, + DownloadUrl = downloadUrl, + ChangelogUrl = changelogUrl, + }; + + static string GetDownloadUrl(string downloadUrl) + { + BravoUnexpectedException.Assert(AppEnvironment.DeploymentMode != AppDeploymentMode.Packaged); - return new BravoUpdate + var downloadUri = new Uri(downloadUrl, UriKind.Absolute); + var downloadFileNameWithoutExtension = Path.GetFileNameWithoutExtension(downloadUri.LocalPath); + var downloadFileExtension = Path.GetExtension(downloadUri.LocalPath); + var downloadFileName = Path.GetFileName(downloadUri.LocalPath); + + if (AppEnvironment.PublishMode == AppPublishMode.FrameworkDependent) { - UpdateChannel = updateChannel, - IsNewerVersion = isNewerVersion, - Version = version, - DownloadUrl = downloadUrl, - ChangelogUrl = changelogUrl, - }; + downloadFileNameWithoutExtension += "-frameworkdependent"; + } - static string GetDownloadUrl(string downloadUrl) + if (AppEnvironment.DeploymentMode == AppDeploymentMode.PerMachine) { - BravoUnexpectedException.Assert(AppEnvironment.DeploymentMode != AppDeploymentMode.Packaged); - - var downloadUri = new Uri(downloadUrl, UriKind.Absolute); - var downloadFileNameWithoutExtension = Path.GetFileNameWithoutExtension(downloadUri.LocalPath); - var downloadFileExtension = Path.GetExtension(downloadUri.LocalPath); - var downloadFileName = Path.GetFileName(downloadUri.LocalPath); - - if (AppEnvironment.PublishMode == AppPublishMode.FrameworkDependent) - { - downloadFileNameWithoutExtension += "-frameworkdependent"; - } - - if (AppEnvironment.DeploymentMode == AppDeploymentMode.PerMachine) - { - // keep current value - } - else if (AppEnvironment.DeploymentMode == AppDeploymentMode.PerUser) - { - downloadFileNameWithoutExtension += "-userinstaller"; - } - else if (AppEnvironment.DeploymentMode == AppDeploymentMode.Portable) - { - downloadFileNameWithoutExtension += "-portable"; - downloadFileExtension = ".zip"; - } - - var newFileName = $"{ downloadFileNameWithoutExtension }{ downloadFileExtension }"; - var newPath = downloadUri.LocalPath.Replace(downloadFileName, newFileName); - var uriBuilder = new UriBuilder(downloadUri) - { - Path = newPath - }; - - return uriBuilder.Uri.AbsoluteUri; + // keep current value } + else if (AppEnvironment.DeploymentMode == AppDeploymentMode.PerUser) + { + downloadFileNameWithoutExtension += "-userinstaller"; + } + else if (AppEnvironment.DeploymentMode == AppDeploymentMode.Portable) + { + downloadFileNameWithoutExtension += "-portable"; + downloadFileExtension = ".zip"; + } + + var newFileName = $"{downloadFileNameWithoutExtension}{downloadFileExtension}"; + var newPath = downloadUri.LocalPath.Replace(downloadFileName, newFileName); + var uriBuilder = new UriBuilder(downloadUri) + { + Path = newPath + }; + + return uriBuilder.Uri.AbsoluteUri; } } } diff --git a/src/Infrastructure/Helpers/ConnectionStringHelper.cs b/src/Infrastructure/Helpers/ConnectionStringHelper.cs index cef66695..31b6132f 100644 --- a/src/Infrastructure/Helpers/ConnectionStringHelper.cs +++ b/src/Infrastructure/Helpers/ConnectionStringHelper.cs @@ -1,162 +1,160 @@ -namespace Sqlbi.Bravo.Infrastructure.Helpers +using System; +using System.Data.Common; +using System.Net; +using Sqlbi.Bravo.Models; + +namespace Sqlbi.Bravo.Infrastructure.Helpers; + +internal static class ConnectionStringHelper { - using Sqlbi.Bravo.Infrastructure.Security; - using Sqlbi.Bravo.Models; - using System; - using System.Data.Common; - using System.Net; + // Connection string properties + // https://docs.microsoft.com/en-us/analysis-services/instances/connection-string-properties-analysis-services?view=asallproducts-allversions + + private const string PasswordKey = "Password"; + private const string ProviderKey = "Provider"; + private const string DataSourceKey = "Data Source"; + private const string InitialCatalogKey = "Initial Catalog"; + private const string IntegratedSecurityKey = "Integrated Security"; + private const string PersistSecurityInfoKey = "Persist Security Info"; + //private const string UseEncryptionForDataKey = "Use Encryption for Data"; + private const string ApplicationNameKey = "Application Name"; + private const string ConnectTimeoutKey = "Connect Timeout"; + private const string IdentityProviderKey = "Identity Provider"; + + private const string ProviderMsolapValue = "MSOLAP"; + private const string IntegratedSecuritySspiValue = "SSPI"; + private const string IntegratedSecurityClaimsTokenValue = "ClaimsToken"; + private const string PersistSecurityInfoValue = "True"; + + public static string BuildFor(IPEndPoint endPoint) + { + // PBIDesktop relies on a local Analysis Services instance that is binded to the loopback interface. + // Because of this, we are reducing the maximum amount of time the client attempts a connection before timing out. + var connectTimeout = 1; + var dataSource = endPoint.ToString(); + + var builder = new DbConnectionStringBuilder() + { + { ProviderKey, ProviderMsolapValue }, + { DataSourceKey, dataSource }, + //{ InitialCatalogKey, databaseName }, + { ConnectTimeoutKey, connectTimeout }, + { IntegratedSecurityKey, IntegratedSecuritySspiValue }, + { PersistSecurityInfoKey, PersistSecurityInfoValue }, + { ApplicationNameKey, AppEnvironment.ApplicationInstanceUniqueName } + }; + + return builder.ConnectionString; + } - internal static class ConnectionStringHelper + public static string BuildFor(PBIDesktopReport report) { - // Connection string properties - // https://docs.microsoft.com/en-us/analysis-services/instances/connection-string-properties-analysis-services?view=asallproducts-allversions - - private const string PasswordKey = "Password"; - private const string ProviderKey = "Provider"; - private const string DataSourceKey = "Data Source"; - private const string InitialCatalogKey = "Initial Catalog"; - private const string IntegratedSecurityKey = "Integrated Security"; - private const string PersistSecurityInfoKey = "Persist Security Info"; - //private const string UseEncryptionForDataKey = "Use Encryption for Data"; - private const string ApplicationNameKey = "Application Name"; - private const string ConnectTimeoutKey = "Connect Timeout"; - private const string IdentityProviderKey = "Identity Provider"; - - private const string ProviderMsolapValue = "MSOLAP"; - private const string IntegratedSecuritySspiValue = "SSPI"; - private const string IntegratedSecurityClaimsTokenValue = "ClaimsToken"; - private const string PersistSecurityInfoValue = "True"; - - public static string BuildFor(IPEndPoint endPoint) + BravoUnexpectedException.ThrowIfNull(report.ServerName); + BravoUnexpectedException.ThrowIfNull(report.DatabaseName); + + var builder = new DbConnectionStringBuilder() { - // PBIDesktop relies on a local Analysis Services instance that is binded to the loopback interface. - // Because of this, we are reducing the maximum amount of time the client attempts a connection before timing out. - var connectTimeout = 1; - var dataSource = endPoint.ToString(); + { ProviderKey, ProviderMsolapValue }, + { DataSourceKey, report.ServerName }, + { InitialCatalogKey, report.DatabaseName }, + { IntegratedSecurityKey, IntegratedSecuritySspiValue }, + { PersistSecurityInfoKey, PersistSecurityInfoValue }, + { ApplicationNameKey, AppEnvironment.ApplicationInstanceUniqueName } + }; + + return builder.ConnectionString; + } - var builder = new DbConnectionStringBuilder() + public static string BuildFor(PBICloudDataset dataset, string accessToken) + { + BravoUnexpectedException.Assert(dataset.ConnectionMode == PBICloudDatasetConnectionMode.Supported); + + if (dataset.IsXmlaEndPointSupported) + { + // Dataset connectivity with the XMLA endpoint + // https://docs.microsoft.com/en-us/power-bi/admin/service-premium-connect-tools + // Connection string properties + // https://docs.microsoft.com/en-us/analysis-services/instances/connection-string-properties-analysis-services?view=asallproducts-allversions + + // TODO: Handle possible duplicated workspace name - when connecting to a workspace with the same name as another workspace, append the workspace guid to the workspace name + // https://docs.microsoft.com/en-us/power-bi/admin/service-premium-connect-tools#duplicate-workspace-names + + // TODO: Handle possible duplicated dataset name - when connecting to a dataset with the same name as another dataset in the same workspace, append the dataset guid to the dataset name + // https://docs.microsoft.com/en-us/power-bi/admin/service-premium-connect-tools#duplicate-dataset-name + + BravoUnexpectedException.ThrowIfNull(dataset.WorkspaceName); + BravoUnexpectedException.ThrowIfNull(dataset.IdentityProvider); + BravoUnexpectedException.ThrowIfNull(dataset.ExternalServerName); + BravoUnexpectedException.ThrowIfNull(dataset.ExternalDatabaseName); + BravoUnexpectedException.ThrowIfNull(accessToken); + + // TODO: add support for B2B users + // - Users with UPNs in the same tenant (not B2B) can replace the tenant name with 'myorg' + // - B2B users must specify their organization UPN in tenant name + // var homeTenant = CurrentAuthentication?.Account.GetTenantProfiles().SingleOrDefault((t) => t.IsHomeTenant); + + var tenantName = "myorg"; + var workspaceName = Uri.EscapeDataString(dataset.WorkspaceName); + var serverNameBuilder = new UriBuilder(dataset.ExternalServerName) { - { ProviderKey, ProviderMsolapValue }, - { DataSourceKey, dataSource }, - //{ InitialCatalogKey, databaseName }, - { ConnectTimeoutKey, connectTimeout }, - { IntegratedSecurityKey, IntegratedSecuritySspiValue }, - { PersistSecurityInfoKey, PersistSecurityInfoValue }, - { ApplicationNameKey, AppEnvironment.ApplicationInstanceUniqueName } + Path = $"/v1.0/{tenantName}/{workspaceName}" }; + var serverName = serverNameBuilder.Uri.AbsoluteUri; + var databaseName = dataset.ExternalDatabaseName; - return builder.ConnectionString; + return Build(serverName, databaseName, accessToken, dataset.IdentityProvider); } + else if (dataset.IsOnPremModel == true) + { + BravoUnexpectedException.ThrowIfNull(dataset.OnPremModelConnectionString); - public static string BuildFor(PBIDesktopReport report) + return dataset.OnPremModelConnectionString; + } + else { - BravoUnexpectedException.ThrowIfNull(report.ServerName); - BravoUnexpectedException.ThrowIfNull(report.DatabaseName); + BravoUnexpectedException.ThrowIfNull(dataset.IdentityProvider); + BravoUnexpectedException.ThrowIfNull(dataset.ExternalServerName); + BravoUnexpectedException.ThrowIfNull(dataset.ExternalDatabaseName); + BravoUnexpectedException.ThrowIfNull(accessToken); + + var serverName = dataset.ExternalServerName; + var databaseName = dataset.ExternalDatabaseName; + return Build(serverName, databaseName, accessToken, dataset.IdentityProvider); + } + + static string Build(string serverName, string databaseName, string accessToken, string identityProvider) + { var builder = new DbConnectionStringBuilder() { { ProviderKey, ProviderMsolapValue }, - { DataSourceKey, report.ServerName }, - { InitialCatalogKey, report.DatabaseName }, - { IntegratedSecurityKey, IntegratedSecuritySspiValue }, + { DataSourceKey, serverName }, + { InitialCatalogKey, databaseName }, + { IntegratedSecurityKey, IntegratedSecurityClaimsTokenValue }, { PersistSecurityInfoKey, PersistSecurityInfoValue }, + { IdentityProviderKey, identityProvider }, + { PasswordKey, accessToken }, // The Analysis Services client libraries automatically add the auth-scheme value "Bearer" to the access token { ApplicationNameKey, AppEnvironment.ApplicationInstanceUniqueName } }; return builder.ConnectionString; } + } - public static string BuildFor(PBICloudDataset dataset, string accessToken) - { - BravoUnexpectedException.Assert(dataset.ConnectionMode == PBICloudDatasetConnectionMode.Supported); - - if (dataset.IsXmlaEndPointSupported) - { - // Dataset connectivity with the XMLA endpoint - // https://docs.microsoft.com/en-us/power-bi/admin/service-premium-connect-tools - // Connection string properties - // https://docs.microsoft.com/en-us/analysis-services/instances/connection-string-properties-analysis-services?view=asallproducts-allversions - - // TODO: Handle possible duplicated workspace name - when connecting to a workspace with the same name as another workspace, append the workspace guid to the workspace name - // https://docs.microsoft.com/en-us/power-bi/admin/service-premium-connect-tools#duplicate-workspace-names - - // TODO: Handle possible duplicated dataset name - when connecting to a dataset with the same name as another dataset in the same workspace, append the dataset guid to the dataset name - // https://docs.microsoft.com/en-us/power-bi/admin/service-premium-connect-tools#duplicate-dataset-name - - BravoUnexpectedException.ThrowIfNull(dataset.WorkspaceName); - BravoUnexpectedException.ThrowIfNull(dataset.IdentityProvider); - BravoUnexpectedException.ThrowIfNull(dataset.ExternalServerName); - BravoUnexpectedException.ThrowIfNull(dataset.ExternalDatabaseName); - BravoUnexpectedException.ThrowIfNull(accessToken); - - // TODO: add support for B2B users - // - Users with UPNs in the same tenant (not B2B) can replace the tenant name with 'myorg' - // - B2B users must specify their organization UPN in tenant name - // var homeTenant = CurrentAuthentication?.Account.GetTenantProfiles().SingleOrDefault((t) => t.IsHomeTenant); - - var tenantName = "myorg"; - var workspaceName = Uri.EscapeDataString(dataset.WorkspaceName); - var serverNameBuilder = new UriBuilder(dataset.ExternalServerName) - { - Path = $"/v1.0/{ tenantName }/{ workspaceName }" - }; - var serverName = serverNameBuilder.Uri.AbsoluteUri; - var databaseName = dataset.ExternalDatabaseName; - - return Build(serverName, databaseName, accessToken, dataset.IdentityProvider); - } - else if (dataset.IsOnPremModel == true) - { - BravoUnexpectedException.ThrowIfNull(dataset.OnPremModelConnectionString); - - return dataset.OnPremModelConnectionString; - } - else - { - BravoUnexpectedException.ThrowIfNull(dataset.IdentityProvider); - BravoUnexpectedException.ThrowIfNull(dataset.ExternalServerName); - BravoUnexpectedException.ThrowIfNull(dataset.ExternalDatabaseName); - BravoUnexpectedException.ThrowIfNull(accessToken); - - var serverName = dataset.ExternalServerName; - var databaseName = dataset.ExternalDatabaseName; - - return Build(serverName, databaseName, accessToken, dataset.IdentityProvider); - } - - static string Build(string serverName, string databaseName, string accessToken, string identityProvider) - { - var builder = new DbConnectionStringBuilder() - { - { ProviderKey, ProviderMsolapValue }, - { DataSourceKey, serverName }, - { InitialCatalogKey, databaseName }, - { IntegratedSecurityKey, IntegratedSecurityClaimsTokenValue }, - { PersistSecurityInfoKey, PersistSecurityInfoValue }, - { IdentityProviderKey, identityProvider }, - { PasswordKey, accessToken }, // The Analysis Services client libraries automatically add the auth-scheme value "Bearer" to the access token - { ApplicationNameKey, AppEnvironment.ApplicationInstanceUniqueName } - }; - - return builder.ConnectionString; - } - } - - public static (string? ServerName, string? DatabaseName) GetConnectionStringProperties(string? connectionString) - { - var builder = new DbConnectionStringBuilder(useOdbcRules: false); - builder.ConnectionString = connectionString; + public static (string? ServerName, string? DatabaseName) GetConnectionStringProperties(string? connectionString) + { + var builder = new DbConnectionStringBuilder(useOdbcRules: false); + builder.ConnectionString = connectionString; - string? serverName = null; - string? databaseName = null; + string? serverName = null; + string? databaseName = null; - if (builder.TryGetValue(DataSourceKey, out var dataSource)) - serverName = (string)dataSource; + if (builder.TryGetValue(DataSourceKey, out var dataSource)) + serverName = (string)dataSource; - if (builder.TryGetValue(InitialCatalogKey, out var initialCatalog)) - databaseName = (string)initialCatalog; + if (builder.TryGetValue(InitialCatalogKey, out var initialCatalog)) + databaseName = (string)initialCatalog; - return (serverName, databaseName); - } + return (serverName, databaseName); } -} \ No newline at end of file +} diff --git a/src/Infrastructure/Helpers/DesktopBridgeHelper.cs b/src/Infrastructure/Helpers/DesktopBridgeHelper.cs index 468868b9..7c8228f2 100644 --- a/src/Infrastructure/Helpers/DesktopBridgeHelper.cs +++ b/src/Infrastructure/Helpers/DesktopBridgeHelper.cs @@ -1,85 +1,84 @@ -namespace Sqlbi.Bravo.Infrastructure.Helpers +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using Sqlbi.Bravo.Infrastructure.Windows.Interop; + +namespace Sqlbi.Bravo.Infrastructure.Helpers; + +internal static class DesktopBridgeHelper { - using Sqlbi.Bravo.Infrastructure.Windows.Interop; - using System; - using System.Diagnostics; - using System.IO; - using System.Text; + /// + /// The process has no package identity + /// + private const long APPMODEL_ERROR_NO_PACKAGE = 15700L; - internal static class DesktopBridgeHelper + private static bool IsWindows7OrLower() { - /// - /// The process has no package identity - /// - private const long APPMODEL_ERROR_NO_PACKAGE = 15700L; + var major = (double)Environment.OSVersion.Version.Major; + var minor = (double)Environment.OSVersion.Version.Minor; - private static bool IsWindows7OrLower() - { - var major = (double)Environment.OSVersion.Version.Major; - var minor = (double)Environment.OSVersion.Version.Minor; + var result = (major + minor / 10.0) <= 6.1; + return result; + } - var result = (major + minor / 10.0) <= 6.1; - return result; - } + /// + /// Returns true if the app is running as an MSIX package on Windows 10, version 1709 (build 16299) or later + /// + public static bool IsRunningAsMsixPackage() + { + // https://docs.microsoft.com/en-us/windows/msix/detect-package-identity - /// - /// Returns true if the app is running as an MSIX package on Windows 10, version 1709 (build 16299) or later - /// - public static bool IsRunningAsMsixPackage() - { - // https://docs.microsoft.com/en-us/windows/msix/detect-package-identity + if (IsWindows7OrLower()) + return false; - if (IsWindows7OrLower()) - return false; + var packageFullNameLength = 0; + var packageFullName = new StringBuilder(0); + _ = Kernel32.GetCurrentPackageFullName(ref packageFullNameLength, packageFullName); - var packageFullNameLength = 0; - var packageFullName = new StringBuilder(0); - _ = Kernel32.GetCurrentPackageFullName(ref packageFullNameLength, packageFullName); + packageFullName = new StringBuilder(packageFullNameLength); + var retval = Kernel32.GetCurrentPackageFullName(ref packageFullNameLength, packageFullName); - packageFullName = new StringBuilder(packageFullNameLength); - var retval = Kernel32.GetCurrentPackageFullName(ref packageFullNameLength, packageFullName); + return retval != APPMODEL_ERROR_NO_PACKAGE; + } - return retval != APPMODEL_ERROR_NO_PACKAGE; - } + /// + /// Attempt to start a new instance of the current application as administrator triggering the request for elevation via UAC + /// + /// If the current process is not running as an MSIX package application + public static bool AppRunAs(bool environmentExit, params string[] customArgs) + { + if (!IsRunningAsMsixPackage()) + throw new InvalidOperationException("The current process is not running as packaged application"); + + // This requires AppExecutionAlias activation in the MSIX installation package + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.DoNotVerify); + var fileName = Path.Combine(localAppData, @"Microsoft\WindowsApps", Path.ChangeExtension(AppEnvironment.ApplicationStoreAliasName, ".exe")); + + var startInfo = new ProcessStartInfo + { + UseShellExecute = true, + FileName = fileName, + Verb = "runas", // this will trigger the request for elevation via UAC + }; - /// - /// Attempt to start a new instance of the current application as administrator triggering the request for elevation via UAC - /// - /// If the current process is not running as an MSIX package application - public static bool AppRunAs(bool environmentExit, params string[] customArgs) + //foreach (var arg in Environment.GetCommandLineArgs().Skip(1)) + // startInfo.ArgumentList.Add(arg); + + foreach (var arg in customArgs) + startInfo.ArgumentList.Add(arg); + + var process = Process.Start(startInfo); + if (process is null || process.HasExited) + return false; + + if (environmentExit) { - if (!IsRunningAsMsixPackage()) - throw new InvalidOperationException("The current process is not running as packaged application"); - - // This requires AppExecutionAlias activation in the MSIX installation package - var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData, Environment.SpecialFolderOption.DoNotVerify); - var fileName = Path.Combine(localAppData, @"Microsoft\WindowsApps", Path.ChangeExtension(AppEnvironment.ApplicationStoreAliasName, ".exe")); - - var startInfo = new ProcessStartInfo - { - UseShellExecute = true, - FileName = fileName, - Verb = "runas", // this will trigger the request for elevation via UAC - }; - - //foreach (var arg in Environment.GetCommandLineArgs().Skip(1)) - // startInfo.ArgumentList.Add(arg); - - foreach (var arg in customArgs) - startInfo.ArgumentList.Add(arg); - - var process = Process.Start(startInfo); - if (process is null || process.HasExited) - return false; - - if (environmentExit) - { - // Exit terminates the application immediately even if other threads are running - // If called from a try or catch block, the code in any finally block does not execute - Environment.Exit(0); - } - - return true; + // Exit terminates the application immediately even if other threads are running + // If called from a try or catch block, the code in any finally block does not execute + Environment.Exit(0); } + + return true; } } diff --git a/src/Infrastructure/Helpers/ExceptionHelper.cs b/src/Infrastructure/Helpers/ExceptionHelper.cs index 404a3a90..8884870c 100644 --- a/src/Infrastructure/Helpers/ExceptionHelper.cs +++ b/src/Infrastructure/Helpers/ExceptionHelper.cs @@ -1,109 +1,108 @@ -namespace Sqlbi.Bravo.Infrastructure.Helpers +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Security; +using System.Threading; +using System.Windows.Forms; + +namespace Sqlbi.Bravo.Infrastructure.Helpers; + +internal static class ExceptionHelper { - using System; - using System.Diagnostics; - using System.Runtime.InteropServices; - using System.Security; - using System.Threading; - using System.Windows.Forms; + public static void WriteToEventLog(Exception exception, EventLogEntryType type, bool throwOnError = true) + { + var message = exception.ToString(); + WriteToEventLog(message, type, throwOnError); + } - internal static class ExceptionHelper + public static void WriteToEventLog(string message, EventLogEntryType type, bool throwOnError = true) { - public static void WriteToEventLog(Exception exception, EventLogEntryType type, bool throwOnError = true) + try { - var message = exception.ToString(); - WriteToEventLog(message, type, throwOnError); + using var eventLog = new EventLog(logName: "Application", machineName: ".", source: "Application"); + eventLog.WriteEntry(message, type); } - - public static void WriteToEventLog(string message, EventLogEntryType type, bool throwOnError = true) + catch { - try - { - using var eventLog = new EventLog(logName: "Application", machineName: ".", source: "Application"); - eventLog.WriteEntry(message, type); - } - catch - { - if (throwOnError) - throw; - } - } - - public static bool IsOrHasInner(this Exception exception) where T : Exception - { - var foundException = Find(exception); - return foundException != null; + if (throwOnError) + throw; } + } - public static T? Find(this Exception exception) where T : Exception - { - if (exception is T foundException) - return foundException; + public static bool IsOrHasInner(this Exception exception) where T : Exception + { + var foundException = Find(exception); + return foundException != null; + } - var innerException = exception.InnerException; + public static T? Find(this Exception exception) where T : Exception + { + if (exception is T foundException) + return foundException; - while (innerException is not null) - { - if (innerException is T foundInnerException) - return foundInnerException; + var innerException = exception.InnerException; - innerException = innerException.InnerException; - } + while (innerException is not null) + { + if (innerException is T foundInnerException) + return foundInnerException; - return null; + innerException = innerException.InnerException; } - public static bool IsSafeException(Exception ex) - { - if (ex is not StackOverflowException && ex is not OutOfMemoryException && ex is not ThreadAbortException && ex is not AccessViolationException && ex is not SEHException) - { - return !typeof(SecurityException).IsAssignableFrom(ex.GetType()); - } + return null; + } - return false; + public static bool IsSafeException(Exception ex) + { + if (ex is not StackOverflowException && ex is not OutOfMemoryException && ex is not ThreadAbortException && ex is not AccessViolationException && ex is not SEHException) + { + return !typeof(SecurityException).IsAssignableFrom(ex.GetType()); } - public static void ShowDialog(Exception exception) + return false; + } + + public static void ShowDialog(Exception exception) + { + var page = new TaskDialogPage() { - var page = new TaskDialogPage() - { - Caption = AppEnvironment.ApplicationMainWindowTitle, - Heading = @$"Unhandled exception has occurred. The application will be shut down and the error details will be logged in the Windows Event Log. + Caption = AppEnvironment.ApplicationMainWindowTitle, + Heading = @$"Unhandled exception has occurred. The application will be shut down and the error details will be logged in the Windows Event Log. -[{ exception.GetType().Name }] { exception.Message }", - Icon = TaskDialogIcon.Error, - AllowCancel = false, - Buttons = +[{exception.GetType().Name}] {exception.Message}", + Icon = TaskDialogIcon.Error, + AllowCancel = false, + Buttons = + { + new TaskDialogCommandLinkButton("&Copy details", "Copy error details to clipboard and close") { - new TaskDialogCommandLinkButton("&Copy details", "Copy error details to clipboard and close") - { - Tag = 10 - }, - new TaskDialogCommandLinkButton("&Close", "Terminate the application") - { - Tag = 20 - }, + Tag = 10 }, - Expander = new TaskDialogExpander() + new TaskDialogCommandLinkButton("&Close", "Terminate the application") { - Expanded = false, - Text = $"{ exception }", - Position = TaskDialogExpanderPosition.AfterFootnote, - } - }; - - var dialogButton = TaskDialog.ShowDialog(page, TaskDialogStartupLocation.CenterScreen); - - switch (dialogButton.Tag) + Tag = 20 + }, + }, + Expander = new TaskDialogExpander() { - case 10: - Clipboard.SetText(page.Expander.Text, TextDataFormat.Text); - break; - case 20: - break; - default: - throw new BravoUnexpectedInvalidOperationException($"Unhandled { nameof(TaskDialogButton) } result ({ dialogButton.Tag })"); + Expanded = false, + Text = $"{exception}", + Position = TaskDialogExpanderPosition.AfterFootnote, } + }; + + var dialogButton = TaskDialog.ShowDialog(page, TaskDialogStartupLocation.CenterScreen); + + switch (dialogButton.Tag) + { + case 10: + Clipboard.SetText(page.Expander.Text, TextDataFormat.Text); + break; + case 20: + break; + default: + throw new BravoUnexpectedInvalidOperationException($"Unhandled {nameof(TaskDialogButton)} result ({dialogButton.Tag})"); } } } diff --git a/src/Infrastructure/Helpers/NetworkHelper.cs b/src/Infrastructure/Helpers/NetworkHelper.cs index a617c603..4a249e8d 100644 --- a/src/Infrastructure/Helpers/NetworkHelper.cs +++ b/src/Infrastructure/Helpers/NetworkHelper.cs @@ -1,163 +1,162 @@ -namespace Sqlbi.Bravo.Infrastructure.Helpers +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; +using Sqlbi.Bravo.Infrastructure.Windows.Interop; + +namespace Sqlbi.Bravo.Infrastructure.Helpers; + +internal static class NetworkHelper { - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; - using Sqlbi.Bravo.Infrastructure.Windows.Interop; - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.Linq; - using System.Net; - using System.Net.NetworkInformation; - using System.Net.Sockets; - using System.Runtime.InteropServices; - - internal static class NetworkHelper + /// + /// Standard host name given to the address of the loopback network interface + /// + public static readonly string Localhost = "localhost"; + + /// + /// A special proxy bypass rule which has the effect of subtracting the implicit loopback rules + /// https://chromium.googlesource.com/chromium/src/+/HEAD/net/docs/proxy.md#overriding-the-implicit-bypass-rules + /// + public static readonly string LoopbackProxyBypassRule = "<-loopback>"; + + /// + /// Returns true if the protocol schema for the provided URI is + /// + public static bool IsASAzureServer(string address) { - /// - /// Standard host name given to the address of the loopback network interface - /// - public static readonly string Localhost = "localhost"; - - /// - /// A special proxy bypass rule which has the effect of subtracting the implicit loopback rules - /// https://chromium.googlesource.com/chromium/src/+/HEAD/net/docs/proxy.md#overriding-the-implicit-bypass-rules - /// - public static readonly string LoopbackProxyBypassRule = "<-loopback>"; - - /// - /// Returns true if the protocol schema for the provided URI is - /// - public static bool IsASAzureServer(string address) + if (address.Contains(Uri.SchemeDelimiter) && Uri.TryCreate(address, UriKind.Absolute, out var addressUri)) { - if (address.Contains(Uri.SchemeDelimiter) && Uri.TryCreate(address, UriKind.Absolute, out var addressUri)) - { - if (addressUri.Scheme.EqualsI(CloudApiClient.ASAzureProtocolScheme)) - return true; - } - - return false; + if (addressUri.Scheme.EqualsI(CloudApiClient.ASAzureProtocolScheme)) + return true; } - /// - /// Returns true if the protocol schema for the provided URI is or - /// - public static bool IsPBICloudDatasetServer(string address) - { - if (address.Contains(Uri.SchemeDelimiter) && Uri.TryCreate(address, UriKind.Absolute, out var addressUri)) - { - var isGenericDataset = addressUri.Scheme.EqualsI(CloudApiClient.PBIDatasetProtocolScheme); - var isPremiumDataset = addressUri.Scheme.EqualsI(CloudApiClient.PBIPremiumXmlaEndpointProtocolScheme); // <-- can be removed ?? + return false; + } - return isPremiumDataset || isGenericDataset; - } + /// + /// Returns true if the protocol schema for the provided URI is or + /// + public static bool IsPBICloudDatasetServer(string address) + { + if (address.Contains(Uri.SchemeDelimiter) && Uri.TryCreate(address, UriKind.Absolute, out var addressUri)) + { + var isGenericDataset = addressUri.Scheme.EqualsI(CloudApiClient.PBIDatasetProtocolScheme); + var isPremiumDataset = addressUri.Scheme.EqualsI(CloudApiClient.PBIPremiumXmlaEndpointProtocolScheme); // <-- can be removed ?? - return false; + return isPremiumDataset || isGenericDataset; } - public static Process? FindEndPointProcess(IPEndPoint endpoint) + return false; + } + + public static Process? FindEndPointProcess(IPEndPoint endpoint) + { + switch (endpoint.AddressFamily) { - switch (endpoint.AddressFamily) + case AddressFamily.InterNetwork: { - case AddressFamily.InterNetwork: - { - var ipv4Connections = GetTcpConnections(AddressFamily.InterNetwork); - var endpointConnection = ipv4Connections.Where((c) => c.LocalEndPoint.Equals(endpoint)).DefaultIfEmpty(); - if (endpointConnection is not null) - { - var process = ProcessHelper.SafeGetProcessById(endpointConnection.Single().ProcessId); - return process; - } - } - break; - case AddressFamily.InterNetworkV6: - { - var ipv6Connections = GetTcpConnections(AddressFamily.InterNetworkV6); - var endpointConnection = ipv6Connections.Where((c) => c.LocalEndPoint.Equals(endpoint)).DefaultIfEmpty(); - if (endpointConnection is not null) - { - var process = ProcessHelper.SafeGetProcessById(endpointConnection.Single().ProcessId); - return process; - } - } - break; + var ipv4Connections = GetTcpConnections(AddressFamily.InterNetwork); + var endpointConnection = ipv4Connections.Where((c) => c.LocalEndPoint.Equals(endpoint)).DefaultIfEmpty(); + if (endpointConnection is not null) + { + var process = ProcessHelper.SafeGetProcessById(endpointConnection.Single().ProcessId); + return process; + } } - - return null; + break; + case AddressFamily.InterNetworkV6: + { + var ipv6Connections = GetTcpConnections(AddressFamily.InterNetworkV6); + var endpointConnection = ipv6Connections.Where((c) => c.LocalEndPoint.Equals(endpoint)).DefaultIfEmpty(); + if (endpointConnection is not null) + { + var process = ProcessHelper.SafeGetProcessById(endpointConnection.Single().ProcessId); + return process; + } + } + break; } - public static IEnumerable<(IPEndPoint EndPoint, TcpState State, int ProcessId)> GetTcpConnections(Func<(IPEndPoint EndPoint, TcpState State, int ProcessId), bool> predicate) - { - var ipv4Connections = GetTcpConnections(AddressFamily.InterNetwork).Select((r) => (r.LocalEndPoint, r.TcpState, r.ProcessId)); - foreach (var connection in ipv4Connections.Where(predicate)) - yield return connection; + return null; + } - var ipv6Connections = GetTcpConnections(AddressFamily.InterNetworkV6).Select((r) => (r.LocalEndPoint, r.TcpState, r.ProcessId)); - foreach (var connection in ipv6Connections.Where(predicate)) - yield return connection; - } + public static IEnumerable<(IPEndPoint EndPoint, TcpState State, int ProcessId)> GetTcpConnections(Func<(IPEndPoint EndPoint, TcpState State, int ProcessId), bool> predicate) + { + var ipv4Connections = GetTcpConnections(AddressFamily.InterNetwork).Select((r) => (r.LocalEndPoint, r.TcpState, r.ProcessId)); + foreach (var connection in ipv4Connections.Where(predicate)) + yield return connection; + + var ipv6Connections = GetTcpConnections(AddressFamily.InterNetworkV6).Select((r) => (r.LocalEndPoint, r.TcpState, r.ProcessId)); + foreach (var connection in ipv6Connections.Where(predicate)) + yield return connection; + } - private static TRow[] GetTcpConnections(AddressFamily addressFamily) + private static TRow[] GetTcpConnections(AddressFamily addressFamily) + { + switch (addressFamily) { - switch (addressFamily) - { - case AddressFamily.InterNetwork: - if (!Socket.OSSupportsIPv4) return Array.Empty(); - break; - case AddressFamily.InterNetworkV6: - if (!Socket.OSSupportsIPv6) return Array.Empty(); - break; - default: - throw new ArgumentException("Unsupported addressing scheme", paramName: nameof(addressFamily)); - } + case AddressFamily.InterNetwork: + if (!Socket.OSSupportsIPv4) return Array.Empty(); + break; + case AddressFamily.InterNetworkV6: + if (!Socket.OSSupportsIPv6) return Array.Empty(); + break; + default: + throw new ArgumentException("Unsupported addressing scheme", paramName: nameof(addressFamily)); + } - var ipVersion = (uint)addressFamily; - var dwOutBufLen = 0u; + var ipVersion = (uint)addressFamily; + var dwOutBufLen = 0u; - var retval = Iphlpapi.GetExtendedTcpTable(pTcpTable: IntPtr.Zero, ref dwOutBufLen, order: false, ipVersion, Iphlpapi.TCP_TABLE_CLASS.TCP_TABLE_OWNER_PID_ALL); - if (retval == NativeMethods.ERROR_INSUFFICIENT_BUFFER) + var retval = Iphlpapi.GetExtendedTcpTable(pTcpTable: IntPtr.Zero, ref dwOutBufLen, order: false, ipVersion, Iphlpapi.TCP_TABLE_CLASS.TCP_TABLE_OWNER_PID_ALL); + if (retval == NativeMethods.ERROR_INSUFFICIENT_BUFFER) + { + var pTcpTable = Marshal.AllocHGlobal((int)dwOutBufLen); + try { - var pTcpTable = Marshal.AllocHGlobal((int)dwOutBufLen); - try + retval = Iphlpapi.GetExtendedTcpTable(pTcpTable, ref dwOutBufLen, order: false, ipVersion, Iphlpapi.TCP_TABLE_CLASS.TCP_TABLE_OWNER_PID_ALL); + if (retval == NativeMethods.ERROR_SUCCESS) { - retval = Iphlpapi.GetExtendedTcpTable(pTcpTable, ref dwOutBufLen, order: false, ipVersion, Iphlpapi.TCP_TABLE_CLASS.TCP_TABLE_OWNER_PID_ALL); - if (retval == NativeMethods.ERROR_SUCCESS) + var tableObject = Marshal.PtrToStructure(pTcpTable, typeof(TTable)); + if (tableObject != null) { - var tableObject = Marshal.PtrToStructure(pTcpTable, typeof(TTable)); - if (tableObject != null) - { - var table = (TTable)tableObject; - var tableNumEntries = (uint)(typeof(TTable).GetField("dwNumEntries")?.GetValue(table) ?? throw new InvalidOperationException("Table field not found [dwNumEntries]")); - var rows = new TRow[tableNumEntries]; - var ptr = (IntPtr)((long)pTcpTable + Marshal.SizeOf(tableNumEntries)); + var table = (TTable)tableObject; + var tableNumEntries = (uint)(typeof(TTable).GetField("dwNumEntries")?.GetValue(table) ?? throw new InvalidOperationException("Table field not found [dwNumEntries]")); + var rows = new TRow[tableNumEntries]; + var ptr = (IntPtr)((long)pTcpTable + Marshal.SizeOf(tableNumEntries)); - for (var i = 0; i < tableNumEntries; i++) + for (var i = 0; i < tableNumEntries; i++) + { + var rowObject = Marshal.PtrToStructure(ptr, typeof(TRow)); + if (rowObject != null) { - var rowObject = Marshal.PtrToStructure(ptr, typeof(TRow)); - if (rowObject != null) - { - var row = (TRow)rowObject; - ptr = (IntPtr)((long)ptr + Marshal.SizeOf(row)); - rows[i] = row; - } + var row = (TRow)rowObject; + ptr = (IntPtr)((long)ptr + Marshal.SizeOf(row)); + rows[i] = row; } - - return rows; } + + return rows; } } - finally - { - Marshal.FreeHGlobal(pTcpTable); - } } - - if (retval == NativeMethods.ERROR_INSUFFICIENT_BUFFER) + finally { - return Array.Empty(); + Marshal.FreeHGlobal(pTcpTable); } + } - throw new NetworkInformationException(errorCode: (int)retval); + if (retval == NativeMethods.ERROR_INSUFFICIENT_BUFFER) + { + return Array.Empty(); } + + throw new NetworkInformationException(errorCode: (int)retval); } } diff --git a/src/Infrastructure/Helpers/ProcessHelper.cs b/src/Infrastructure/Helpers/ProcessHelper.cs index 5401e5a4..a3a009dc 100644 --- a/src/Infrastructure/Helpers/ProcessHelper.cs +++ b/src/Infrastructure/Helpers/ProcessHelper.cs @@ -1,404 +1,402 @@ -namespace Sqlbi.Bravo.Infrastructure.Helpers +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Management; +using System.Runtime.ExceptionServices; +using System.Security.Claims; +using System.Security.Principal; +using System.Threading; +using System.Windows.Forms; +using Sqlbi.Bravo.Infrastructure.Extensions; + +namespace Sqlbi.Bravo.Infrastructure.Helpers; + +internal static class ProcessHelper { - using Sqlbi.Bravo.Infrastructure.Extensions; - using System; - using System.Collections.Generic; - using System.ComponentModel; - using System.Diagnostics; - using System.Diagnostics.CodeAnalysis; - using System.Globalization; - using System.IO; - using System.Linq; - using System.Management; - using System.Runtime.ExceptionServices; - using System.Security.Claims; - using System.Security.Principal; - using System.Threading; - using System.Threading.Tasks; - using System.Windows.Forms; - - internal static class ProcessHelper + public static T RunOnSTAThread(Func func) { - public static T RunOnSTAThread(Func func) - { - T result = default!; - - RunOnSTAThread(() => - { - result = func(); - }); + T result = default!; - return result; - } - - public static void RunOnSTAThread(Action action) + RunOnSTAThread(() => { - Exception? exception = null; + result = func(); + }); - var thread = new Thread(() => - { - try - { - action(); - } - catch (Exception ex) - { - exception = ex; - } - }); + return result; + } - thread.SetApartmentState(ApartmentState.STA); - thread.CurrentCulture = CultureInfo.CurrentCulture; - thread.CurrentUICulture = CultureInfo.CurrentUICulture; - // Set as background to ensure that it does not prevent the process from exiting - thread.IsBackground = true; - thread.Start(); - thread.Join(); + public static void RunOnSTAThread(Action action) + { + Exception? exception = null; - if (exception is not null) + var thread = new Thread(() => + { + try { - // Re-throw the exception on the calling thread to preserve the original stack trace - ExceptionDispatchInfo.Capture(exception).Throw(); + action(); } - } - - public static void InvokeOnUIThread(Control control, Action action) => InvokeOnUIThread(action, control); - - public static void InvokeOnUIThread(Action action, Control? control = null) - { - if (control is null) + catch (Exception ex) { - var mainWindowHandle = GetCurrentProcessMainWindowHandle(); - control = Control.FromHandle(mainWindowHandle); + exception = ex; } + }); - //if (!Application.MessageLoop) - //{ - //} + thread.SetApartmentState(ApartmentState.STA); + thread.CurrentCulture = CultureInfo.CurrentCulture; + thread.CurrentUICulture = CultureInfo.CurrentUICulture; + // Set as background to ensure that it does not prevent the process from exiting + thread.IsBackground = true; + thread.Start(); + thread.Join(); - // Control.FromHandle returns null when the handle does not belong to a control of this - // process: there is no UI thread to marshal to, so the action runs on the calling thread. - if (control is not null && control.InvokeRequired) - { - control.Invoke(action); - } - else - { - action(); - } + if (exception is not null) + { + // Re-throw the exception on the calling thread to preserve the original stack trace + ExceptionDispatchInfo.Capture(exception).Throw(); } + } + + public static void InvokeOnUIThread(Control control, Action action) => InvokeOnUIThread(action, control); - /// - /// Makes the ambient - /// while runs - it does not itself run anything on the UI thread, it only lets code - /// that reads the ambient context capture the right one. Keep synchronous - any - /// inside it would resume after this method has already restored the previous context. - /// - public static T RunWithUISynchronizationContext(Func callback) + public static void InvokeOnUIThread(Action action, Control? control = null) + { + if (control is null) { - var previousSynchronizationContext = SynchronizationContext.Current; + var mainWindowHandle = GetCurrentProcessMainWindowHandle(); + control = Control.FromHandle(mainWindowHandle); + } - SynchronizationContext.SetSynchronizationContext(AppWindow.UISynchronizationContext); - try - { - return callback(); - } - finally - { - SynchronizationContext.SetSynchronizationContext(previousSynchronizationContext); - } + //if (!Application.MessageLoop) + //{ + //} + + // Control.FromHandle returns null when the handle does not belong to a control of this + // process: there is no UI thread to marshal to, so the action runs on the calling thread. + if (control is not null && control.InvokeRequired) + { + control.Invoke(action); + } + else + { + action(); } + } + + /// + /// Makes the ambient + /// while runs - it does not itself run anything on the UI thread, it only lets code + /// that reads the ambient context capture the right one. Keep synchronous - any + /// inside it would resume after this method has already restored the previous context. + /// + public static T RunWithUISynchronizationContext(Func callback) + { + var previousSynchronizationContext = SynchronizationContext.Current; - public static void RunOnUISynchronizationContext(Action action) + SynchronizationContext.SetSynchronizationContext(AppWindow.UISynchronizationContext); + try + { + return callback(); + } + finally { - var previousSynchronizationContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(previousSynchronizationContext); + } + } - SynchronizationContext.SetSynchronizationContext(AppWindow.UISynchronizationContext); - try - { - action(); - } - finally - { - SynchronizationContext.SetSynchronizationContext(previousSynchronizationContext); - } + public static void RunOnUISynchronizationContext(Action action) + { + var previousSynchronizationContext = SynchronizationContext.Current; + + SynchronizationContext.SetSynchronizationContext(AppWindow.UISynchronizationContext); + try + { + action(); + } + finally + { + SynchronizationContext.SetSynchronizationContext(previousSynchronizationContext); } + } + + public static void OpenControlPanelItem(string canonicalName) + { + // https://docs.microsoft.com/en-us/windows/win32/shell/controlpanel-canonical-names - public static void OpenControlPanelItem(string canonicalName) + var startInfo = new ProcessStartInfo { - // https://docs.microsoft.com/en-us/windows/win32/shell/controlpanel-canonical-names - - var startInfo = new ProcessStartInfo - { - FileName = Environment.ExpandEnvironmentVariables("%WINDIR%\\System32\\control.exe"), - Arguments = canonicalName - }; + FileName = Environment.ExpandEnvironmentVariables("%WINDIR%\\System32\\control.exe"), + Arguments = canonicalName + }; - using var process = Process.Start(startInfo); - } + using var process = Process.Start(startInfo); + } - public static bool OpenBrowser(Uri address) + public static bool OpenBrowser(Uri address) + { + if (address.IsAbsoluteUri && !address.IsFile && !address.IsUnc && !address.IsLoopback) { - if (address.IsAbsoluteUri && !address.IsFile && !address.IsUnc && !address.IsLoopback) + if (address.Scheme.EqualsI(Uri.UriSchemeHttps) || address.Scheme.EqualsI(Uri.UriSchemeHttp)) { - if (address.Scheme.EqualsI(Uri.UriSchemeHttps) || address.Scheme.EqualsI(Uri.UriSchemeHttp)) + if (AppEnvironment.TrustedUriHosts.Any((trustedHost) => address.Host.EqualsI(trustedHost) || address.Host.EndsWith($".{trustedHost}", StringComparison.OrdinalIgnoreCase))) { - if (AppEnvironment.TrustedUriHosts.Any((trustedHost) => address.Host.EqualsI(trustedHost) || address.Host.EndsWith($".{ trustedHost }", StringComparison.OrdinalIgnoreCase))) + using var process = Process.Start(new ProcessStartInfo { - using var process = Process.Start(new ProcessStartInfo - { - FileName = address.OriginalString, - UseShellExecute = true, - }); + FileName = address.OriginalString, + UseShellExecute = true, + }); - return true; - } + return true; } } - - return false; } - public static bool OpenFileExplorer(string path) + return false; + } + + public static bool OpenFileExplorer(string path) + { + if (Directory.Exists(path)) { - if (Directory.Exists(path)) + var startInfo = new ProcessStartInfo { - var startInfo = new ProcessStartInfo - { - FileName = Environment.ExpandEnvironmentVariables("%WINDIR%\\explorer.exe"), - Arguments = $"/root,\"{ path }\"" - }; - - using var process = Process.Start(startInfo); - return true; - } + FileName = Environment.ExpandEnvironmentVariables("%WINDIR%\\explorer.exe"), + Arguments = $"/root,\"{path}\"" + }; - return false; + using var process = Process.Start(startInfo); + return true; } - public static bool OpenShellExecute(string path, bool waitForStarted, [NotNullWhen(true)] out int? processId, CancellationToken cancellationToken = default) + return false; + } + + public static bool OpenShellExecute(string path, bool waitForStarted, [NotNullWhen(true)] out int? processId, CancellationToken cancellationToken = default) + { + if (File.Exists(path)) { - if (File.Exists(path)) - { - const string Pbix = ".pbix"; - const string Xlsx = ".xlsx"; - const string CodeWorkspace = ".code-workspace"; + const string Pbix = ".pbix"; + const string Xlsx = ".xlsx"; + const string CodeWorkspace = ".code-workspace"; - var extension = Path.GetExtension(path); - var isAllowed = (new[] { Pbix, Xlsx, CodeWorkspace }).Any((ext) => ext.EqualsI(extension)); - var isPbix = extension.EqualsI(Pbix); + var extension = Path.GetExtension(path); + var isAllowed = (new[] { Pbix, Xlsx, CodeWorkspace }).Any((ext) => ext.EqualsI(extension)); + var isPbix = extension.EqualsI(Pbix); - if (isAllowed) + if (isAllowed) + { + var startInfo = new ProcessStartInfo { - var startInfo = new ProcessStartInfo - { - FileName = path, - UseShellExecute = true - }; + FileName = path, + UseShellExecute = true + }; - using var process = Process.Start(startInfo); + using var process = Process.Start(startInfo); + + if (process is not null && !process.HasExited) + { + cancellationToken.ThrowIfCancellationRequested(); - if (process is not null && !process.HasExited) + if (waitForStarted) { - cancellationToken.ThrowIfCancellationRequested(); + try + { + _ = process.WaitForInputIdle(5_000); + } + catch (InvalidOperationException) + { + // ignore + } - if (waitForStarted) + if (isPbix) { - try - { - _ = process.WaitForInputIdle(5_000); - } - catch (InvalidOperationException) - { - // ignore - } + // We force 5 minutes which is the default timeout of HTTP requests + var waitTimeout = TimeSpan.FromMinutes(5).TotalSeconds; - if (isPbix) + for (var i = 0; i < waitTimeout; i++) { - // We force 5 minutes which is the default timeout of HTTP requests - var waitTimeout = TimeSpan.FromMinutes(5).TotalSeconds; + // Cancellation can be requested by the user via the "Cancel" button or when the HTTP request times out. + cancellationToken.ThrowIfCancellationRequested(); - for (var i = 0; i < waitTimeout; i++) + if (process.HasExited) { - // Cancellation can be requested by the user via the "Cancel" button or when the HTTP request times out. - cancellationToken.ThrowIfCancellationRequested(); - - if (process.HasExited) - { - processId = null; - return false; - } + processId = null; + return false; + } - // If the window title is null it means that the SSAS instance is not yet started and/or the model is not yet fully loaded - var isAvailable = process.GetPBIDesktopMainWindowTitle() is not null; - if (isAvailable) - break; + // If the window title is null it means that the SSAS instance is not yet started and/or the model is not yet fully loaded + var isAvailable = process.GetPBIDesktopMainWindowTitle() is not null; + if (isAvailable) + break; - Thread.Sleep(1_000); - } + Thread.Sleep(1_000); } } - - processId = process.Id; - return true; } + + processId = process.Id; + return true; } } - - processId = null; - return false; } - public static bool Open(string path) - { - if (File.Exists(path)) - { - if (OpenShellExecute(path, waitForStarted: false, out _)) - return true; - } - else if (Directory.Exists(path)) - { - if (OpenFileExplorer(path)) - return true; - } + processId = null; + return false; + } - return false; + public static bool Open(string path) + { + if (File.Exists(path)) + { + if (OpenShellExecute(path, waitForStarted: false, out _)) + return true; } - - public static IReadOnlyList GetProcessesByName(string processName) + else if (Directory.Exists(path)) { - var processes = Process.GetProcessesByName(processName).ToList(); + if (OpenFileExplorer(path)) + return true; + } + + return false; + } + + public static IReadOnlyList GetProcessesByName(string processName) + { + var processes = Process.GetProcessesByName(processName).ToList(); - for (var i = processes.Count - 1; i >= 0; i--) + for (var i = processes.Count - 1; i >= 0; i--) + { + if (processes[i].SessionId != AppEnvironment.SessionId) { - if (processes[i].SessionId != AppEnvironment.SessionId) - { - processes[i].Dispose(); - processes.RemoveAt(i); - } + processes[i].Dispose(); + processes.RemoveAt(i); } - - return processes; } - public static Process? GetParentProcess() - { - // ManagementObjectSearcher.Get() raises a System.InvalidCastException when executed on the current thread, this regardless of the apartment state of the current thread (which is STA) - // - // System.InvalidCastException "Specified cast is not valid." - // at System.StubHelpers.InterfaceMarshaler.ConvertToNative(Object objSrc, IntPtr itfMT, IntPtr classMT, Int32 flags) - // at System.Management.SecuredIWbemServicesHandler.ExecQuery_(String strQueryLanguage, String strQuery, Int32 lFlags, IWbemContext pCtx, IEnumWbemClassObject& ppEnum) - // at System.Management.ManagementObjectSearcher.Get() + return processes; + } - var parentProcessId = (int?)null; + public static Process? GetParentProcess() + { + // ManagementObjectSearcher.Get() raises a System.InvalidCastException when executed on the current thread, this regardless of the apartment state of the current thread (which is STA) + // + // System.InvalidCastException "Specified cast is not valid." + // at System.StubHelpers.InterfaceMarshaler.ConvertToNative(Object objSrc, IntPtr itfMT, IntPtr classMT, Int32 flags) + // at System.Management.SecuredIWbemServicesHandler.ExecQuery_(String strQueryLanguage, String strQuery, Int32 lFlags, IWbemContext pCtx, IEnumWbemClassObject& ppEnum) + // at System.Management.ManagementObjectSearcher.Get() - RunOnSTAThread(GetImpl); + var parentProcessId = (int?)null; - var parentProcess = SafeGetProcessById(parentProcessId); - return parentProcess; + RunOnSTAThread(GetImpl); - void GetImpl() - { - var queryString = $"SELECT ParentProcessId FROM Win32_Process WHERE ProcessId = { AppEnvironment.ProcessId } AND SessionId = { AppEnvironment.SessionId }"; + var parentProcess = SafeGetProcessById(parentProcessId); + return parentProcess; - using var searcher = new ManagementObjectSearcher(queryString); - using var collection = searcher.Get(); - using var @object = collection.OfType().SingleOrDefault(); + void GetImpl() + { + var queryString = $"SELECT ParentProcessId FROM Win32_Process WHERE ProcessId = {AppEnvironment.ProcessId} AND SessionId = {AppEnvironment.SessionId}"; - if (@object is not null) - { - parentProcessId = (int)(uint)@object["ParentProcessId"]; - } + using var searcher = new ManagementObjectSearcher(queryString); + using var collection = searcher.Get(); + using var @object = collection.OfType().SingleOrDefault(); + + if (@object is not null) + { + parentProcessId = (int)(uint)@object["ParentProcessId"]; } } + } - public static IntPtr GetCurrentProcessMainWindowHandle() - { - using var current = Process.GetCurrentProcess(); - - return current.MainWindowHandle; - } + public static IntPtr GetCurrentProcessMainWindowHandle() + { + using var current = Process.GetCurrentProcess(); - public static IntPtr GetParentProcessMainWindowHandle() - { - using var parent = GetParentProcess(); - - if (parent is not null) - return parent.MainWindowHandle; + return current.MainWindowHandle; + } - return IntPtr.Zero; - } + public static IntPtr GetParentProcessMainWindowHandle() + { + using var parent = GetParentProcess(); - public static Process? SafeGetProcessById(int? processId) - { - if (processId is null) - return null; + if (parent is not null) + return parent.MainWindowHandle; - try - { - var process = Process.GetProcessById(processId.Value); // Throws ArgumentException if the process specified by the processId parameter is not running. + return IntPtr.Zero; + } - if (process.SessionId != AppEnvironment.SessionId) - return null; + public static Process? SafeGetProcessById(int? processId) + { + if (processId is null) + return null; - if (process.HasExited) - return null; + try + { + var process = Process.GetProcessById(processId.Value); // Throws ArgumentException if the process specified by the processId parameter is not running. - _ = process.ProcessName; // Throws InvalidOperationException if the process has exited, so the requested information is not available + if (process.SessionId != AppEnvironment.SessionId) + return null; - return process; - } - catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException) - { + if (process.HasExited) return null; - } - } - public static bool IsUserAdministrator() + _ = process.ProcessName; // Throws InvalidOperationException if the process has exited, so the requested information is not available + + return process; + } + catch (Exception ex) when (ex is ArgumentException || ex is InvalidOperationException) { - // Move to Infrastructure.Security namespace + return null; + } + } - using var windowsIdentity = WindowsIdentity.GetCurrent(); + public static bool IsUserAdministrator() + { + // Move to Infrastructure.Security namespace - if (windowsIdentity is not null) - { - var windowsPrincipal = new WindowsPrincipal(windowsIdentity); - var userClaims = new List(windowsPrincipal.UserClaims); + using var windowsIdentity = WindowsIdentity.GetCurrent(); - var builtinAdministratorsSid = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, domainSid: null); - var isUserAdministrator = windowsPrincipal.UserClaims.Any((claim) => claim.Value.Contains(builtinAdministratorsSid.Value)); + if (windowsIdentity is not null) + { + var windowsPrincipal = new WindowsPrincipal(windowsIdentity); + var userClaims = new List(windowsPrincipal.UserClaims); - return isUserAdministrator; - } + var builtinAdministratorsSid = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, domainSid: null); + var isUserAdministrator = windowsPrincipal.UserClaims.Any((claim) => claim.Value.Contains(builtinAdministratorsSid.Value)); - return false; + return isUserAdministrator; } - public static bool IsRunningAsAdministrator() - { - // Move to Infrastructure.Security namespace + return false; + } - using var windowsIdentity = WindowsIdentity.GetCurrent(); + public static bool IsRunningAsAdministrator() + { + // Move to Infrastructure.Security namespace - if (windowsIdentity?.Owner is not null) - { - var isRunningAsAdministrator = windowsIdentity.Owner.IsWellKnown(WellKnownSidType.BuiltinAdministratorsSid); - return isRunningAsAdministrator; - } + using var windowsIdentity = WindowsIdentity.GetCurrent(); - return false; + if (windowsIdentity?.Owner is not null) + { + var isRunningAsAdministrator = windowsIdentity.Owner.IsWellKnown(WellKnownSidType.BuiltinAdministratorsSid); + return isRunningAsAdministrator; } - public static bool SafePredicate(Func predicate) + return false; + } + + public static bool SafePredicate(Func predicate) + { + try { - try - { - return predicate(); - } - catch (Exception ex) when (ex is InvalidOperationException || ex is Win32Exception) - { - return false; - } + return predicate(); + } + catch (Exception ex) when (ex is InvalidOperationException || ex is Win32Exception) + { + return false; } } } diff --git a/src/Infrastructure/Helpers/TabularModelHelper.cs b/src/Infrastructure/Helpers/TabularModelHelper.cs index 04e024c6..e341a9ef 100644 --- a/src/Infrastructure/Helpers/TabularModelHelper.cs +++ b/src/Infrastructure/Helpers/TabularModelHelper.cs @@ -1,87 +1,86 @@ -namespace Sqlbi.Bravo.Infrastructure.Helpers -{ - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Security; - using Sqlbi.Bravo.Models.FormatDax; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Text; - using TOM = Microsoft.AnalysisServices.Tabular; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Security; +using Sqlbi.Bravo.Models.FormatDax; +using TOM = Microsoft.AnalysisServices.Tabular; + +namespace Sqlbi.Bravo.Infrastructure.Helpers; - internal static class TabularModelHelper +internal static class TabularModelHelper +{ + /// + /// Compute a string identifier for a specific version of a tabular model by using Name, Version and LastUpdate properties + /// + public static string GetDatabaseETag(string name, long version, DateTime lastUpdate) { - /// - /// Compute a string identifier for a specific version of a tabular model by using Name, Version and LastUpdate properties - /// - public static string GetDatabaseETag(string name, long version, DateTime lastUpdate) + var buffers = new byte[][] { - var buffers = new byte[][] - { - Encoding.UTF8.GetBytes(name), - BitConverter.GetBytes(version), - BitConverter.GetBytes(lastUpdate.Ticks) - }; + Encoding.UTF8.GetBytes(name), + BitConverter.GetBytes(version), + BitConverter.GetBytes(lastUpdate.Ticks) + }; - var etag = Cryptography.MD5Hash(buffers); - return etag; - } + var etag = Cryptography.MD5Hash(buffers); + return etag; + } - public static DatabaseUpdateResult Update(TOM.Database database, IEnumerable measures) - { - var databaseETag = database.GetETag(refresh: false); + public static DatabaseUpdateResult Update(TOM.Database database, IEnumerable measures) + { + var databaseETag = database.GetETag(refresh: false); - foreach (var formattedMeasure in measures) - { - if (formattedMeasure.ETag != databaseETag) - throw new BravoException(BravoProblem.TOMDatabaseUpdateConflictMeasure); + foreach (var formattedMeasure in measures) + { + if (formattedMeasure.ETag != databaseETag) + throw new BravoException(BravoProblem.TOMDatabaseUpdateConflictMeasure); - if (formattedMeasure.Errors?.Any() ?? false) - throw new BravoException(BravoProblem.TOMDatabaseUpdateErrorMeasure); + if (formattedMeasure.Errors?.Any() ?? false) + throw new BravoException(BravoProblem.TOMDatabaseUpdateErrorMeasure); - var unformattedMeasure = database.Model.Tables[formattedMeasure.TableName].Measures[formattedMeasure.Name]; - var formattedExpression = formattedMeasure.Expression.ApplyLineBreakStyle(formattedMeasure.LineBreakStyle); + var unformattedMeasure = database.Model.Tables[formattedMeasure.TableName].Measures[formattedMeasure.Name]; + var formattedExpression = formattedMeasure.Expression.ApplyLineBreakStyle(formattedMeasure.LineBreakStyle); - if (unformattedMeasure.Expression != formattedExpression) - unformattedMeasure.Expression = formattedExpression; - } + if (unformattedMeasure.Expression != formattedExpression) + unformattedMeasure.Expression = formattedExpression; + } - if (database.Model.HasLocalChanges) + if (database.Model.HasLocalChanges) + { + try { - try - { - database.Model.SaveChanges().ThrowOnError(); - } - catch (Microsoft.AnalysisServices.OperationException ex) - { - throw new BravoException(BravoProblem.TOMDatabaseUpdateFailed, ex.Message, ex); - } - databaseETag = database.GetETag(); + database.Model.SaveChanges().ThrowOnError(); } - - var updateResult = new DatabaseUpdateResult + catch (Microsoft.AnalysisServices.OperationException ex) { - DatabaseETag = databaseETag - }; - - return updateResult; + throw new BravoException(BravoProblem.TOMDatabaseUpdateFailed, ex.Message, ex); + } + databaseETag = database.GetETag(); } - public static bool IsValidTableName(string? tableName) + var updateResult = new DatabaseUpdateResult { - if (tableName.IsNullOrWhiteSpace()) - return false; + DatabaseETag = databaseETag + }; - if (tableName.Any(char.IsControl)) - return false; + return updateResult; + } - return true; - } + public static bool IsValidTableName(string? tableName) + { + if (tableName.IsNullOrWhiteSpace()) + return false; - public static string GetDaxTableName(string? tableName) - { - var daxTableName = $"'{ tableName?.Replace("'", "''") }'"; - return daxTableName; - } + if (tableName.Any(char.IsControl)) + return false; + + return true; + } + + public static string GetDaxTableName(string? tableName) + { + var daxTableName = $"'{tableName?.Replace("'", "''")}'"; + return daxTableName; } -} \ No newline at end of file +} diff --git a/src/Infrastructure/Helpers/ThemeHelper.cs b/src/Infrastructure/Helpers/ThemeHelper.cs index b2d2fb82..da8d9eb2 100644 --- a/src/Infrastructure/Helpers/ThemeHelper.cs +++ b/src/Infrastructure/Helpers/ThemeHelper.cs @@ -1,156 +1,155 @@ -namespace Sqlbi.Bravo.Infrastructure.Helpers -{ - using Sqlbi.Bravo.Infrastructure.Configuration.Settings; - using Sqlbi.Bravo.Infrastructure.Windows.Interop; - using System; - using System.Diagnostics; - using System.Runtime.InteropServices; +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Sqlbi.Bravo.Infrastructure.Configuration.Settings; +using Sqlbi.Bravo.Infrastructure.Windows.Interop; + +namespace Sqlbi.Bravo.Infrastructure.Helpers; - internal static class ThemeHelper +internal static class ThemeHelper +{ + private static readonly Version Windows10Version1809 = new(10, 0, 17763); + //private static readonly Version Windows10Version1909 = new(10, 0, 18363); + //private static readonly Version Windows10Version21H1 = new(10, 0, 19043); + private static readonly Version Windows10Version21H2 = new(10, 0, 22000); + private static readonly bool IsWindows10Version1809 = Environment.OSVersion.Version == Windows10Version1809; + //private static readonly bool IsWindows10Version1909OrNewer = Environment.OSVersion.Version >= Windows10Version1909; + //private static readonly bool IsWindows10Version21H1OrNewer = Environment.OSVersion.Version >= Windows10Version21H1; + private static readonly bool IsWindows10Version21H2OrNewer = Environment.OSVersion.Version >= Windows10Version21H2; + private static readonly bool IsDarkModeSupported = Environment.OSVersion.Version >= Windows10Version1809; + + public static void InitializeTheme(IntPtr hWnd, ThemeType theme) { - private static readonly Version Windows10Version1809 = new(10, 0, 17763); - //private static readonly Version Windows10Version1909 = new(10, 0, 18363); - //private static readonly Version Windows10Version21H1 = new(10, 0, 19043); - private static readonly Version Windows10Version21H2 = new(10, 0, 22000); - private static readonly bool IsWindows10Version1809 = Environment.OSVersion.Version == Windows10Version1809; - //private static readonly bool IsWindows10Version1909OrNewer = Environment.OSVersion.Version >= Windows10Version1909; - //private static readonly bool IsWindows10Version21H1OrNewer = Environment.OSVersion.Version >= Windows10Version21H1; - private static readonly bool IsWindows10Version21H2OrNewer = Environment.OSVersion.Version >= Windows10Version21H2; - private static readonly bool IsDarkModeSupported = Environment.OSVersion.Version >= Windows10Version1809; - - public static void InitializeTheme(IntPtr hWnd, ThemeType theme) + if (IsDarkModeSupported) { - if (IsDarkModeSupported) + if (IsWindows10Version1809) { - if (IsWindows10Version1809) - { - _ = Uxtheme.AllowDarkModeForApp(allow: true); - } - else - { - _ = Uxtheme.SetPreferredAppMode(mode: Uxtheme.PreferredAppMode.AllowDark); - } + _ = Uxtheme.AllowDarkModeForApp(allow: true); + } + else + { + _ = Uxtheme.SetPreferredAppMode(mode: Uxtheme.PreferredAppMode.AllowDark); + } - Uxtheme.RefreshImmersiveColorPolicyState(); + Uxtheme.RefreshImmersiveColorPolicyState(); - RefreshNonClientArea(hWnd, theme); - } + RefreshNonClientArea(hWnd, theme); } + } - public static void ChangeTheme(ThemeType theme) + public static void ChangeTheme(ThemeType theme) + { + if (IsDarkModeSupported) { - if (IsDarkModeSupported) - { - var hWnd = ProcessHelper.GetCurrentProcessMainWindowHandle(); + var hWnd = ProcessHelper.GetCurrentProcessMainWindowHandle(); - ChangeTheme(hWnd, theme); - } + ChangeTheme(hWnd, theme); } + } - public static void ChangeTheme(IntPtr hWnd, ThemeType theme) + public static void ChangeTheme(IntPtr hWnd, ThemeType theme) + { + if (IsDarkModeSupported) { - if (IsDarkModeSupported) - { - RefreshNonClientArea(hWnd, theme); - } + RefreshNonClientArea(hWnd, theme); } + } - public static bool ShouldUseDarkMode(ThemeType theme) + public static bool ShouldUseDarkMode(ThemeType theme) + { + if (IsDarkModeSupported /* && !SystemInformation.HighContrast */) { - if (IsDarkModeSupported /* && !SystemInformation.HighContrast */) - { - bool useDarkMode; + bool useDarkMode; - if (theme == ThemeType.Auto) - { - useDarkMode = Uxtheme.ShouldAppsUseDarkMode(); - } - else - { - useDarkMode = theme == ThemeType.Dark; // Uxtheme.IsDarkModeAllowedForWindow(hWnd); - } - - return useDarkMode; + if (theme == ThemeType.Auto) + { + useDarkMode = Uxtheme.ShouldAppsUseDarkMode(); + } + else + { + useDarkMode = theme == ThemeType.Dark; // Uxtheme.IsDarkModeAllowedForWindow(hWnd); } - return false; + return useDarkMode; } - private static void RefreshNonClientArea(IntPtr hWnd, ThemeType theme) - { - Debug.Assert(IsDarkModeSupported); + return false; + } + + private static void RefreshNonClientArea(IntPtr hWnd, ThemeType theme) + { + Debug.Assert(IsDarkModeSupported); + + var useDarkMode = ShouldUseDarkMode(theme); - var useDarkMode = ShouldUseDarkMode(theme); + if (IsWindows10Version21H2OrNewer) // >= Windows 11 + { + // Undocumented DWMWINDOWATTRIBUTE supported on Windows 11 only + const Dwmapi.DWMWINDOWATTRIBUTE DWMWA_CAPTION_COLOR = (Dwmapi.DWMWINDOWATTRIBUTE)35; + //const Dwmapi.DWMWINDOWATTRIBUTE DWMWA_TEXT_COLOR = (Dwmapi.DWMWINDOWATTRIBUTE)36; - if (IsWindows10Version21H2OrNewer) // >= Windows 11 + COLORREF color = useDarkMode ? AppEnvironment.ThemeColorDark : AppEnvironment.ThemeColorLight; + GCHandle pinnedColor = GCHandle.Alloc(color, GCHandleType.Pinned); + try { - // Undocumented DWMWINDOWATTRIBUTE supported on Windows 11 only - const Dwmapi.DWMWINDOWATTRIBUTE DWMWA_CAPTION_COLOR = (Dwmapi.DWMWINDOWATTRIBUTE)35; - //const Dwmapi.DWMWINDOWATTRIBUTE DWMWA_TEXT_COLOR = (Dwmapi.DWMWINDOWATTRIBUTE)36; + var dwAttribute = DWMWA_CAPTION_COLOR; + var pvAttribute = pinnedColor.AddrOfPinnedObject(); + var cbAttribute = Marshal.SizeOf(color); - COLORREF color = useDarkMode ? AppEnvironment.ThemeColorDark : AppEnvironment.ThemeColorLight; - GCHandle pinnedColor = GCHandle.Alloc(color, GCHandleType.Pinned); - try - { - var dwAttribute = DWMWA_CAPTION_COLOR; - var pvAttribute = pinnedColor.AddrOfPinnedObject(); - var cbAttribute = Marshal.SizeOf(color); + _ = Dwmapi.DwmSetWindowAttribute(hWnd, dwAttribute, pvAttribute, cbAttribute); + } + finally + { + if (pinnedColor.IsAllocated) + pinnedColor.Free(); + } + } + else // if (IsWindows10Version1909OrNewer) + { + var size = Marshal.SizeOf(useDarkMode); + var ptr = Marshal.AllocHGlobal(size); + try + { + Marshal.WriteInt32(ptr, ofs: 0, val: useDarkMode ? 1 : 0); - _ = Dwmapi.DwmSetWindowAttribute(hWnd, dwAttribute, pvAttribute, cbAttribute); - } - finally + var data = new User32.WINDOWCOMPOSITIONATTRIBDATA { - if (pinnedColor.IsAllocated) - pinnedColor.Free(); - } + Attrib = User32.WINDOWCOMPOSITIONATTRIB.WCA_USEDARKMODECOLORS, + pvData = ptr, + cbData = size + }; + + _ = User32.SetWindowCompositionAttribute(hWnd, ref data); + + // >> HACK: To force non-client area repainting + // >> Win10 20H2 build 19042 + var forceActive = new IntPtr(1); /* TRUE */ + var forceInactive = IntPtr.Zero; /* FALSE */ + _ = User32.SendMessage(hWnd, WindowMessage.WM_NCACTIVATE, wParam: forceInactive, IntPtr.Zero); + _ = User32.SendMessage(hWnd, WindowMessage.WM_NCACTIVATE, wParam: forceActive, IntPtr.Zero); + // << HACK } - else // if (IsWindows10Version1909OrNewer) + finally { - var size = Marshal.SizeOf(useDarkMode); - var ptr = Marshal.AllocHGlobal(size); - try - { - Marshal.WriteInt32(ptr, ofs: 0, val: useDarkMode ? 1 : 0); - - var data = new User32.WINDOWCOMPOSITIONATTRIBDATA - { - Attrib = User32.WINDOWCOMPOSITIONATTRIB.WCA_USEDARKMODECOLORS, - pvData = ptr, - cbData = size - }; - - _ = User32.SetWindowCompositionAttribute(hWnd, ref data); - - // >> HACK: To force non-client area repainting - // >> Win10 20H2 build 19042 - var forceActive = new IntPtr(1); /* TRUE */ - var forceInactive = IntPtr.Zero; /* FALSE */ - _ = User32.SendMessage(hWnd, WindowMessage.WM_NCACTIVATE, wParam: forceInactive, IntPtr.Zero); - _ = User32.SendMessage(hWnd, WindowMessage.WM_NCACTIVATE, wParam: forceActive, IntPtr.Zero); - // << HACK - } - finally - { - Marshal.FreeHGlobal(ptr); - } + Marshal.FreeHGlobal(ptr); } - //else - //{ - // const Dwmapi.DWMWINDOWATTRIBUTE DWMWA_USE_IMMERSIVE_DARK_MODE_19 = (Dwmapi.DWMWINDOWATTRIBUTE)19; - // const Dwmapi.DWMWINDOWATTRIBUTE DWMWA_USE_IMMERSIVE_DARK_MODE_20 = (Dwmapi.DWMWINDOWATTRIBUTE)20; - - // var dwAttribute = DWMWA_USE_IMMERSIVE_DARK_MODE_20; - // var pvAttribute = new IntPtr(useDarkMode ? 1 : 0); - // var cbAttribute = Marshal.SizeOf(pvAttribute); - - // var hresult = Dwmapi.DwmSetWindowAttribute(hWnd, dwAttribute, pvAttribute, cbAttribute); - // if (hresult != HRESULT.S_OK) - // { - // dwAttribute = DWMWA_USE_IMMERSIVE_DARK_MODE_19; - - // _ = Dwmapi.DwmSetWindowAttribute(hWnd, dwAttribute, pvAttribute, cbAttribute); - // } - //} } + //else + //{ + // const Dwmapi.DWMWINDOWATTRIBUTE DWMWA_USE_IMMERSIVE_DARK_MODE_19 = (Dwmapi.DWMWINDOWATTRIBUTE)19; + // const Dwmapi.DWMWINDOWATTRIBUTE DWMWA_USE_IMMERSIVE_DARK_MODE_20 = (Dwmapi.DWMWINDOWATTRIBUTE)20; + + // var dwAttribute = DWMWA_USE_IMMERSIVE_DARK_MODE_20; + // var pvAttribute = new IntPtr(useDarkMode ? 1 : 0); + // var cbAttribute = Marshal.SizeOf(pvAttribute); + + // var hresult = Dwmapi.DwmSetWindowAttribute(hWnd, dwAttribute, pvAttribute, cbAttribute); + // if (hresult != HRESULT.S_OK) + // { + // dwAttribute = DWMWA_USE_IMMERSIVE_DARK_MODE_19; + + // _ = Dwmapi.DwmSetWindowAttribute(hWnd, dwAttribute, pvAttribute, cbAttribute); + // } + //} } } diff --git a/src/Infrastructure/Helpers/TokenCacheHelper.cs b/src/Infrastructure/Helpers/TokenCacheHelper.cs index ddb7fb3f..68c69f08 100644 --- a/src/Infrastructure/Helpers/TokenCacheHelper.cs +++ b/src/Infrastructure/Helpers/TokenCacheHelper.cs @@ -1,81 +1,80 @@ -namespace Sqlbi.Bravo.Infrastructure.Helpers +using System; +using System.IO; +using System.Security.Cryptography; +using Microsoft.Identity.Client; +using Sqlbi.Bravo.Infrastructure.Security; +using Sqlbi.Bravo.Models; + +namespace Sqlbi.Bravo.Infrastructure.Helpers; + +internal static class TokenCacheHelper { - using Microsoft.Identity.Client; - using Sqlbi.Bravo.Infrastructure.Security; - using Sqlbi.Bravo.Models; - using System; - using System.IO; - using System.Security.Cryptography; + private static readonly object _tokenCacheLock = new(); - internal static class TokenCacheHelper + private static void BeforeAccessCallback(TokenCacheNotificationArgs args) { - private static readonly object _tokenCacheLock = new(); - - private static void BeforeAccessCallback(TokenCacheNotificationArgs args) + lock (_tokenCacheLock) { - lock (_tokenCacheLock) - { - byte[]? cachedBytes = null; + byte[]? cachedBytes = null; - if (File.Exists(AppEnvironment.MsalTokenCacheFilePath)) + if (File.Exists(AppEnvironment.MsalTokenCacheFilePath)) + { + var encryptedBytes = File.ReadAllBytes(AppEnvironment.MsalTokenCacheFilePath); + if (encryptedBytes.Length > 0) { - var encryptedBytes = File.ReadAllBytes(AppEnvironment.MsalTokenCacheFilePath); - if (encryptedBytes.Length > 0) + try { - try - { - cachedBytes = Cryptography.Unprotect(encryptedBytes); - } - catch (CryptographicException ex) - { - AppEnvironment.AddDiagnostics(name: $"{nameof(TokenCacheHelper)}.{nameof(BeforeAccessCallback)}", ex, DiagnosticMessageSeverity.Warning); + cachedBytes = Cryptography.Unprotect(encryptedBytes); + } + catch (CryptographicException ex) + { + AppEnvironment.AddDiagnostics(name: $"{nameof(TokenCacheHelper)}.{nameof(BeforeAccessCallback)}", ex, DiagnosticMessageSeverity.Warning); - // Delete the file in order to force a new authentication - File.Delete(AppEnvironment.MsalTokenCacheFilePath); - } + // Delete the file in order to force a new authentication + File.Delete(AppEnvironment.MsalTokenCacheFilePath); } } - - args.TokenCache.DeserializeMsalV3(cachedBytes); } + + args.TokenCache.DeserializeMsalV3(cachedBytes); } + } - private static void AfterAccessCallback(TokenCacheNotificationArgs args) + private static void AfterAccessCallback(TokenCacheNotificationArgs args) + { + if (args.HasStateChanged) // if the access operation resulted in a cache update { - if (args.HasStateChanged) // if the access operation resulted in a cache update + lock (_tokenCacheLock) { - lock (_tokenCacheLock) - { - var cachedBytes = args.TokenCache.SerializeMsalV3(); - var encryptedBytes = Cryptography.Protect(cachedBytes); + var cachedBytes = args.TokenCache.SerializeMsalV3(); + var encryptedBytes = Cryptography.Protect(cachedBytes); - File.WriteAllBytes(AppEnvironment.MsalTokenCacheFilePath, encryptedBytes); - } + File.WriteAllBytes(AppEnvironment.MsalTokenCacheFilePath, encryptedBytes); } } + } - /// - /// Registers a token cache to synchronize with the persistent storage. - /// - /// The application token cache, typically referenced as - /// Call to have the given token cache stop syncronizing. - public static void RegisterCache(ITokenCache tokenCache) - { - ArgumentNullException.ThrowIfNull(tokenCache); + /// + /// Registers a token cache to synchronize with the persistent storage. + /// + /// The application token cache, typically referenced as + /// Call to have the given token cache stop syncronizing. + public static void RegisterCache(ITokenCache tokenCache) + { + ArgumentNullException.ThrowIfNull(tokenCache); - tokenCache.SetBeforeAccess(BeforeAccessCallback); - tokenCache.SetAfterAccess(AfterAccessCallback); - } + tokenCache.SetBeforeAccess(BeforeAccessCallback); + tokenCache.SetAfterAccess(AfterAccessCallback); + } - /// - /// Unregisters a token cache so it no longer synchronizes with on disk storage. - /// - public static void UnregisterCache(ITokenCache tokenCache) - { - ArgumentNullException.ThrowIfNull(tokenCache); + /// + /// Unregisters a token cache so it no longer synchronizes with on disk storage. + /// + public static void UnregisterCache(ITokenCache tokenCache) + { + ArgumentNullException.ThrowIfNull(tokenCache); - tokenCache.SetBeforeAccess(beforeAccess: null); - tokenCache.SetAfterAccess(afterAccess: null); - } + tokenCache.SetBeforeAccess(beforeAccess: null); + tokenCache.SetAfterAccess(afterAccess: null); } } diff --git a/src/Infrastructure/Helpers/VpaxHelper.cs b/src/Infrastructure/Helpers/VpaxHelper.cs index 5ec1b225..9f519e3e 100644 --- a/src/Infrastructure/Helpers/VpaxHelper.cs +++ b/src/Infrastructure/Helpers/VpaxHelper.cs @@ -1,87 +1,88 @@ -namespace Sqlbi.Bravo.Infrastructure.Helpers -{ - using Dax.Metadata; - using Dax.Model.Extractor; - using Dax.Vpax.Tools; - using Sqlbi.Bravo.Infrastructure.Services; +using System.IO; +using System.Threading; +using Dax.Metadata; +using Dax.Model.Extractor; +using Dax.Vpax.Tools; +using Sqlbi.Bravo.Infrastructure.Services; + +namespace Sqlbi.Bravo.Infrastructure.Helpers; - internal static class VpaxHelper +internal static class VpaxHelper +{ + public static void ExportVpax(Stream stream, TabularConnectionWrapper connection, CancellationToken cancellationToken) { - public static void ExportVpax(Stream stream, TabularConnectionWrapper connection, CancellationToken cancellationToken) - { - var daxModel = GetDaxModel(connection, statisticsEnabled: true, cancellationToken); + var daxModel = GetDaxModel(connection, statisticsEnabled: true, cancellationToken); - // Bravo always includes the DaxVpaView.json and Model.bim in the VPAX file. - var vpaModel = new Dax.ViewVpaExport.Model(daxModel); - var tomDatabase = connection.Database; + // Bravo always includes the DaxVpaView.json and Model.bim in the VPAX file. + var vpaModel = new Dax.ViewVpaExport.Model(daxModel); + var tomDatabase = connection.Database; - try - { - VpaxTools.ExportVpax(stream, daxModel, vpaModel, tomDatabase); - } - catch (IOException ex) - { - throw new BravoException(BravoProblem.VpaxFileExportError, ex.Message, ex); - } + try + { + VpaxTools.ExportVpax(stream, daxModel, vpaModel, tomDatabase); } - - public static Model GetDaxModel(Stream stream) + catch (IOException ex) { - Model? model; + throw new BravoException(BravoProblem.VpaxFileExportError, ex.Message, ex); + } + } - try - { - model = VpaxTools.ImportVpax(stream).DaxModel; - } - catch (FileFormatException ex) - { - throw new BravoException(BravoProblem.VpaxFileImportError, ex.Message, ex); - } + public static Model GetDaxModel(Stream stream) + { + Model? model; - if (model is null) - { - // If DaxModel is null at this stage, the archive must be considered invalid - // or corrupted, for example if it does not contain the parts required by the - // ECMA-376 specification. This may also occur if the underlying - // System.IO.Packaging.Package was not properly finalized during creation - // (i.e., not correctly closed or disposed), such as when an error happened - // while flushing the stream. - throw new BravoException(BravoProblem.VpaxFileImportError, "The VPAX file may be invalid or corrupted."); - } + try + { + model = VpaxTools.ImportVpax(stream).DaxModel; + } + catch (FileFormatException ex) + { + throw new BravoException(BravoProblem.VpaxFileImportError, ex.Message, ex); + } - return model; + if (model is null) + { + // If DaxModel is null at this stage, the archive must be considered invalid + // or corrupted, for example if it does not contain the parts required by the + // ECMA-376 specification. This may also occur if the underlying + // System.IO.Packaging.Package was not properly finalized during creation + // (i.e., not correctly closed or disposed), such as when an error happened + // while flushing the stream. + throw new BravoException(BravoProblem.VpaxFileImportError, "The VPAX file may be invalid or corrupted."); } - public static Model GetDaxModel(TabularConnectionWrapper connectionWrapper, bool statisticsEnabled, CancellationToken cancellationToken) + return model; + } + + public static Model GetDaxModel(TabularConnectionWrapper connectionWrapper, bool statisticsEnabled, CancellationToken cancellationToken) + { + var server = connectionWrapper.Server; + var database = connectionWrapper.Database; + var daxModel = TomExtractor.GetDaxModel(database.Model, extractorApp: AppEnvironment.ApplicationName, extractorVersion: AppEnvironment.VersionInfo.Version); + + using var connection = connectionWrapper.CreateAdomdConnection(open: false); { - var server = connectionWrapper.Server; - var database = connectionWrapper.Database; - var daxModel = TomExtractor.GetDaxModel(database.Model, extractorApp: AppEnvironment.ApplicationName, extractorVersion: AppEnvironment.VersionInfo.Version); + cancellationToken.ThrowIfCancellationRequested(); + DmvExtractor.PopulateFromDmv(daxModel, connection, server.Name, database.Name, extractorApp: AppEnvironment.ApplicationName, extractorVersion: AppEnvironment.VersionInfo.Version); - using var connection = connectionWrapper.CreateAdomdConnection(open: false); + if (statisticsEnabled) { + // TODO: Currently, we are forcing a full stats collection from DirectQuery and DirectLake partitions. We might consider parameterizing this behavior in the future + var analyzeDirectQuery = true; + var analyzeDirectLake = DirectLakeExtractionMode.Full; + var referentialIntegrityViolationSampleRows = 0; // RI violation sampling is not required for model analysis in Bravo nor for VPAX export. + cancellationToken.ThrowIfCancellationRequested(); - DmvExtractor.PopulateFromDmv(daxModel, connection, server.Name, database.Name, extractorApp: AppEnvironment.ApplicationName, extractorVersion: AppEnvironment.VersionInfo.Version); + StatExtractor.UpdateStatisticsModel(daxModel, connection, referentialIntegrityViolationSampleRows, analyzeDirectQuery, analyzeDirectLake); // TOFIX: remove deprecated (requires refactoring VertiPaqAnalyzer APIs) - if (statisticsEnabled) + if (analyzeDirectLake > DirectLakeExtractionMode.ResidentOnly && daxModel.HasDirectLakePartitions()) { - // TODO: Currently, we are forcing a full stats collection from DirectQuery and DirectLake partitions. We might consider parameterizing this behavior in the future - var analyzeDirectQuery = true; - var analyzeDirectLake = DirectLakeExtractionMode.Full; - var referentialIntegrityViolationSampleRows = 0; // RI violation sampling is not required for model analysis in Bravo nor for VPAX export. - cancellationToken.ThrowIfCancellationRequested(); - StatExtractor.UpdateStatisticsModel(daxModel, connection, referentialIntegrityViolationSampleRows, analyzeDirectQuery, analyzeDirectLake); // TOFIX: remove deprecated (requires refactoring VertiPaqAnalyzer APIs) - - if (analyzeDirectLake > DirectLakeExtractionMode.ResidentOnly && daxModel.HasDirectLakePartitions()) - { - cancellationToken.ThrowIfCancellationRequested(); - DmvExtractor.PopulateFromDmv(daxModel, connection, server.Name, database.Name, extractorApp: AppEnvironment.ApplicationName, extractorVersion: AppEnvironment.VersionInfo.Version); - } + DmvExtractor.PopulateFromDmv(daxModel, connection, server.Name, database.Name, extractorApp: AppEnvironment.ApplicationName, extractorVersion: AppEnvironment.VersionInfo.Version); } } - - return daxModel; } + + return daxModel; } } diff --git a/src/Infrastructure/Helpers/VpaxObfuscatorHelper.cs b/src/Infrastructure/Helpers/VpaxObfuscatorHelper.cs index 8ba19584..05f50d69 100644 --- a/src/Infrastructure/Helpers/VpaxObfuscatorHelper.cs +++ b/src/Infrastructure/Helpers/VpaxObfuscatorHelper.cs @@ -1,9 +1,11 @@ -namespace Sqlbi.Bravo.Infrastructure.Helpers; - +using System; +using System.IO; using Dax.Metadata; using Dax.Vpax.Obfuscator; using Dax.Vpax.Obfuscator.Common; +namespace Sqlbi.Bravo.Infrastructure.Helpers; + internal static class VpaxObfuscatorHelper { public static void ObfuscateAndExportDictionary(Stream vpaxStream, string path) diff --git a/src/Infrastructure/Helpers/WebView2Helper.cs b/src/Infrastructure/Helpers/WebView2Helper.cs index ed756ac6..76ffd139 100644 --- a/src/Infrastructure/Helpers/WebView2Helper.cs +++ b/src/Infrastructure/Helpers/WebView2Helper.cs @@ -1,245 +1,244 @@ -namespace Sqlbi.Bravo.Infrastructure.Helpers +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Reflection; +using System.Windows.Forms; +using Microsoft.Web.WebView2.Core; +using Sqlbi.Bravo.Infrastructure.Configuration.Settings; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Windows; +using Sqlbi.Bravo.Infrastructure.Windows.Interop; + +namespace Sqlbi.Bravo.Infrastructure.Helpers; + +internal static class WebView2Helper { - using Microsoft.Web.WebView2.Core; - using Sqlbi.Bravo.Infrastructure.Configuration.Settings; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Windows; - using Sqlbi.Bravo.Infrastructure.Windows.Interop; - using System; - using System.Collections.Generic; - using System.Diagnostics; - using System.IO; - using System.Linq; - using System.Net; - using System.Net.Http; - using System.Reflection; - using System.Windows.Forms; - - internal static class WebView2Helper - { - //[DllImport(ExternDll.WebView2Loader)] - //internal static extern int GetAvailableCoreWebView2BrowserVersionString([In][MarshalAs(UnmanagedType.LPWStr)] string? browserExecutableFolder, [MarshalAs(UnmanagedType.LPWStr)] ref string versionInfo); + //[DllImport(ExternDll.WebView2Loader)] + //internal static extern int GetAvailableCoreWebView2BrowserVersionString([In][MarshalAs(UnmanagedType.LPWStr)] string? browserExecutableFolder, [MarshalAs(UnmanagedType.LPWStr)] ref string versionInfo); - /// - /// The Bootstrapper is a tiny installer that downloads the Evergreen Runtime matching device architecture and installs it locally. - /// https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section - /// - public static string EvergreenRuntimeBootstrapperUrl = "https://go.microsoft.com/fwlink/p/?LinkId=2124703"; - public static string MicrosoftReferenceUrl = "https://developer.microsoft.com/en-us/microsoft-edge/webview2"; + /// + /// The Bootstrapper is a tiny installer that downloads the Evergreen Runtime matching device architecture and installs it locally. + /// https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section + /// + public static string EvergreenRuntimeBootstrapperUrl = "https://go.microsoft.com/fwlink/p/?LinkId=2124703"; + public static string MicrosoftReferenceUrl = "https://developer.microsoft.com/en-us/microsoft-edge/webview2"; - public static void TryAndIgnoreUnsupportedError(Action action) + public static void TryAndIgnoreUnsupportedError(Action action) + { + // + // Feature-detecting to test whether the installed Runtime supports recently added APIs + // https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/versioning#feature-detecting-to-test-whether-the-installed-runtime-supports-recently-added-apis + // + try { - // - // Feature-detecting to test whether the installed Runtime supports recently added APIs - // https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/versioning#feature-detecting-to-test-whether-the-installed-runtime-supports-recently-added-apis - // - try - { - action?.Invoke(); - } - catch (NotImplementedException ex) when (ex.InnerException is InvalidCastException innerEx && innerEx.HResult == HRESULT.E_NOINTERFACE) - { - // Ignore unsupported feature - } - catch (InvalidCastException ex) when (ex.HResult == HRESULT.E_NOINTERFACE) - { - // Ignore unsupported feature - } + action?.Invoke(); } - - public static string? GetRuntimeVersionInfo() + catch (NotImplementedException ex) when (ex.InnerException is InvalidCastException innerEx && innerEx.HResult == HRESULT.E_NOINTERFACE) { - try - { - var versionInfo = CoreWebView2Environment.GetAvailableBrowserVersionString(browserExecutableFolder: null); - return versionInfo; - } - catch (WebView2RuntimeNotFoundException) - { - return null; - } -/* - var versionInfo = (string?)null; -#pragma warning disable CS8601 // Possible null reference assignment. - var errorCode = GetAvailableCoreWebView2BrowserVersionString(browserExecutableFolder: null, ref versionInfo); -#pragma warning restore CS8601 // Possible null reference assignment. - if (errorCode == HRESULT.E_FILENOTFOUND) - { - // WebView2 runtime not found - return null; - } + // Ignore unsupported feature + } + catch (InvalidCastException ex) when (ex.HResult == HRESULT.E_NOINTERFACE) + { + // Ignore unsupported feature + } + } - Marshal.ThrowExceptionForHR(errorCode); + public static string? GetRuntimeVersionInfo() + { + try + { + var versionInfo = CoreWebView2Environment.GetAvailableBrowserVersionString(browserExecutableFolder: null); return versionInfo; -*/ } - - public static void EnsureRuntimeIsInstalled() + catch (WebView2RuntimeNotFoundException) { - if (AppEnvironment.IsWebView2RuntimeInstalled) - return; + return null; + } + /* + var versionInfo = (string?)null; + #pragma warning disable CS8601 // Possible null reference assignment. + var errorCode = GetAvailableCoreWebView2BrowserVersionString(browserExecutableFolder: null, ref versionInfo); + #pragma warning restore CS8601 // Possible null reference assignment. + if (errorCode == HRESULT.E_FILENOTFOUND) + { + // WebView2 runtime not found + return null; + } - var heading = $"{ AppEnvironment.ApplicationMainWindowTitle } requires the Microsoft Edge WebView2 runtime which is not currently installed.\r\n\r\nChoose an option to proceed with the installation:"; - var footnoteText = $"For more details please refer to the following address:\r\n\r\n - { AppEnvironment.ApplicationWebsiteUrl }\r\n - { MicrosoftReferenceUrl }"; - var automaticButton = new TaskDialogCommandLinkButton("&Automatic", "Download and install Microsoft Edge WebView2 runtime now"); - var manualButton = new TaskDialogCommandLinkButton("&Manual", "Open the browser on the download page"); - var cancelButton = new TaskDialogCommandLinkButton("&Cancel", "Close the application without installing"); + Marshal.ThrowExceptionForHR(errorCode); + return versionInfo; + */ + } - var dialogButton = MessageDialog.ShowDialog(heading, text: null, footnoteText, allowCancel: false, automaticButton, manualButton, cancelButton); + public static void EnsureRuntimeIsInstalled() + { + if (AppEnvironment.IsWebView2RuntimeInstalled) + return; - if (dialogButton == automaticButton) - { - DownloadAndInstallRuntime(); - } - else if (dialogButton == manualButton) - { - var address = new Uri(MicrosoftReferenceUrl, uriKind: UriKind.Absolute); - _ = ProcessHelper.OpenBrowser(address); - } - else if (dialogButton == cancelButton) - { - // - } + var heading = $"{AppEnvironment.ApplicationMainWindowTitle} requires the Microsoft Edge WebView2 runtime which is not currently installed.\r\n\r\nChoose an option to proceed with the installation:"; + var footnoteText = $"For more details please refer to the following address:\r\n\r\n - {AppEnvironment.ApplicationWebsiteUrl}\r\n - {MicrosoftReferenceUrl}"; + var automaticButton = new TaskDialogCommandLinkButton("&Automatic", "Download and install Microsoft Edge WebView2 runtime now"); + var manualButton = new TaskDialogCommandLinkButton("&Manual", "Open the browser on the download page"); + var cancelButton = new TaskDialogCommandLinkButton("&Cancel", "Close the application without installing"); - Environment.Exit(NativeMethods.ERROR_SUCCESS); - } + var dialogButton = MessageDialog.ShowDialog(heading, text: null, footnoteText, allowCancel: false, automaticButton, manualButton, cancelButton); - private static void DownloadAndInstallRuntime() + if (dialogButton == automaticButton) + { + DownloadAndInstallRuntime(); + } + else if (dialogButton == manualButton) + { + var address = new Uri(MicrosoftReferenceUrl, uriKind: UriKind.Absolute); + _ = ProcessHelper.OpenBrowser(address); + } + else if (dialogButton == cancelButton) { - // TODO: use http client from pool, add proxy support - using var httpClient = new HttpClient(); + // + } - var fileBytes = httpClient.GetByteArrayAsync(EvergreenRuntimeBootstrapperUrl).GetAwaiter().GetResult(); - var filePath = Path.Combine(AppEnvironment.ApplicationTempPath, $"MicrosoftEdgeWebview2Setup-{DateTime.Now:yyyyMMddHHmmss}.exe"); + Environment.Exit(NativeMethods.ERROR_SUCCESS); + } - File.WriteAllBytes(filePath, fileBytes); + private static void DownloadAndInstallRuntime() + { + // TODO: use http client from pool, add proxy support + using var httpClient = new HttpClient(); - using var process = Process.Start(filePath); // add switches ? i.e. /silent /install - process.WaitForExit(); + var fileBytes = httpClient.GetByteArrayAsync(EvergreenRuntimeBootstrapperUrl).GetAwaiter().GetResult(); + var filePath = Path.Combine(AppEnvironment.ApplicationTempPath, $"MicrosoftEdgeWebview2Setup-{DateTime.Now:yyyyMMddHHmmss}.exe"); - if (process.ExitCode != NativeMethods.ERROR_SUCCESS) - { - ExceptionHelper.WriteToEventLog($"WebView2 bootstrapper exit code '{ process.ExitCode }'", EventLogEntryType.Warning); - } + File.WriteAllBytes(filePath, fileBytes); + + using var process = Process.Start(filePath); // add switches ? i.e. /silent /install + process.WaitForExit(); + + if (process.ExitCode != NativeMethods.ERROR_SUCCESS) + { + ExceptionHelper.WriteToEventLog($"WebView2 bootstrapper exit code '{process.ExitCode}'", EventLogEntryType.Warning); } + } - public static string GetProxyArguments(ProxySettings? proxySettings, IWebProxy systemProxy) + public static string GetProxyArguments(ProxySettings? proxySettings, IWebProxy systemProxy) + { + // Command-line options for proxy settings + // https://docs.microsoft.com/en-us/deployedge/edge-learnmore-cmdline-options-proxy-settings#command-line-options-for-proxy-settings + + var proxyArguments = (proxySettings?.Type) switch { - // Command-line options for proxy settings - // https://docs.microsoft.com/en-us/deployedge/edge-learnmore-cmdline-options-proxy-settings#command-line-options-for-proxy-settings + ProxyType.None => "--no-proxy-server", + ProxyType.Custom => GetCustomProxyArguments(proxySettings), + _ => GetSystemProxyArguments(systemProxy), + }; - var proxyArguments = (proxySettings?.Type) switch - { - ProxyType.None => "--no-proxy-server", - ProxyType.Custom => GetCustomProxyArguments(proxySettings), - _ => GetSystemProxyArguments(systemProxy), - }; + return proxyArguments; - return proxyArguments; + static string GetCustomProxyArguments(ProxySettings proxySettings) + { + var server = proxySettings.Address; + var bypassList = string.Join(';', ProxySettings.GetSafeBypassList(proxySettings.BypassList, includeLoopback: true)); + var arguments = "--proxy-server=\"{0}\" --proxy-bypass-list=\"{1}\"".FormatInvariant(server, bypassList); - static string GetCustomProxyArguments(ProxySettings proxySettings) - { - var server = proxySettings.Address; - var bypassList = string.Join(';', ProxySettings.GetSafeBypassList(proxySettings.BypassList, includeLoopback: true)); - var arguments = "--proxy-server=\"{0}\" --proxy-bypass-list=\"{1}\"".FormatInvariant(server, bypassList); + return arguments; + } - return arguments; - } + static string GetSystemProxyArguments(IWebProxy systemProxy) + { + var systemProxyType = systemProxy.GetType(); - static string GetSystemProxyArguments(IWebProxy systemProxy) + if (systemProxyType.FullName == "System.Net.Http.HttpEnvironmentProxy") { - var systemProxyType = systemProxy.GetType(); + string[]? bypass = null; + Uri? httpsProxyUri = null; + Uri? httpProxyUri = null; - if (systemProxyType.FullName == "System.Net.Http.HttpEnvironmentProxy") - { - string[]? bypass = null; - Uri? httpsProxyUri = null; - Uri? httpProxyUri = null; + var bypassObject = systemProxyType.GetField("_bypass", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(systemProxy); + if (bypassObject is IEnumerable items) + bypass = items.ToArray(); - var bypassObject = systemProxyType.GetField("_bypass", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(systemProxy); - if (bypassObject is IEnumerable items) - bypass = items.ToArray(); + var httpProxyUriObject = systemProxyType.GetField("_httpProxyUri", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(systemProxy); + if (httpProxyUriObject is Uri httpUri) + httpProxyUri = httpUri; - var httpProxyUriObject = systemProxyType.GetField("_httpProxyUri", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(systemProxy); - if (httpProxyUriObject is Uri httpUri) - httpProxyUri = httpUri; + var httpsProxyUriObject = systemProxyType.GetField("_httpsProxyUri", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(systemProxy); + if (httpsProxyUriObject is Uri httpsUri) + httpsProxyUri = httpsUri; - var httpsProxyUriObject = systemProxyType.GetField("_httpsProxyUri", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(systemProxy); - if (httpsProxyUriObject is Uri httpsUri) - httpsProxyUri = httpsUri; - - var arguments = new List(); + var arguments = new List(); + { + var server = "{0};{1}".FormatInvariant(httpProxyUri, httpsProxyUri).Trim(';'); + if (server.Length > 0) { - var server = "{0};{1}".FormatInvariant(httpProxyUri, httpsProxyUri).Trim(';'); - if (server.Length > 0) - { - arguments.Add("--proxy-server=\"{0}\"".FormatInvariant(server)); - } - - var bypassList = string.Join(';', ProxySettings.GetSafeBypassList(bypass, includeLoopback: true)); - if (bypassList.Length > 0) - { - arguments.Add("--proxy-bypass-list=\"{0}\"".FormatInvariant(bypassList)); - } + arguments.Add("--proxy-server=\"{0}\"".FormatInvariant(server)); } - var proxyArguments = string.Join(' ', arguments); - return proxyArguments; + var bypassList = string.Join(';', ProxySettings.GetSafeBypassList(bypass, includeLoopback: true)); + if (bypassList.Length > 0) + { + arguments.Add("--proxy-bypass-list=\"{0}\"".FormatInvariant(bypassList)); + } } - else if (systemProxyType.FullName == "System.Net.Http.HttpWindowsProxy") + + var proxyArguments = string.Join(' ', arguments); + return proxyArguments; + } + else if (systemProxyType.FullName == "System.Net.Http.HttpWindowsProxy") + { + string[]? bypass = null; + string? proxy = null; + string? autoConfigUrl = null; + + var bypassObject = systemProxyType.GetField("_bypass", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(systemProxy); + if (bypassObject is IEnumerable items) + bypass = items.ToArray(); + + var proxyHelperObject = systemProxyType.GetField("_proxyHelper", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(systemProxy); + var proxyHelperType = proxyHelperObject?.GetType(); + if (proxyHelperType?.FullName == "System.Net.Http.WinInetProxyHelper") { - string[]? bypass = null; - string? proxy = null; - string? autoConfigUrl = null; + var proxyObject = proxyHelperType.GetField("_proxy", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(proxyHelperObject); + if (proxyObject is string proxyValue) + proxy = proxyValue; - var bypassObject = systemProxyType.GetField("_bypass", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(systemProxy); - if (bypassObject is IEnumerable items) - bypass = items.ToArray(); + var autoConfigUrlObject = proxyHelperType.GetField("_autoConfigUrl", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(proxyHelperObject); + if (autoConfigUrlObject is string autoConfigUrlValue) + autoConfigUrl = autoConfigUrlValue; + } - var proxyHelperObject = systemProxyType.GetField("_proxyHelper", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(systemProxy); - var proxyHelperType = proxyHelperObject?.GetType(); - if (proxyHelperType?.FullName == "System.Net.Http.WinInetProxyHelper") + var arguments = new List(); + { + if (proxy?.Length > 0) { - var proxyObject = proxyHelperType.GetField("_proxy", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(proxyHelperObject); - if (proxyObject is string proxyValue) - proxy = proxyValue; - - var autoConfigUrlObject = proxyHelperType.GetField("_autoConfigUrl", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(proxyHelperObject); - if (autoConfigUrlObject is string autoConfigUrlValue) - autoConfigUrl = autoConfigUrlValue; + arguments.Add("--proxy-server=\"{0}\"".FormatInvariant(proxy)); } - var arguments = new List(); + var bypassList = string.Join(';', ProxySettings.GetSafeBypassList(bypass, includeLoopback: true)); + if (bypassList.Length > 0) { - if (proxy?.Length > 0) - { - arguments.Add("--proxy-server=\"{0}\"".FormatInvariant(proxy)); - } - - var bypassList = string.Join(';', ProxySettings.GetSafeBypassList(bypass, includeLoopback: true)); - if (bypassList.Length > 0) - { - arguments.Add("--proxy-bypass-list=\"{0}\"".FormatInvariant(bypassList)); - } - - if (autoConfigUrl?.Length > 0) - { - arguments.Add("--proxy-pac-url=\"{0}\"".FormatInvariant(autoConfigUrl)); - } + arguments.Add("--proxy-bypass-list=\"{0}\"".FormatInvariant(bypassList)); } - var proxyArguments = string.Join(' ', arguments); - return proxyArguments; - } - else if (systemProxyType.FullName == "System.Net.Http.HttpNoProxy") - { - return "--no-proxy-server"; - } - else - { - throw new BravoUnexpectedException($"Unexpected { nameof(IWebProxy) } type ({ systemProxyType.FullName })"); + if (autoConfigUrl?.Length > 0) + { + arguments.Add("--proxy-pac-url=\"{0}\"".FormatInvariant(autoConfigUrl)); + } } + + var proxyArguments = string.Join(' ', arguments); + return proxyArguments; + } + else if (systemProxyType.FullName == "System.Net.Http.HttpNoProxy") + { + return "--no-proxy-server"; + } + else + { + throw new BravoUnexpectedException($"Unexpected {nameof(IWebProxy)} type ({systemProxyType.FullName})"); } } } -} \ No newline at end of file +} diff --git a/src/Infrastructure/Helpers/WindowDialogHelper.cs b/src/Infrastructure/Helpers/WindowDialogHelper.cs index 4204e99c..e6faa847 100644 --- a/src/Infrastructure/Helpers/WindowDialogHelper.cs +++ b/src/Infrastructure/Helpers/WindowDialogHelper.cs @@ -1,121 +1,120 @@ -namespace Sqlbi.Bravo.Infrastructure.Helpers -{ - using Sqlbi.Bravo.Infrastructure.Windows; - using System; - using System.Diagnostics.CodeAnalysis; - using System.Threading; +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Sqlbi.Bravo.Infrastructure.Windows; - internal static class WindowDialogHelper - { - public static bool OpenFileDialog(string filter, [NotNullWhen(true)] out string? path, CancellationToken cancellationToken) - { - var dialogOwner = Win32WindowWrapper.CreateFrom(ProcessHelper.GetCurrentProcessMainWindowHandle()); - var dialogResult = System.Windows.Forms.DialogResult.None; +namespace Sqlbi.Bravo.Infrastructure.Helpers; - using var dialog = new System.Windows.Forms.OpenFileDialog() - { - InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments, Environment.SpecialFolderOption.DoNotVerify), - Filter = filter, - Title = "Open file", - ShowReadOnly = false, - CheckFileExists = true - }; +internal static class WindowDialogHelper +{ + public static bool OpenFileDialog(string filter, [NotNullWhen(true)] out string? path, CancellationToken cancellationToken) + { + var dialogOwner = Win32WindowWrapper.CreateFrom(ProcessHelper.GetCurrentProcessMainWindowHandle()); + var dialogResult = System.Windows.Forms.DialogResult.None; - if (!cancellationToken.IsCancellationRequested) - { - ProcessHelper.RunOnSTAThread(() => dialogResult = dialog.ShowDialog(dialogOwner)); + using var dialog = new System.Windows.Forms.OpenFileDialog() + { + InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments, Environment.SpecialFolderOption.DoNotVerify), + Filter = filter, + Title = "Open file", + ShowReadOnly = false, + CheckFileExists = true + }; - //var dialog2 = new Bravo.Infrastructure.Windows.SaveFileDialog - //{ - // InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments, Environment.SpecialFolderOption.DoNotVerify), - // Filter = "Vpax files (*.vpax)|*.vpax|All files (*.*)|*.*", - // Title = "Export file", - // DefaultExt = "vpax", - // //FileName = fileName - //}; - //var result = dialog2.ShowDialog(hWnd: Process.GetCurrentProcess().MainWindowHandle); - } + if (!cancellationToken.IsCancellationRequested) + { + ProcessHelper.RunOnSTAThread(() => dialogResult = dialog.ShowDialog(dialogOwner)); - if (dialogResult == System.Windows.Forms.DialogResult.OK) - { - path = dialog.FileName; - return true; - } - else - { - path = null; - return false; - } + //var dialog2 = new Bravo.Infrastructure.Windows.SaveFileDialog + //{ + // InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments, Environment.SpecialFolderOption.DoNotVerify), + // Filter = "Vpax files (*.vpax)|*.vpax|All files (*.*)|*.*", + // Title = "Export file", + // DefaultExt = "vpax", + // //FileName = fileName + //}; + //var result = dialog2.ShowDialog(hWnd: Process.GetCurrentProcess().MainWindowHandle); } - public static bool SaveFileDialog(string? fileName, string? filter, string defaultExt, [NotNullWhen(true)] out string? path, CancellationToken cancellationToken) + if (dialogResult == System.Windows.Forms.DialogResult.OK) { - var dialogOwner = Win32WindowWrapper.CreateFrom(ProcessHelper.GetCurrentProcessMainWindowHandle()); - var dialogResult = System.Windows.Forms.DialogResult.None; - var defaultExtLowercase = defaultExt.ToLower(); + path = dialog.FileName; + return true; + } + else + { + path = null; + return false; + } + } - using var dialog = new System.Windows.Forms.SaveFileDialog() - { - InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments, Environment.SpecialFolderOption.DoNotVerify), - Filter = filter ?? $"{ defaultExt } files (*.{ defaultExtLowercase })|*.{ defaultExtLowercase }|All files (*.*)|*.*", - Title = "Save file", - DefaultExt = defaultExtLowercase, - FileName = fileName - }; + public static bool SaveFileDialog(string? fileName, string? filter, string defaultExt, [NotNullWhen(true)] out string? path, CancellationToken cancellationToken) + { + var dialogOwner = Win32WindowWrapper.CreateFrom(ProcessHelper.GetCurrentProcessMainWindowHandle()); + var dialogResult = System.Windows.Forms.DialogResult.None; + var defaultExtLowercase = defaultExt.ToLower(); - if (!cancellationToken.IsCancellationRequested) - { - ProcessHelper.RunOnSTAThread(() => dialogResult = dialog.ShowDialog(dialogOwner)); + using var dialog = new System.Windows.Forms.SaveFileDialog() + { + InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments, Environment.SpecialFolderOption.DoNotVerify), + Filter = filter ?? $"{defaultExt} files (*.{defaultExtLowercase})|*.{defaultExtLowercase}|All files (*.*)|*.*", + Title = "Save file", + DefaultExt = defaultExtLowercase, + FileName = fileName + }; - //var dialog2 = new Bravo.Infrastructure.Windows.SaveFileDialog - //{ - // InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments, Environment.SpecialFolderOption.DoNotVerify), - // Filter = "Vpax files (*.vpax)|*.vpax|All files (*.*)|*.*", - // Title = "Export file", - // DefaultExt = "vpax", - // //FileName = fileName - //}; - //var result = dialog2.ShowDialog(hWnd: Process.GetCurrentProcess().MainWindowHandle); - } + if (!cancellationToken.IsCancellationRequested) + { + ProcessHelper.RunOnSTAThread(() => dialogResult = dialog.ShowDialog(dialogOwner)); - if (dialogResult == System.Windows.Forms.DialogResult.OK) - { - path = dialog.FileName!; - return true; - } - else - { - path = null; - return false; - } + //var dialog2 = new Bravo.Infrastructure.Windows.SaveFileDialog + //{ + // InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments, Environment.SpecialFolderOption.DoNotVerify), + // Filter = "Vpax files (*.vpax)|*.vpax|All files (*.*)|*.*", + // Title = "Export file", + // DefaultExt = "vpax", + // //FileName = fileName + //}; + //var result = dialog2.ShowDialog(hWnd: Process.GetCurrentProcess().MainWindowHandle); } - public static bool BrowseFolderDialog([NotNullWhen(true)] out string? path, CancellationToken cancellationToken) + if (dialogResult == System.Windows.Forms.DialogResult.OK) { - var dialogOwner = Win32WindowWrapper.CreateFrom(ProcessHelper.GetCurrentProcessMainWindowHandle()); - var dialogResult = System.Windows.Forms.DialogResult.None; + path = dialog.FileName!; + return true; + } + else + { + path = null; + return false; + } + } - using var dialog = new System.Windows.Forms.FolderBrowserDialog() - { - RootFolder = Environment.SpecialFolder.MyDocuments, - ShowNewFolderButton = true, - }; - - if (!cancellationToken.IsCancellationRequested) - { - ProcessHelper.RunOnSTAThread(() => dialogResult = dialog.ShowDialog(dialogOwner)); - } + public static bool BrowseFolderDialog([NotNullWhen(true)] out string? path, CancellationToken cancellationToken) + { + var dialogOwner = Win32WindowWrapper.CreateFrom(ProcessHelper.GetCurrentProcessMainWindowHandle()); + var dialogResult = System.Windows.Forms.DialogResult.None; + + using var dialog = new System.Windows.Forms.FolderBrowserDialog() + { + RootFolder = Environment.SpecialFolder.MyDocuments, + ShowNewFolderButton = true, + }; - if (dialogResult == System.Windows.Forms.DialogResult.OK) - { - path = dialog.SelectedPath; - return true; - } - else - { - path = null; - return false; - } + if (!cancellationToken.IsCancellationRequested) + { + ProcessHelper.RunOnSTAThread(() => dialogResult = dialog.ShowDialog(dialogOwner)); + } + + if (dialogResult == System.Windows.Forms.DialogResult.OK) + { + path = dialog.SelectedPath; + return true; + } + else + { + path = null; + return false; } } } diff --git a/src/Infrastructure/Messages/AppInstanceStartupMessage.cs b/src/Infrastructure/Messages/AppInstanceStartupMessage.cs index 0b4f95b1..efcf5776 100644 --- a/src/Infrastructure/Messages/AppInstanceStartupMessage.cs +++ b/src/Infrastructure/Messages/AppInstanceStartupMessage.cs @@ -1,125 +1,124 @@ -namespace Sqlbi.Bravo.Infrastructure.Messages +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; +using Sqlbi.Bravo.Infrastructure.Configuration.Settings; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Models; +using SSAS = Microsoft.AnalysisServices; + +namespace Sqlbi.Bravo.Infrastructure.Messages; + +internal class AppInstanceStartupMessage { - using Sqlbi.Bravo.Infrastructure.Configuration.Settings; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Models; - using System.IO; - using System.Text.Json; - using System.Text.Json.Serialization; - using SSAS = Microsoft.AnalysisServices; - - internal class AppInstanceStartupMessage - { - [JsonPropertyName("isEmpty")] - public bool? IsEmpty { get; set; } + [JsonPropertyName("isEmpty")] + public bool? IsEmpty { get; set; } - [JsonPropertyName("parentProcessId")] - public int? ParentProcessId { get; set; } + [JsonPropertyName("parentProcessId")] + public int? ParentProcessId { get; set; } - [JsonPropertyName("parentProcessName")] - public string? ParentProcessName { get; set; } + [JsonPropertyName("parentProcessName")] + public string? ParentProcessName { get; set; } - [JsonPropertyName("parentProcessMainWindowTitle")] - public string? ParentProcessMainWindowTitle { get; set; } + [JsonPropertyName("parentProcessMainWindowTitle")] + public string? ParentProcessMainWindowTitle { get; set; } - [JsonPropertyName("serverName")] - public string? ArgumentServerName { get; set; } + [JsonPropertyName("serverName")] + public string? ArgumentServerName { get; set; } - [JsonPropertyName("databaseName")] - public string? ArgumentDatabaseName { get; set; } + [JsonPropertyName("databaseName")] + public string? ArgumentDatabaseName { get; set; } - [JsonPropertyName("commandLineErrors")] - public string[]? CommandLineErrors { get; set; } + [JsonPropertyName("commandLineErrors")] + public string[]? CommandLineErrors { get; set; } - [JsonIgnore] - public bool IsExternalTool => AppEnvironment.PBIDesktopProcessName.EqualsI(ParentProcessName); + [JsonIgnore] + public bool IsExternalTool => AppEnvironment.PBIDesktopProcessName.EqualsI(ParentProcessName); - public static AppInstanceStartupMessage CreateFrom(StartupSettings settings) + public static AppInstanceStartupMessage CreateFrom(StartupSettings settings) + { + var message = new AppInstanceStartupMessage { - var message = new AppInstanceStartupMessage - { - IsEmpty = settings.IsEmpty, - ParentProcessId = settings.ParentProcessId, - ParentProcessName = settings.ParentProcessName, - ParentProcessMainWindowTitle = settings.ParentProcessMainWindowTitle, - ArgumentServerName = settings.ArgumentServerName, - ArgumentDatabaseName = settings.ArgumentDatabaseName, - CommandLineErrors = settings.CommandLineErrors, - }; + IsEmpty = settings.IsEmpty, + ParentProcessId = settings.ParentProcessId, + ParentProcessName = settings.ParentProcessName, + ParentProcessMainWindowTitle = settings.ParentProcessMainWindowTitle, + ArgumentServerName = settings.ArgumentServerName, + ArgumentDatabaseName = settings.ArgumentDatabaseName, + CommandLineErrors = settings.CommandLineErrors, + }; + + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(AppInstanceStartupMessage)}.{nameof(CreateFrom)}", content: JsonSerializer.Serialize(message)); + + return message; + } +} - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{ nameof(AppInstanceStartupMessage) }.{ nameof(CreateFrom) }", content: JsonSerializer.Serialize(message)); +internal static class AppInstanceStartupMessageExtensions +{ + public static JsonElement ToJsonElement(this AppInstanceStartupMessage startupMessage) + { + var messageString = JsonSerializer.Serialize(startupMessage, AppEnvironment.DefaultJsonOptions); + var messageJson = JsonSerializer.Deserialize(messageString); - return message; - } + return messageJson; } - internal static class AppInstanceStartupMessageExtensions + public static string? ToWebMessageString(this AppInstanceStartupMessage startupMessage) { - public static JsonElement ToJsonElement(this AppInstanceStartupMessage startupMessage) + string? webMessageString = null; + + if (startupMessage.ArgumentServerName is null) { - var messageString = JsonSerializer.Serialize(startupMessage, AppEnvironment.DefaultJsonOptions); - var messageJson = JsonSerializer.Deserialize(messageString); + var jsonMessage = startupMessage.ToJsonElement(); + var webMessage = UnknownWebMessage.CreateFrom(jsonMessage); - return messageJson; + webMessageString = webMessage.AsString; } - - public static string? ToWebMessageString(this AppInstanceStartupMessage startupMessage) + else if (NetworkHelper.IsPBICloudDatasetServer(startupMessage.ArgumentServerName) || NetworkHelper.IsASAzureServer(startupMessage.ArgumentServerName)) { - string? webMessageString = null; - - if (startupMessage.ArgumentServerName is null) - { - var jsonMessage = startupMessage.ToJsonElement(); - var webMessage = UnknownWebMessage.CreateFrom(jsonMessage); - - webMessageString = webMessage.AsString; - } - else if (NetworkHelper.IsPBICloudDatasetServer(startupMessage.ArgumentServerName) || NetworkHelper.IsASAzureServer(startupMessage.ArgumentServerName)) + var webMessage = new PBICloudDatasetOpenWebMessage { - var webMessage = new PBICloudDatasetOpenWebMessage + Dataset = new PBICloudDataset { - Dataset = new PBICloudDataset - { - ServerName = CommonHelper.NormalizeUriString(startupMessage.ArgumentServerName), - DatabaseName = startupMessage.ArgumentDatabaseName, - ConnectionMode = PBICloudDatasetConnectionMode.Unknown - }, - }; - - webMessageString = webMessage.AsString; - } - else - { - // SQL Server Analysis Services instance listens on one TCP port for all IP addresses (included loopback) assigned or aliased to the computer - - var report = new PBIDesktopReport - { - ProcessId = startupMessage.ParentProcessId, - ReportName = startupMessage.ParentProcessMainWindowTitle, - ServerName = startupMessage.ArgumentServerName, + ServerName = CommonHelper.NormalizeUriString(startupMessage.ArgumentServerName), DatabaseName = startupMessage.ArgumentDatabaseName, - CompatibilityMode = SSAS.CompatibilityMode.Unknown, - ConnectionMode = PBIDesktopReportConnectionMode.Supported - }; + ConnectionMode = PBICloudDatasetConnectionMode.Unknown + }, + }; - if (!startupMessage.IsExternalTool) - { - if (report.ReportName?.ContainsInvalidPathChars() == false) - report.ReportName = Path.GetFileNameWithoutExtension(report.ReportName); + webMessageString = webMessage.AsString; + } + else + { + // SQL Server Analysis Services instance listens on one TCP port for all IP addresses (included loopback) assigned or aliased to the computer - report.ReportName += $" ({ startupMessage.ParentProcessId })"; - } + var report = new PBIDesktopReport + { + ProcessId = startupMessage.ParentProcessId, + ReportName = startupMessage.ParentProcessMainWindowTitle, + ServerName = startupMessage.ArgumentServerName, + DatabaseName = startupMessage.ArgumentDatabaseName, + CompatibilityMode = SSAS.CompatibilityMode.Unknown, + ConnectionMode = PBIDesktopReportConnectionMode.Supported + }; - var webMessage = PBIDesktopReportOpenWebMessage.CreateFrom(report); - webMessageString = webMessage.AsString; - } + if (!startupMessage.IsExternalTool) + { + if (report.ReportName?.ContainsInvalidPathChars() == false) + report.ReportName = Path.GetFileNameWithoutExtension(report.ReportName); - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{ nameof(AppInstanceStartupMessage) }.{ nameof(ToWebMessageString) }", content: webMessageString); + report.ReportName += $" ({startupMessage.ParentProcessId})"; + } - return webMessageString; + var webMessage = PBIDesktopReportOpenWebMessage.CreateFrom(report); + webMessageString = webMessage.AsString; } + + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(AppInstanceStartupMessage)}.{nameof(ToWebMessageString)}", content: webMessageString); + + return webMessageString; } } diff --git a/src/Infrastructure/Messages/WebMessageType.cs b/src/Infrastructure/Messages/WebMessageType.cs index ea7e33e4..d32fc719 100644 --- a/src/Infrastructure/Messages/WebMessageType.cs +++ b/src/Infrastructure/Messages/WebMessageType.cs @@ -1,10 +1,9 @@ -namespace Sqlbi.Bravo.Infrastructure.Messages +namespace Sqlbi.Bravo.Infrastructure.Messages; + +internal enum WebMessageType { - internal enum WebMessageType - { - Unknown = 0, - ReportOpen = 1, - DatasetOpen = 2, - VpaxOpen = 3, - } + Unknown = 0, + ReportOpen = 1, + DatasetOpen = 2, + VpaxOpen = 3, } diff --git a/src/Infrastructure/Messages/WebMessages.cs b/src/Infrastructure/Messages/WebMessages.cs index fa80e9dc..8a210f59 100644 --- a/src/Infrastructure/Messages/WebMessages.cs +++ b/src/Infrastructure/Messages/WebMessages.cs @@ -1,122 +1,120 @@ -namespace Sqlbi.Bravo.Infrastructure.Messages -{ - using Sqlbi.Bravo.Infrastructure.Configuration.Settings; - using Sqlbi.Bravo.Models; - using System; - using System.ComponentModel.DataAnnotations; - using System.Text.Json; - using System.Text.Json.Serialization; - - internal interface IWebMessage - { - /// - /// Message type identifier - /// - WebMessageType MessageType { get; } - } +using System; +using System.ComponentModel.DataAnnotations; +using System.Text.Json; +using System.Text.Json.Serialization; +using Sqlbi.Bravo.Models; - internal class UnknownWebMessage : IWebMessage - { - [Required] - [JsonPropertyName("type")] - public WebMessageType MessageType => WebMessageType.Unknown; +namespace Sqlbi.Bravo.Infrastructure.Messages; - [JsonPropertyName("message")] - public JsonElement? Message { get; set; } +internal interface IWebMessage +{ + /// + /// Message type identifier + /// + WebMessageType MessageType { get; } +} - [JsonPropertyName("exception")] - public JsonElement? Exception { get; set; } +internal class UnknownWebMessage : IWebMessage +{ + [Required] + [JsonPropertyName("type")] + public WebMessageType MessageType => WebMessageType.Unknown; - [JsonIgnore] - public string AsString => JsonSerializer.Serialize(this, AppEnvironment.DefaultJsonOptions); + [JsonPropertyName("message")] + public JsonElement? Message { get; set; } - public static UnknownWebMessage CreateFrom(JsonElement message) - { - var webMessage = new UnknownWebMessage - { - Message = message, - Exception = null, - }; + [JsonPropertyName("exception")] + public JsonElement? Exception { get; set; } - return webMessage; - } + [JsonIgnore] + public string AsString => JsonSerializer.Serialize(this, AppEnvironment.DefaultJsonOptions); - public static UnknownWebMessage CreateFrom(Exception exception) + public static UnknownWebMessage CreateFrom(JsonElement message) + { + var webMessage = new UnknownWebMessage { - if (exception is AggregateException aggregateException) - exception = aggregateException.GetBaseException(); - - var exceptionObject = new - { - Message = exception.Message, - Details = exception.ToString(), - }; - - var exceptionObjectString = JsonSerializer.Serialize(exceptionObject, AppEnvironment.DefaultJsonOptions); - var exceptionObjectJson = JsonSerializer.Deserialize(exceptionObjectString); - - var webMessage = new UnknownWebMessage - { - Message = null, - Exception = exceptionObjectJson, - }; - - return webMessage; - } + Message = message, + Exception = null, + }; + + return webMessage; } - internal class PBIDesktopReportOpenWebMessage : IWebMessage + public static UnknownWebMessage CreateFrom(Exception exception) { - [Required] - [JsonPropertyName("type")] - public WebMessageType MessageType => WebMessageType.ReportOpen; + if (exception is AggregateException aggregateException) + exception = aggregateException.GetBaseException(); - [JsonPropertyName("report")] - public PBIDesktopReport? Report { get; set; } + var exceptionObject = new + { + Message = exception.Message, + Details = exception.ToString(), + }; - [JsonIgnore] - public string AsString => JsonSerializer.Serialize(this, AppEnvironment.DefaultJsonOptions); + var exceptionObjectString = JsonSerializer.Serialize(exceptionObject, AppEnvironment.DefaultJsonOptions); + var exceptionObjectJson = JsonSerializer.Deserialize(exceptionObjectString); - public static PBIDesktopReportOpenWebMessage CreateFrom(PBIDesktopReport report) + var webMessage = new UnknownWebMessage { - var webMessage = new PBIDesktopReportOpenWebMessage - { - Report = report, - }; + Message = null, + Exception = exceptionObjectJson, + }; - return webMessage; - } + return webMessage; } +} - internal class PBICloudDatasetOpenWebMessage : IWebMessage - { - [Required] - [JsonPropertyName("type")] - public WebMessageType MessageType => WebMessageType.DatasetOpen; +internal class PBIDesktopReportOpenWebMessage : IWebMessage +{ + [Required] + [JsonPropertyName("type")] + public WebMessageType MessageType => WebMessageType.ReportOpen; - [JsonPropertyName("dataset")] - public PBICloudDataset? Dataset { get; set; } + [JsonPropertyName("report")] + public PBIDesktopReport? Report { get; set; } - [JsonIgnore] - public string AsString => JsonSerializer.Serialize(this, AppEnvironment.DefaultJsonOptions); - } + [JsonIgnore] + public string AsString => JsonSerializer.Serialize(this, AppEnvironment.DefaultJsonOptions); - internal class VpaxFileOpenWebMessage : IWebMessage + public static PBIDesktopReportOpenWebMessage CreateFrom(PBIDesktopReport report) { - [Required] - [JsonPropertyName("type")] - public WebMessageType MessageType => WebMessageType.VpaxOpen; + var webMessage = new PBIDesktopReportOpenWebMessage + { + Report = report, + }; - [JsonPropertyName("name")] - public string? Name { get; set; } + return webMessage; + } +} - [JsonPropertyName("blob")] - public byte[]? Content { get; set; } +internal class PBICloudDatasetOpenWebMessage : IWebMessage +{ + [Required] + [JsonPropertyName("type")] + public WebMessageType MessageType => WebMessageType.DatasetOpen; - [JsonPropertyName("lastModified")] - public long? LastModified { get; set; } + [JsonPropertyName("dataset")] + public PBICloudDataset? Dataset { get; set; } - [JsonIgnore] - public string AsString => JsonSerializer.Serialize(this, AppEnvironment.DefaultJsonOptions); - } + [JsonIgnore] + public string AsString => JsonSerializer.Serialize(this, AppEnvironment.DefaultJsonOptions); +} + +internal class VpaxFileOpenWebMessage : IWebMessage +{ + [Required] + [JsonPropertyName("type")] + public WebMessageType MessageType => WebMessageType.VpaxOpen; + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("blob")] + public byte[]? Content { get; set; } + + [JsonPropertyName("lastModified")] + public long? LastModified { get; set; } + + [JsonIgnore] + public string AsString => JsonSerializer.Serialize(this, AppEnvironment.DefaultJsonOptions); } diff --git a/src/Infrastructure/Models/IDataModel.cs b/src/Infrastructure/Models/IDataModel.cs index 0ed5673c..a1fab801 100644 --- a/src/Infrastructure/Models/IDataModel.cs +++ b/src/Infrastructure/Models/IDataModel.cs @@ -1,11 +1,10 @@ -namespace Sqlbi.Bravo.Infrastructure.Models -{ - using System; +using System; + +namespace Sqlbi.Bravo.Infrastructure.Models; - internal interface IDataModel : IEquatable - { - public string? ServerName { get; set; } +internal interface IDataModel : IEquatable +{ + public string? ServerName { get; set; } - public string? DatabaseName { get; set; } - } -} \ No newline at end of file + public string? DatabaseName { get; set; } +} diff --git a/src/Infrastructure/Policies/IPolicySource.cs b/src/Infrastructure/Policies/IPolicySource.cs index 7464d793..c3241754 100644 --- a/src/Infrastructure/Policies/IPolicySource.cs +++ b/src/Infrastructure/Policies/IPolicySource.cs @@ -1,48 +1,48 @@ -namespace Sqlbi.Bravo.Infrastructure.Policies +using System; +using Microsoft.Win32; + +namespace Sqlbi.Bravo.Infrastructure.Policies; + +/// +/// Abstraction over a single raw policy value store (e.g. a registry key), so that +/// ' parsing/precedence logic does not depend on +/// directly and can be unit tested against a fake, without touching the real registry. +/// +internal interface IPolicySource { - using Microsoft.Win32; + int? GetInt(string name); + string? GetString(string name); +} - /// - /// Abstraction over a single raw policy value store (e.g. a registry key), so that - /// ' parsing/precedence logic does not depend on - /// directly and can be unit tested against a fake, without touching the real registry. - /// - internal interface IPolicySource - { - int? GetInt(string name); - string? GetString(string name); - } +/// +/// Typed reading conventions shared by every : a policy is a +/// DWORD (0/1 -> bool, or a defined enum member) or a string. Kept as extensions rather than +/// interface members so itself stays minimal (raw int/string only). +/// +internal static class PolicySourceExtensions +{ + private const int PolicyDisabledValue = 0; + private const int PolicyEnabledValue = 1; - /// - /// Typed reading conventions shared by every : a policy is a - /// DWORD (0/1 -> bool, or a defined enum member) or a string. Kept as extensions rather than - /// interface members so itself stays minimal (raw int/string only). - /// - internal static class PolicySourceExtensions + extension(IPolicySource source) { - private const int PolicyDisabledValue = 0; - private const int PolicyEnabledValue = 1; - - extension(IPolicySource source) + public bool? GetBool(string name) { - public bool? GetBool(string name) + return source.GetInt(name) switch { - return source.GetInt(name) switch - { - null => null, // Policy not set - PolicyDisabledValue => false, - PolicyEnabledValue => true, - _ => null, // Invalid policy value - }; - } + null => null, // Policy not set + PolicyDisabledValue => false, + PolicyEnabledValue => true, + _ => null, // Invalid policy value + }; + } - public T? GetEnum(string name) where T : struct, Enum - { - if (source.GetInt(name) is { } value && Enum.IsDefined(typeof(T), value)) - return (T)(object)value; + public T? GetEnum(string name) where T : struct, Enum + { + if (source.GetInt(name) is { } value && Enum.IsDefined(typeof(T), value)) + return (T)(object)value; - return null; // Policy not set or invalid - } + return null; // Policy not set or invalid } } } diff --git a/src/Infrastructure/Policies/Policies.cs b/src/Infrastructure/Policies/Policies.cs index 9201b321..c0dc94aa 100644 --- a/src/Infrastructure/Policies/Policies.cs +++ b/src/Infrastructure/Policies/Policies.cs @@ -1,29 +1,28 @@ -namespace Sqlbi.Bravo.Infrastructure.Policies -{ - using Sqlbi.Bravo.Infrastructure.Configuration.Settings; +using Sqlbi.Bravo.Infrastructure.Configuration.Settings; - internal interface IPolicies - { - bool? TelemetryEnabled { get; } - UpdateChannelType? UpdateChannel { get; } - bool? UpdateCheckEnabled { get; } - bool? UseSystemBrowserForAuthentication { get; } - bool? BuiltInTemplatesEnabled { get; } - bool? CustomTemplatesEnabled { get; } - string? CustomTemplatesOrganizationRepositoryPath { get; } - } +namespace Sqlbi.Bravo.Infrastructure.Policies; - /// - /// Immutable snapshot of Bravo's effective policy values. Pure data - see - /// for how instances are read from the registry, parsed, - /// and merged with LocalMachine/CurrentUser precedence. - /// - internal sealed record Policies( - bool? TelemetryEnabled, - UpdateChannelType? UpdateChannel, - bool? UpdateCheckEnabled, - bool? UseSystemBrowserForAuthentication, - bool? BuiltInTemplatesEnabled, - bool? CustomTemplatesEnabled, - string? CustomTemplatesOrganizationRepositoryPath) : IPolicies; +internal interface IPolicies +{ + bool? TelemetryEnabled { get; } + UpdateChannelType? UpdateChannel { get; } + bool? UpdateCheckEnabled { get; } + bool? UseSystemBrowserForAuthentication { get; } + bool? BuiltInTemplatesEnabled { get; } + bool? CustomTemplatesEnabled { get; } + string? CustomTemplatesOrganizationRepositoryPath { get; } } + +/// +/// Immutable snapshot of Bravo's effective policy values. Pure data - see +/// for how instances are read from the registry, parsed, +/// and merged with LocalMachine/CurrentUser precedence. +/// +internal sealed record Policies( + bool? TelemetryEnabled, + UpdateChannelType? UpdateChannel, + bool? UpdateCheckEnabled, + bool? UseSystemBrowserForAuthentication, + bool? BuiltInTemplatesEnabled, + bool? CustomTemplatesEnabled, + string? CustomTemplatesOrganizationRepositoryPath) : IPolicies; diff --git a/src/Infrastructure/Policies/PoliciesFactory.cs b/src/Infrastructure/Policies/PoliciesFactory.cs index 0bf6344d..0daffa76 100644 --- a/src/Infrastructure/Policies/PoliciesFactory.cs +++ b/src/Infrastructure/Policies/PoliciesFactory.cs @@ -1,47 +1,46 @@ -namespace Sqlbi.Bravo.Infrastructure.Policies +using Microsoft.Win32; +using Sqlbi.Bravo.Infrastructure.Configuration.Settings; + +namespace Sqlbi.Bravo.Infrastructure.Policies; + +/// +/// Builds instances: parses a single , +/// and composes the effective policy set from LocalMachine + CurrentUser with precedence. +/// +internal static class PoliciesFactory { - using Microsoft.Win32; - using Sqlbi.Bravo.Infrastructure.Configuration.Settings; + private const string OptionSettingsSubKeyName = @"SOFTWARE\Policies\SQLBI\Bravo\OptionSettings"; - /// - /// Builds instances: parses a single , - /// and composes the effective policy set from LocalMachine + CurrentUser with precedence. - /// - internal static class PoliciesFactory + public static Policies Create() { - private const string OptionSettingsSubKeyName = @"SOFTWARE\Policies\SQLBI\Bravo\OptionSettings"; - - public static Policies Create() - { - using var machineKey = Registry.LocalMachine.OpenSubKey(OptionSettingsSubKeyName); - var machinePolicies = FromSource(new RegistryPolicySource(machineKey)); + using var machineKey = Registry.LocalMachine.OpenSubKey(OptionSettingsSubKeyName); + var machinePolicies = FromSource(new RegistryPolicySource(machineKey)); - using var userKey = Registry.CurrentUser.OpenSubKey(OptionSettingsSubKeyName); - var userPolicies = FromSource(new RegistryPolicySource(userKey)); + using var userKey = Registry.CurrentUser.OpenSubKey(OptionSettingsSubKeyName); + var userPolicies = FromSource(new RegistryPolicySource(userKey)); - return Merge(machinePolicies, userPolicies); - } + return Merge(machinePolicies, userPolicies); + } - internal static Policies FromSource(IPolicySource source) => new( - TelemetryEnabled: source.GetBool("TelemetryEnabled"), - UpdateChannel: source.GetEnum("UpdateChannel"), - UpdateCheckEnabled: source.GetBool("UpdateCheckEnabled"), - UseSystemBrowserForAuthentication: source.GetBool("UseSystemBrowserForAuthentication"), - BuiltInTemplatesEnabled: source.GetBool("BuiltInTemplatesEnabled"), - CustomTemplatesEnabled: source.GetBool("CustomTemplatesEnabled"), - CustomTemplatesOrganizationRepositoryPath: source.GetString("CustomTemplatesOrganizationRepositoryPath")); + internal static Policies FromSource(IPolicySource source) => new( + TelemetryEnabled: source.GetBool("TelemetryEnabled"), + UpdateChannel: source.GetEnum("UpdateChannel"), + UpdateCheckEnabled: source.GetBool("UpdateCheckEnabled"), + UseSystemBrowserForAuthentication: source.GetBool("UseSystemBrowserForAuthentication"), + BuiltInTemplatesEnabled: source.GetBool("BuiltInTemplatesEnabled"), + CustomTemplatesEnabled: source.GetBool("CustomTemplatesEnabled"), + CustomTemplatesOrganizationRepositoryPath: source.GetString("CustomTemplatesOrganizationRepositoryPath")); - internal static Policies Merge(Policies machinePolicies, Policies userPolicies) - { - // LocalMachine takes precedence over CurrentUser when both are configured - return new Policies( - TelemetryEnabled: machinePolicies.TelemetryEnabled ?? userPolicies.TelemetryEnabled, - UpdateChannel: machinePolicies.UpdateChannel ?? userPolicies.UpdateChannel, - UpdateCheckEnabled: machinePolicies.UpdateCheckEnabled ?? userPolicies.UpdateCheckEnabled, - UseSystemBrowserForAuthentication: machinePolicies.UseSystemBrowserForAuthentication ?? userPolicies.UseSystemBrowserForAuthentication, - BuiltInTemplatesEnabled: machinePolicies.BuiltInTemplatesEnabled ?? userPolicies.BuiltInTemplatesEnabled, - CustomTemplatesEnabled: machinePolicies.CustomTemplatesEnabled ?? userPolicies.CustomTemplatesEnabled, - CustomTemplatesOrganizationRepositoryPath: machinePolicies.CustomTemplatesOrganizationRepositoryPath ?? userPolicies.CustomTemplatesOrganizationRepositoryPath); - } + internal static Policies Merge(Policies machinePolicies, Policies userPolicies) + { + // LocalMachine takes precedence over CurrentUser when both are configured + return new Policies( + TelemetryEnabled: machinePolicies.TelemetryEnabled ?? userPolicies.TelemetryEnabled, + UpdateChannel: machinePolicies.UpdateChannel ?? userPolicies.UpdateChannel, + UpdateCheckEnabled: machinePolicies.UpdateCheckEnabled ?? userPolicies.UpdateCheckEnabled, + UseSystemBrowserForAuthentication: machinePolicies.UseSystemBrowserForAuthentication ?? userPolicies.UseSystemBrowserForAuthentication, + BuiltInTemplatesEnabled: machinePolicies.BuiltInTemplatesEnabled ?? userPolicies.BuiltInTemplatesEnabled, + CustomTemplatesEnabled: machinePolicies.CustomTemplatesEnabled ?? userPolicies.CustomTemplatesEnabled, + CustomTemplatesOrganizationRepositoryPath: machinePolicies.CustomTemplatesOrganizationRepositoryPath ?? userPolicies.CustomTemplatesOrganizationRepositoryPath); } } diff --git a/src/Infrastructure/Policies/RegistryPolicySource.cs b/src/Infrastructure/Policies/RegistryPolicySource.cs index 8770f502..bf598cd0 100644 --- a/src/Infrastructure/Policies/RegistryPolicySource.cs +++ b/src/Infrastructure/Policies/RegistryPolicySource.cs @@ -1,19 +1,18 @@ -namespace Sqlbi.Bravo.Infrastructure.Policies -{ - using Microsoft.Win32; +using Microsoft.Win32; + +namespace Sqlbi.Bravo.Infrastructure.Policies; - /// - /// Adapter that bridges to a real . - /// Intentionally a thin pass-through with no logic of its own. - /// - internal sealed class RegistryPolicySource(RegistryKey? key) : IPolicySource - { - private readonly RegistryKey? _key = key; +/// +/// Adapter that bridges to a real . +/// Intentionally a thin pass-through with no logic of its own. +/// +internal sealed class RegistryPolicySource(RegistryKey? key) : IPolicySource +{ + private readonly RegistryKey? _key = key; - public int? GetInt(string name) - => _key?.GetValue(name) is int value ? value : null; + public int? GetInt(string name) + => _key?.GetValue(name) is int value ? value : null; - public string? GetString(string name) - => _key?.GetValue(name) as string; - } + public string? GetString(string name) + => _key?.GetValue(name) as string; } diff --git a/src/Infrastructure/Policies/ServiceCollectionExtensions.cs b/src/Infrastructure/Policies/ServiceCollectionExtensions.cs index 469ac404..58436656 100644 --- a/src/Infrastructure/Policies/ServiceCollectionExtensions.cs +++ b/src/Infrastructure/Policies/ServiceCollectionExtensions.cs @@ -1,14 +1,13 @@ -namespace Sqlbi.Bravo.Infrastructure.Policies -{ - using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +namespace Sqlbi.Bravo.Infrastructure.Policies; - internal static class ServiceCollectionExtensions +internal static class ServiceCollectionExtensions +{ + public static IServiceCollection AddGroupPolicies(this IServiceCollection services) { - public static IServiceCollection AddGroupPolicies(this IServiceCollection services) - { - services.AddSingleton(_ => PoliciesFactory.Create()); + services.AddSingleton(_ => PoliciesFactory.Create()); - return services; - } + return services; } } diff --git a/src/Infrastructure/PowerBI/Cloud/Authentication/AuthenticatedSession.cs b/src/Infrastructure/PowerBI/Cloud/Authentication/AuthenticatedSession.cs index ab9a195a..6e901215 100644 --- a/src/Infrastructure/PowerBI/Cloud/Authentication/AuthenticatedSession.cs +++ b/src/Infrastructure/PowerBI/Cloud/Authentication/AuthenticatedSession.cs @@ -1,15 +1,12 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Authentication -{ - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Authentication; - /// - /// Represents an authenticated session with the Power BI cloud service, - /// containing the authentication result and the associated cloud environment. - /// - public sealed class AuthenticatedSession(AuthenticationResult authenticationResult, CloudEnvironment environment) - { - public AuthenticationResult AuthenticationResult { get; } = authenticationResult; +/// +/// Represents an authenticated session with the Power BI cloud service, +/// containing the authentication result and the associated cloud environment. +/// +public sealed class AuthenticatedSession(AuthenticationResult authenticationResult, CloudEnvironment environment) +{ + public AuthenticationResult AuthenticationResult { get; } = authenticationResult; - public CloudEnvironment Environment { get; } = environment; - } + public CloudEnvironment Environment { get; } = environment; } diff --git a/src/Infrastructure/PowerBI/Cloud/Authentication/AuthenticationResult.cs b/src/Infrastructure/PowerBI/Cloud/Authentication/AuthenticationResult.cs index 7618263e..19f5f661 100644 --- a/src/Infrastructure/PowerBI/Cloud/Authentication/AuthenticationResult.cs +++ b/src/Infrastructure/PowerBI/Cloud/Authentication/AuthenticationResult.cs @@ -1,43 +1,44 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Authentication +using System; +using System.Diagnostics; +using Msal = Microsoft.Identity.Client; + +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Authentication; + +/// +/// Bravo-owned snapshot of an MSAL authentication result, decoupled from . +/// +[DebuggerDisplay("{Email} ({Name})")] +public sealed class AuthenticationResult { - using Msal = Microsoft.Identity.Client; + private static readonly TimeSpan ExpirationBuffer = TimeSpan.FromSeconds(30); - /// - /// Bravo-owned snapshot of an MSAL authentication result, decoupled from . - /// - [DebuggerDisplay("{Email} ({Name})")] - public sealed class AuthenticationResult + private AuthenticationResult( + string accessToken, DateTimeOffset expiresOn, string tenantId, string userId, string identifier, string email, string name) { - private static readonly TimeSpan ExpirationBuffer = TimeSpan.FromSeconds(30); - - private AuthenticationResult( - string accessToken, DateTimeOffset expiresOn, string tenantId, string userId, string identifier, string email, string name) - { - AccessToken = accessToken; - ExpiresOn = expiresOn; - TenantId = tenantId; - UserId = userId; - Identifier = identifier; - Email = email; - Name = name; - } + AccessToken = accessToken; + ExpiresOn = expiresOn; + TenantId = tenantId; + UserId = userId; + Identifier = identifier; + Email = email; + Name = name; + } - public string AccessToken { get; } - public DateTimeOffset ExpiresOn { get; } - public string TenantId { get; } - public string UserId { get; } - public string Identifier { get; } - public string Email { get; } - public string Name { get; } - public bool IsExpired => ExpiresOn < DateTimeOffset.UtcNow.Add(ExpirationBuffer); + public string AccessToken { get; } + public DateTimeOffset ExpiresOn { get; } + public string TenantId { get; } + public string UserId { get; } + public string Identifier { get; } + public string Email { get; } + public string Name { get; } + public bool IsExpired => ExpiresOn < DateTimeOffset.UtcNow.Add(ExpirationBuffer); - public static AuthenticationResult From(Msal.AuthenticationResult msalResult) => new( - accessToken: msalResult.AccessToken, - expiresOn: msalResult.ExpiresOn, - tenantId: msalResult.TenantId, - userId: msalResult.UniqueId, - identifier: msalResult.Account.HomeAccountId.Identifier, - email: msalResult.Account.Username, - name: msalResult.ClaimsPrincipal.FindFirst((c) => c.Type == "name")?.Value ?? string.Empty); - } + public static AuthenticationResult From(Msal.AuthenticationResult msalResult) => new( + accessToken: msalResult.AccessToken, + expiresOn: msalResult.ExpiresOn, + tenantId: msalResult.TenantId, + userId: msalResult.UniqueId, + identifier: msalResult.Account.HomeAccountId.Identifier, + email: msalResult.Account.Username, + name: msalResult.ClaimsPrincipal.FindFirst((c) => c.Type == "name")?.Value ?? string.Empty); } diff --git a/src/Infrastructure/PowerBI/Cloud/Authentication/CloudAuthenticationClient.cs b/src/Infrastructure/PowerBI/Cloud/Authentication/CloudAuthenticationClient.cs index d0b07ec3..c14d8d9c 100644 --- a/src/Infrastructure/PowerBI/Cloud/Authentication/CloudAuthenticationClient.cs +++ b/src/Infrastructure/PowerBI/Cloud/Authentication/CloudAuthenticationClient.cs @@ -1,149 +1,151 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Authentication +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Desktop; +using Sqlbi.Bravo.Infrastructure.Configuration; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Policies; +using Msal = Microsoft.Identity.Client; + +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Authentication; + +public interface ICloudAuthenticationClient { - using Microsoft.Identity.Client; - using Microsoft.Identity.Client.Desktop; - using Sqlbi.Bravo.Infrastructure.Configuration; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Policies; - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; - using Msal = Microsoft.Identity.Client; - - public interface ICloudAuthenticationClient - { - Task AcquireTokenAsync(CloudEnvironment environment, string email, CancellationToken cancellationToken); + Task AcquireTokenAsync(CloudEnvironment environment, string email, CancellationToken cancellationToken); - Task ClearTokenCacheAsync(CloudEnvironment environment); - } + Task ClearTokenCacheAsync(CloudEnvironment environment); +} - /// - /// Handles authentication with Microsoft Entra ID (Azure AD) using MSAL.NET, including token acquisition and cache management. - /// - internal sealed class CloudAuthenticationClient(IPolicies policies) : ICloudAuthenticationClient - { - private const string SystemBrowserRedirectUri = "http://localhost"; - private const string OrganizationalAccountsOnlyQueryParameter = "msafed=0"; // no Microsoft accounts (MSA) allowed +/// +/// Handles authentication with Microsoft Entra ID (Azure AD) using MSAL.NET, including token acquisition and cache management. +/// +internal sealed class CloudAuthenticationClient(IPolicies policies) : ICloudAuthenticationClient +{ + private const string SystemBrowserRedirectUri = "http://localhost"; + private const string OrganizationalAccountsOnlyQueryParameter = "msafed=0"; // no Microsoft accounts (MSA) allowed - private readonly IPolicies _policies = policies; + private readonly IPolicies _policies = policies; - public async Task AcquireTokenAsync( - CloudEnvironment environment, string email, CancellationToken cancellationToken) + public async Task AcquireTokenAsync( + CloudEnvironment environment, string email, CancellationToken cancellationToken) + { + var client = CreatePublicClient(environment); + var scopes = CreateScopes(environment); + try { - var client = CreatePublicClient(environment); - var scopes = CreateScopes(environment); - try - { - // TODO: no B2B/guest-tenant support yet - acquiring a token for a workspace hosted in a tenant other than the - // user's home tenant would require passing a tenantId here and calling WithTenantId(tenantId) on the builders. - var msalResult = await AcquireTokenSilentAsync(client, scopes, email, cancellationToken).ConfigureAwait(false); - return AuthenticationResult.From(msalResult); - } - // Catching MsalServiceException (not just its MsalUiRequiredException subclass) also covers Conditional - // Access claims challenges, which MSAL surfaces as a plain MsalServiceException with a non-empty Claims - // See https://learn.microsoft.com/entra/msal/dotnet/advanced/exceptions/#handling-claim-challenge-exceptions-in-msalnet - catch (MsalServiceException ex) - { - var msalResult = await AcquireTokenInteractiveAsync(client, scopes, email, ex.Claims, cancellationToken).ConfigureAwait(false); - return AuthenticationResult.From(msalResult); - } + // TODO: no B2B/guest-tenant support yet - acquiring a token for a workspace hosted in a tenant other than the + // user's home tenant would require passing a tenantId here and calling WithTenantId(tenantId) on the builders. + var msalResult = await AcquireTokenSilentAsync(client, scopes, email, cancellationToken).ConfigureAwait(false); + return AuthenticationResult.From(msalResult); } - - public async Task ClearTokenCacheAsync(CloudEnvironment environment) + // Catching MsalServiceException (not just its MsalUiRequiredException subclass) also covers Conditional + // Access claims challenges, which MSAL surfaces as a plain MsalServiceException with a non-empty Claims + // See https://learn.microsoft.com/entra/msal/dotnet/advanced/exceptions/#handling-claim-challenge-exceptions-in-msalnet + catch (MsalServiceException ex) { - var client = CreatePublicClient(environment); - var accounts = (await client.GetAccountsAsync().ConfigureAwait(false)).ToArray(); + var msalResult = await AcquireTokenInteractiveAsync(client, scopes, email, ex.Claims, cancellationToken).ConfigureAwait(false); + return AuthenticationResult.From(msalResult); + } + } - foreach (var account in accounts) - { - await client.RemoveAsync(account).ConfigureAwait(false); - } + public async Task ClearTokenCacheAsync(CloudEnvironment environment) + { + var client = CreatePublicClient(environment); + var accounts = (await client.GetAccountsAsync().ConfigureAwait(false)).ToArray(); + + foreach (var account in accounts) + { + await client.RemoveAsync(account).ConfigureAwait(false); } + } - private static async Task AcquireTokenSilentAsync( - IPublicClientApplication client, string[] scopes, string email, CancellationToken cancellationToken) + private static async Task AcquireTokenSilentAsync( + IPublicClientApplication client, string[] scopes, string email, CancellationToken cancellationToken) + { + var extraQueryParameters = OrganizationalAccountsOnlyQueryParameter; + var loginHint = email; + + var builder = client.AcquireTokenSilent(scopes, loginHint) + .WithExtraQueryParameters(extraQueryParameters); + + return await builder.ExecuteAsync(cancellationToken).ConfigureAwait(false); + } + + private async Task AcquireTokenInteractiveAsync( + IPublicClientApplication client, string[] scopes, string email, string claims, CancellationToken cancellationToken) + { + var useSystemBrowser = _policies.UseSystemBrowserForAuthentication ?? UserPreferences.Current.UseSystemBrowserForAuthentication; + var useEmbeddedBrowser = !useSystemBrowser; + var extraQueryParameters = OrganizationalAccountsOnlyQueryParameter; + var prompt = Prompt.SelectAccount; + var loginHint = email; + + // AcquireTokenInteractive(scopes) captures SynchronizationContext.Current so the builder must be + // created on the UI thread to ensure that the interactive flow is executed on the UI thread. + var builder = ProcessHelper.RunWithUISynchronizationContext(() => { - var extraQueryParameters = OrganizationalAccountsOnlyQueryParameter; - var loginHint = email; + return client.AcquireTokenInteractive(scopes); + }); - var builder = client.AcquireTokenSilent(scopes, loginHint) - .WithExtraQueryParameters(extraQueryParameters); + builder + .WithExtraQueryParameters(extraQueryParameters) + .WithUseEmbeddedWebView(useEmbeddedBrowser) + .WithLoginHint(loginHint) + .WithPrompt(prompt) + .WithClaims(claims); - return await builder.ExecuteAsync(cancellationToken).ConfigureAwait(false); - } + using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - private async Task AcquireTokenInteractiveAsync( - IPublicClientApplication client, string[] scopes, string email, string claims, CancellationToken cancellationToken) + if (useEmbeddedBrowser) + { + var windowHandle = ProcessHelper.GetCurrentProcessMainWindowHandle(); + builder.WithParentActivityOrWindow(windowHandle); + } + else // use system browser { - var useSystemBrowser = _policies.UseSystemBrowserForAuthentication ?? UserPreferences.Current.UseSystemBrowserForAuthentication; - var useEmbeddedBrowser = !useSystemBrowser; - var extraQueryParameters = OrganizationalAccountsOnlyQueryParameter; - var prompt = Prompt.SelectAccount; - var loginHint = email; - - // AcquireTokenInteractive(scopes) captures SynchronizationContext.Current so the builder must be - // created on the UI thread to ensure that the interactive flow is executed on the UI thread. - var builder = ProcessHelper.RunWithUISynchronizationContext(() => - { - return client.AcquireTokenInteractive(scopes); - }); - - builder - .WithExtraQueryParameters(extraQueryParameters) - .WithUseEmbeddedWebView(useEmbeddedBrowser) - .WithLoginHint(loginHint) - .WithPrompt(prompt) - .WithClaims(claims); - - using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - - if (useEmbeddedBrowser) - { - var windowHandle = ProcessHelper.GetCurrentProcessMainWindowHandle(); - builder.WithParentActivityOrWindow(windowHandle); - } - else // use system browser - { - // The system browser is a separate, untracked OS process: there is no way to detect the user closing - // it, so a hard ceiling is the only safeguard against an indefinitely pending sign-in. - cancellationTokenSource.CancelAfter(TimeSpan.FromMinutes(2)); - } - - try - { - return await builder.ExecuteAsync(cancellationTokenSource.Token).ConfigureAwait(false); - } - catch (MsalException ex) when (ex.IsAuthenticationCanceled()) - { - // The user canceled the sign-in prompt, either by closing the embedded browser or the system browser. - // Normalize the exception to OperationCanceledException, like the other cancellation paths - throw new OperationCanceledException("Authentication was canceled by the user.", ex); - } + // The system browser is a separate, untracked OS process: there is no way to detect the user closing + // it, so a hard ceiling is the only safeguard against an indefinitely pending sign-in. + cancellationTokenSource.CancelAfter(TimeSpan.FromMinutes(2)); } - private IPublicClientApplication CreatePublicClient(CloudEnvironment environment) + try + { + return await builder.ExecuteAsync(cancellationTokenSource.Token).ConfigureAwait(false); + } + catch (MsalException ex) when (ex.IsAuthenticationCanceled()) { - var useSystemBrowser = _policies.UseSystemBrowserForAuthentication ?? UserPreferences.Current.UseSystemBrowserForAuthentication; - var useEmbeddedBrowser = !useSystemBrowser; - var redirectUri = (useEmbeddedBrowser ? environment.RedirectUri : SystemBrowserRedirectUri); + // The user canceled the sign-in prompt, either by closing the embedded browser or the system browser. + // Normalize the exception to OperationCanceledException, like the other cancellation paths + throw new OperationCanceledException("Authentication was canceled by the user.", ex); + } + } - var builder = PublicClientApplicationBuilder.Create(environment.ClientId) - .WithAuthority(environment.AuthorityUri) - .WithRedirectUri(redirectUri); + private IPublicClientApplication CreatePublicClient(CloudEnvironment environment) + { + var useSystemBrowser = _policies.UseSystemBrowserForAuthentication ?? UserPreferences.Current.UseSystemBrowserForAuthentication; + var useEmbeddedBrowser = !useSystemBrowser; + var redirectUri = (useEmbeddedBrowser ? environment.RedirectUri : SystemBrowserRedirectUri); - if (useEmbeddedBrowser) - builder.WithWindowsEmbeddedBrowserSupport(); + var builder = PublicClientApplicationBuilder.Create(environment.ClientId) + .WithAuthority(environment.AuthorityUri) + .WithRedirectUri(redirectUri); - var client = builder.Build(); + if (useEmbeddedBrowser) + builder.WithWindowsEmbeddedBrowserSupport(); - TokenCacheHelper.RegisterCache(client.UserTokenCache); + var client = builder.Build(); - return client; - } + TokenCacheHelper.RegisterCache(client.UserTokenCache); - private static string[] CreateScopes(CloudEnvironment environment) - { - var resource = environment.ResourceId.TrimEnd('/'); - return [$"{resource}/.default"]; - } + return client; + } + + private static string[] CreateScopes(CloudEnvironment environment) + { + var resource = environment.ResourceId.TrimEnd('/'); + return [$"{resource}/.default"]; } } diff --git a/src/Infrastructure/PowerBI/Cloud/Authentication/CloudAuthenticationService.cs b/src/Infrastructure/PowerBI/Cloud/Authentication/CloudAuthenticationService.cs index df47508d..20750a3b 100644 --- a/src/Infrastructure/PowerBI/Cloud/Authentication/CloudAuthenticationService.cs +++ b/src/Infrastructure/PowerBI/Cloud/Authentication/CloudAuthenticationService.cs @@ -1,91 +1,92 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Authentication -{ - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Configuration; - using Sqlbi.Bravo.Models; +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Configuration; +using Sqlbi.Bravo.Models; - public interface ICloudAuthenticationService - { - Task SignInAsync(string email, CloudEnvironment environment, CancellationToken cancellationToken); +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Authentication; - Task EnsureSignedInAsync(CancellationToken cancellationToken); +public interface ICloudAuthenticationService +{ + Task SignInAsync(string email, CloudEnvironment environment, CancellationToken cancellationToken); - Task SignOutAsync(CancellationToken cancellationToken); - } + Task EnsureSignedInAsync(CancellationToken cancellationToken); - /// - /// Orchestrates PBI Cloud sign-in/out and holds session state, delegating all MSAL work to . - /// - internal class CloudAuthenticationService( - ICloudAuthenticationClient cloudAuthenticationClient, - ICloudConfigurationService cloudConfigurationService) : ICloudAuthenticationService, IDisposable - { - private readonly ICloudAuthenticationClient _cloudAuthenticationClient = cloudAuthenticationClient; - private readonly ICloudConfigurationService _cloudConfigurationService = cloudConfigurationService; - private readonly SemaphoreSlim _authenticationSemaphore = new(1, 1); + Task SignOutAsync(CancellationToken cancellationToken); +} + +/// +/// Orchestrates PBI Cloud sign-in/out and holds session state, delegating all MSAL work to . +/// +internal class CloudAuthenticationService( + ICloudAuthenticationClient cloudAuthenticationClient, + ICloudConfigurationService cloudConfigurationService) : ICloudAuthenticationService, IDisposable +{ + private readonly ICloudAuthenticationClient _cloudAuthenticationClient = cloudAuthenticationClient; + private readonly ICloudConfigurationService _cloudConfigurationService = cloudConfigurationService; + private readonly SemaphoreSlim _authenticationSemaphore = new(1, 1); - private AuthenticatedSession? _currentSession; + private AuthenticatedSession? _currentSession; - public async Task SignInAsync(string email, CloudEnvironment environment, CancellationToken cancellationToken) + public async Task SignInAsync(string email, CloudEnvironment environment, CancellationToken cancellationToken) + { + await _authenticationSemaphore.WaitAsync(cancellationToken); + try { - await _authenticationSemaphore.WaitAsync(cancellationToken); - try - { - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(CloudAuthenticationService)}.{nameof(SignInAsync)}", JsonSerializer.Serialize(environment)); + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(CloudAuthenticationService)}.{nameof(SignInAsync)}", JsonSerializer.Serialize(environment)); - var authenticationResult = await _cloudAuthenticationClient.AcquireTokenAsync(environment, email, cancellationToken); - var clusterUri = await _cloudConfigurationService.ResolveTenantClusterUriAsync(environment, authenticationResult.AccessToken, cancellationToken); + var authenticationResult = await _cloudAuthenticationClient.AcquireTokenAsync(environment, email, cancellationToken); + var clusterUri = await _cloudConfigurationService.ResolveTenantClusterUriAsync(environment, authenticationResult.AccessToken, cancellationToken); - var newEnvironment = environment with { ClusterUri = clusterUri }; - var newSession = new AuthenticatedSession(authenticationResult, newEnvironment); + var newEnvironment = environment with { ClusterUri = clusterUri }; + var newSession = new AuthenticatedSession(authenticationResult, newEnvironment); - return _currentSession = newSession; - } - finally - { - _authenticationSemaphore.Release(); - } + return _currentSession = newSession; } - - public async Task EnsureSignedInAsync(CancellationToken cancellationToken) + finally { - var session = _currentSession; - if (session is null) - return null; + _authenticationSemaphore.Release(); + } + } - if (session.AuthenticationResult.IsExpired) - return await SignInAsync(session.AuthenticationResult.Email, session.Environment, cancellationToken); + public async Task EnsureSignedInAsync(CancellationToken cancellationToken) + { + var session = _currentSession; + if (session is null) + return null; - return session; - } + if (session.AuthenticationResult.IsExpired) + return await SignInAsync(session.AuthenticationResult.Email, session.Environment, cancellationToken); - public async Task SignOutAsync(CancellationToken cancellationToken) - { - await _authenticationSemaphore.WaitAsync(cancellationToken); - try - { - if (_currentSession is not null) - { - await _cloudAuthenticationClient.ClearTokenCacheAsync(_currentSession.Environment); - } + return session; + } - _currentSession = null; - } - finally + public async Task SignOutAsync(CancellationToken cancellationToken) + { + await _authenticationSemaphore.WaitAsync(cancellationToken); + try + { + if (_currentSession is not null) { - _authenticationSemaphore.Release(); + await _cloudAuthenticationClient.ClearTokenCacheAsync(_currentSession.Environment); } - } - - #region IDisposable - public void Dispose() + _currentSession = null; + } + finally { - _authenticationSemaphore.Dispose(); + _authenticationSemaphore.Release(); } + } - #endregion + #region IDisposable + + public void Dispose() + { + _authenticationSemaphore.Dispose(); } + + #endregion } diff --git a/src/Infrastructure/PowerBI/Cloud/CloudApiClient.cs b/src/Infrastructure/PowerBI/Cloud/CloudApiClient.cs index 44b5c98f..83b3437b 100644 --- a/src/Infrastructure/PowerBI/Cloud/CloudApiClient.cs +++ b/src/Infrastructure/PowerBI/Cloud/CloudApiClient.cs @@ -1,148 +1,154 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Authentication; +using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; +using Sqlbi.Bravo.Models; + +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; + +public interface ICloudApiClient { - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Authentication; - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; - using Sqlbi.Bravo.Models; - using System.Drawing; - using System.Drawing.Imaging; - using System.Net.Http; - using System.Net.Http.Headers; - - public interface ICloudApiClient - { - Task GetUserPhotoAsync(AuthenticatedSession session, CancellationToken cancellationToken); + Task GetUserPhotoAsync(AuthenticatedSession session, CancellationToken cancellationToken); - Task> GetDatasetsAsync(AuthenticatedSession session, CancellationToken cancellationToken); - } + Task> GetDatasetsAsync(AuthenticatedSession session, CancellationToken cancellationToken); +} - internal class CloudApiClient : ICloudApiClient +internal class CloudApiClient : ICloudApiClient +{ + private readonly HttpClient _httpClient; + private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web) { - private readonly HttpClient _httpClient; - private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web) - { - PropertyNameCaseInsensitive = false, // required by SharedDatasetModel LastRefreshTime/lastRefreshTime properties - }; + PropertyNameCaseInsensitive = false, // required by SharedDatasetModel LastRefreshTime/lastRefreshTime properties + }; - public const string PBIDatasetProtocolScheme = "pbiazure"; - public const string PBIPremiumXmlaEndpointProtocolScheme = "powerbi"; - //public const string PBIPremiumDedicatedProtocolScheme = "pbidedicated"; - public const string ASAzureProtocolScheme = "asazure"; - //public const string ASAzureLinkProtocolScheme = "link"; + public const string PBIDatasetProtocolScheme = "pbiazure"; + public const string PBIPremiumXmlaEndpointProtocolScheme = "powerbi"; + //public const string PBIPremiumDedicatedProtocolScheme = "pbidedicated"; + public const string ASAzureProtocolScheme = "asazure"; + //public const string ASAzureLinkProtocolScheme = "link"; - public CloudApiClient(IHttpClientFactory httpClientFactory) - { - _httpClient = httpClientFactory.CreateClient(ServiceCollectionExtensions.PowerBIApiHttpClientName); - } + public CloudApiClient(IHttpClientFactory httpClientFactory) + { + _httpClient = httpClientFactory.CreateClient(ServiceCollectionExtensions.PowerBIApiHttpClientName); + } - public async Task GetUserPhotoAsync(AuthenticatedSession session, CancellationToken cancellationToken) - { - var relativeUri = "powerbi/version/201606/resource/userPhoto/?userId={0}".FormatInvariant(session.AuthenticationResult.Email); - var requestUri = session.Environment.GetBackendRequestUri(relativeUri); + public async Task GetUserPhotoAsync(AuthenticatedSession session, CancellationToken cancellationToken) + { + var relativeUri = "powerbi/version/201606/resource/userPhoto/?userId={0}".FormatInvariant(session.AuthenticationResult.Email); + var requestUri = session.Environment.GetBackendRequestUri(relativeUri); - using var httpRequest = new HttpRequestMessage(HttpMethod.Get, requestUri); - httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", session.AuthenticationResult.AccessToken); + using var httpRequest = new HttpRequestMessage(HttpMethod.Get, requestUri); + httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", session.AuthenticationResult.AccessToken); - using var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); + using var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); - // Any non-200 response (no photo, server error, ...) is treated the same way: no photo to show. - if (httpResponse.StatusCode != HttpStatusCode.OK) - return null; + // Any non-200 response (no photo, server error, ...) is treated the same way: no photo to show. + if (httpResponse.StatusCode != HttpStatusCode.OK) + return null; - using var bitmapStream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken); - using var bitmap = TryCreateBitmap(bitmapStream); - if (bitmap is null) - return null; // Invalid image data + using var bitmapStream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken); + using var bitmap = TryCreateBitmap(bitmapStream); + if (bitmap is null) + return null; // Invalid image data - var mimeType = TryGetMimeType(bitmap); - if (mimeType is null) - return null; // Unknown image format + var mimeType = TryGetMimeType(bitmap); + if (mimeType is null) + return null; // Unknown image format - var base64String = GetBase64String(bitmap); + var base64String = GetBase64String(bitmap); - return "data:{0};base64,{1}".FormatInvariant(mimeType, base64String); + return "data:{0};base64,{1}".FormatInvariant(mimeType, base64String); - static Bitmap? TryCreateBitmap(Stream stream) + static Bitmap? TryCreateBitmap(Stream stream) + { + try { - try - { - return new Bitmap(stream); - } - catch (ArgumentException) - { - return null; - } + return new Bitmap(stream); } - - static string? TryGetMimeType(Bitmap bitmap) - => ImageCodecInfo.GetImageDecoders().FirstOrDefault((c) => c.FormatID == bitmap.RawFormat.Guid)?.MimeType; - - static string GetBase64String(Bitmap bitmap) + catch (ArgumentException) { - using var stream = new MemoryStream(); - bitmap.Save(stream, bitmap.RawFormat); - return Convert.ToBase64String(stream.ToArray()); + return null; } } - public async Task> GetDatasetsAsync(AuthenticatedSession session, CancellationToken cancellationToken) + static string? TryGetMimeType(Bitmap bitmap) + => ImageCodecInfo.GetImageDecoders().FirstOrDefault((c) => c.FormatID == bitmap.RawFormat.Guid)?.MimeType; + + static string GetBase64String(Bitmap bitmap) { - var cloudWorkspaces = await GetCloudWorkspacesAsync(session, cancellationToken); - var cloudSharedModels = await GetCloudSharedModelsAsync(session, cancellationToken); + using var stream = new MemoryStream(); + bitmap.Save(stream, bitmap.RawFormat); + return Convert.ToBase64String(stream.ToArray()); + } + } - if (AppEnvironment.IsDiagnosticLevelVerbose) - { - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{ nameof(CloudApiClient) }.{ nameof(GetDatasetsAsync) }.{ nameof(cloudWorkspaces) }", content: JsonSerializer.Serialize(cloudWorkspaces)); - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{ nameof(CloudApiClient) }.{ nameof(GetDatasetsAsync) }.{ nameof(cloudSharedModels) }", content: JsonSerializer.Serialize(cloudSharedModels)); - } + public async Task> GetDatasetsAsync(AuthenticatedSession session, CancellationToken cancellationToken) + { + var cloudWorkspaces = await GetCloudWorkspacesAsync(session, cancellationToken); + var cloudSharedModels = await GetCloudSharedModelsAsync(session, cancellationToken); - var datasets = cloudWorkspaces.Join(cloudSharedModels, (w) => w.ObjectId?.ToLowerInvariant(), (d) => d.ObjectId?.ToLowerInvariant(), (w, d) => PBICloudDataset.CreateFrom(session.Environment, w, d)).ToArray(); + if (AppEnvironment.IsDiagnosticLevelVerbose) + { + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(CloudApiClient)}.{nameof(GetDatasetsAsync)}.{nameof(cloudWorkspaces)}", content: JsonSerializer.Serialize(cloudWorkspaces)); + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(CloudApiClient)}.{nameof(GetDatasetsAsync)}.{nameof(cloudSharedModels)}", content: JsonSerializer.Serialize(cloudSharedModels)); + } - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{ nameof(CloudApiClient) }.{ nameof(GetDatasetsAsync) }", content: JsonSerializer.Serialize(datasets)); + var datasets = cloudWorkspaces.Join(cloudSharedModels, (w) => w.ObjectId?.ToLowerInvariant(), (d) => d.ObjectId?.ToLowerInvariant(), (w, d) => PBICloudDataset.CreateFrom(session.Environment, w, d)).ToArray(); - return datasets; - } + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(CloudApiClient)}.{nameof(GetDatasetsAsync)}", content: JsonSerializer.Serialize(datasets)); - private async Task> GetCloudWorkspacesAsync(AuthenticatedSession session, CancellationToken cancellationToken) - { - var baseUri = new Uri(session.Environment.ClusterUri); - var relativeUri = "powerbi/databases/v201606/workspaces"; - var requestUri = new Uri(baseUri, relativeUri); + return datasets; + } - using var httpRequest = new HttpRequestMessage(HttpMethod.Get, requestUri); - httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", session.AuthenticationResult.AccessToken); + private async Task> GetCloudWorkspacesAsync(AuthenticatedSession session, CancellationToken cancellationToken) + { + var baseUri = new Uri(session.Environment.ClusterUri); + var relativeUri = "powerbi/databases/v201606/workspaces"; + var requestUri = new Uri(baseUri, relativeUri); - using var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); - httpResponse.EnsureSuccessStatusCode(); + using var httpRequest = new HttpRequestMessage(HttpMethod.Get, requestUri); + httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", session.AuthenticationResult.AccessToken); - var json = await httpResponse.Content.ReadAsStringAsync(cancellationToken); + using var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); + httpResponse.EnsureSuccessStatusCode(); - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{ nameof(CloudApiClient) }.{ nameof(GetCloudWorkspacesAsync) }", json); + var json = await httpResponse.Content.ReadAsStringAsync(cancellationToken); - return JsonSerializer.Deserialize(json, _jsonOptions) ?? []; - } + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(CloudApiClient)}.{nameof(GetCloudWorkspacesAsync)}", json); - private async Task> GetCloudSharedModelsAsync(AuthenticatedSession session, CancellationToken cancellationToken) - { - var baseUri = new Uri(session.Environment.ClusterUri); - var relativeUri = "metadata/v201901/gallery/sharedDatasets"; - var requestUri = new Uri(baseUri, relativeUri); + return JsonSerializer.Deserialize(json, _jsonOptions) ?? []; + } + + private async Task> GetCloudSharedModelsAsync(AuthenticatedSession session, CancellationToken cancellationToken) + { + var baseUri = new Uri(session.Environment.ClusterUri); + var relativeUri = "metadata/v201901/gallery/sharedDatasets"; + var requestUri = new Uri(baseUri, relativeUri); - using var httpRequest = new HttpRequestMessage(HttpMethod.Get, requestUri); - httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", session.AuthenticationResult.AccessToken); + using var httpRequest = new HttpRequestMessage(HttpMethod.Get, requestUri); + httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", session.AuthenticationResult.AccessToken); - using var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); - httpResponse.EnsureSuccessStatusCode(); + using var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); + httpResponse.EnsureSuccessStatusCode(); - var json = await httpResponse.Content.ReadAsStringAsync(cancellationToken); + var json = await httpResponse.Content.ReadAsStringAsync(cancellationToken); - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{ nameof(CloudApiClient) }.{ nameof(GetCloudSharedModelsAsync) }", json); + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(CloudApiClient)}.{nameof(GetCloudSharedModelsAsync)}", json); - return JsonSerializer.Deserialize(json, _jsonOptions) ?? []; - } + return JsonSerializer.Deserialize(json, _jsonOptions) ?? []; } } diff --git a/src/Infrastructure/PowerBI/Cloud/CloudEnvironment.cs b/src/Infrastructure/PowerBI/Cloud/CloudEnvironment.cs index 2942b9b6..704e403f 100644 --- a/src/Infrastructure/PowerBI/Cloud/CloudEnvironment.cs +++ b/src/Infrastructure/PowerBI/Cloud/CloudEnvironment.cs @@ -1,40 +1,42 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud -{ - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; +using System; +using System.Diagnostics; +using System.Linq; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; - [DebuggerDisplay("{Name}")] - public sealed record CloudEnvironment( - string Name, - string Description, - string AuthorityUri, - string ClientId, - string RedirectUri, - string ResourceId, - string BackendUri, - string ClusterUri) - { - public Uri GetBackendRequestUri(string relativeUri) - => new(new Uri(BackendUri), relativeUri); +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; - public string GetIdentityProvider() - => $"{AuthorityUri}, {ResourceId}, {ClientId}"; +[DebuggerDisplay("{Name}")] +public sealed record CloudEnvironment( + string Name, + string Description, + string AuthorityUri, + string ClientId, + string RedirectUri, + string ResourceId, + string BackendUri, + string ClusterUri) +{ + public Uri GetBackendRequestUri(string relativeUri) + => new(new Uri(BackendUri), relativeUri); - internal static CloudEnvironment FromContract(CloudEnvironmentContract contract) - { - var aadService = contract.Services.Single((s) => s.IsAad()); - var powerbiBackendService = contract.Services.Single((s) => s.IsPowerBIBackend()); - var powerbiDesktopClient = contract.Clients.Single((c) => c.IsPowerBIDesktop()); + public string GetIdentityProvider() + => $"{AuthorityUri}, {ResourceId}, {ClientId}"; + + internal static CloudEnvironment FromContract(CloudEnvironmentContract contract) + { + var aadService = contract.Services.Single((s) => s.IsAad()); + var powerbiBackendService = contract.Services.Single((s) => s.IsPowerBIBackend()); + var powerbiDesktopClient = contract.Clients.Single((c) => c.IsPowerBIDesktop()); - return new CloudEnvironment( - Name: contract.CloudName, - Description: contract.GetDescription(), - AuthorityUri: aadService.Endpoint, - ClientId: powerbiDesktopClient.AppId, - RedirectUri: powerbiDesktopClient.RedirectUri, - ResourceId: powerbiBackendService.ResourceId, - BackendUri: powerbiBackendService.Endpoint, - ClusterUri: string.Empty); - } + return new CloudEnvironment( + Name: contract.CloudName, + Description: contract.GetDescription(), + AuthorityUri: aadService.Endpoint, + ClientId: powerbiDesktopClient.AppId, + RedirectUri: powerbiDesktopClient.RedirectUri, + ResourceId: powerbiBackendService.ResourceId, + BackendUri: powerbiBackendService.Endpoint, + ClusterUri: string.Empty); } } diff --git a/src/Infrastructure/PowerBI/Cloud/Configuration/CloudConfigurationService.cs b/src/Infrastructure/PowerBI/Cloud/Configuration/CloudConfigurationService.cs index bdc2cabd..2002d4df 100644 --- a/src/Infrastructure/PowerBI/Cloud/Configuration/CloudConfigurationService.cs +++ b/src/Infrastructure/PowerBI/Cloud/Configuration/CloudConfigurationService.cs @@ -1,96 +1,100 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Configuration +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Web; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; +using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Serialization; +using Sqlbi.Bravo.Models; + +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Configuration; + +internal interface ICloudConfigurationService { - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.PowerBI; - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Serialization; - using Sqlbi.Bravo.Models; - using System.Net.Http; - using System.Net.Http.Headers; - using System.Web; - - internal interface ICloudConfigurationService + Task> DiscoverEnvironmentsAsync(string email, CancellationToken cancellationToken); + + Task ResolveTenantClusterUriAsync(CloudEnvironment environment, string accessToken, CancellationToken cancellationToken); +} + +internal sealed class CloudConfigurationService : ICloudConfigurationService +{ + // The Global Cloud is the default discovery base URI for Power BI service discovery. + private static readonly Uri s_defaultDiscoveryBaseUri = new("https://api.powerbi.com", UriKind.Absolute); + + private readonly HttpClient _httpClient; + private readonly Uri _discoveryBaseUri; + + public CloudConfigurationService(IHttpClientFactory httpClientFactory, ILocalConfigurationReader localConfigurationReader) { - Task> DiscoverEnvironmentsAsync(string email, CancellationToken cancellationToken); + _httpClient = httpClientFactory.CreateClient(ServiceCollectionExtensions.PowerBIApiHttpClientName); - Task ResolveTenantClusterUriAsync(CloudEnvironment environment, string accessToken, CancellationToken cancellationToken); + _discoveryBaseUri = localConfigurationReader.GetPowerBIServiceDiscoveryBaseUri() + ?? s_defaultDiscoveryBaseUri; } - internal sealed class CloudConfigurationService : ICloudConfigurationService + public async Task> DiscoverEnvironmentsAsync(string email, CancellationToken cancellationToken) { - // The Global Cloud is the default discovery base URI for Power BI service discovery. - private static readonly Uri s_defaultDiscoveryBaseUri = new("https://api.powerbi.com", UriKind.Absolute); + var response = await DiscoverEnvironmentsAsync(email, apiVersion: "v202408", cancellationToken); + if (response is null) + response = await DiscoverEnvironmentsAsync(email, apiVersion: "v202003", cancellationToken); - private readonly HttpClient _httpClient; - private readonly Uri _discoveryBaseUri; + var environments = response?.Environments ?? []; - public CloudConfigurationService(IHttpClientFactory httpClientFactory, ILocalConfigurationReader localConfigurationReader) - { - _httpClient = httpClientFactory.CreateClient(ServiceCollectionExtensions.PowerBIApiHttpClientName); + return [.. environments + .Where((e) => !e.IsMicrosoftInternalCloud()) // Filter out Microsoft internal environments + .Select(CloudEnvironment.FromContract)]; + } - _discoveryBaseUri = localConfigurationReader.GetPowerBIServiceDiscoveryBaseUri() - ?? s_defaultDiscoveryBaseUri; - } + public async Task ResolveTenantClusterUriAsync(CloudEnvironment environment, string accessToken, CancellationToken cancellationToken) + { + var relativeUri = "spglobalservice/GetOrInsertClusterUrisByTenantlocation"; + var requestUri = environment.GetBackendRequestUri(relativeUri); - public async Task> DiscoverEnvironmentsAsync(string email, CancellationToken cancellationToken) - { - var response = await DiscoverEnvironmentsAsync(email, apiVersion: "v202408", cancellationToken); - if (response is null) - response = await DiscoverEnvironmentsAsync(email, apiVersion: "v202003", cancellationToken); + using var httpRequest = new HttpRequestMessage(HttpMethod.Put, requestUri); + httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + httpRequest.Content = new StringContent(string.Empty, Encoding.UTF8, MediaTypeNames.Application.Json); - var environments = response?.Environments ?? []; + using var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); + httpResponse.EnsureSuccessStatusCode(); - return [.. environments - .Where((e) => !e.IsMicrosoftInternalCloud()) // Filter out Microsoft internal environments - .Select(CloudEnvironment.FromContract)]; - } + var json = await httpResponse.Content.ReadAsStringAsync(cancellationToken); - public async Task ResolveTenantClusterUriAsync(CloudEnvironment environment, string accessToken, CancellationToken cancellationToken) - { - var relativeUri = "spglobalservice/GetOrInsertClusterUrisByTenantlocation"; - var requestUri = environment.GetBackendRequestUri(relativeUri); + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(CloudConfigurationService)}.{nameof(ResolveTenantClusterUriAsync)}", json); + + var tenantCluster = CloudContractJsonSerializer.Deserialize(json); + return tenantCluster.FixedClusterUri; + } - using var httpRequest = new HttpRequestMessage(HttpMethod.Put, requestUri); - httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); - httpRequest.Content = new StringContent(string.Empty, Encoding.UTF8, MediaTypeNames.Application.Json); + private async Task DiscoverEnvironmentsAsync(string email, string apiVersion, CancellationToken cancellationToken) + { + var relativeUri = "powerbi/globalservice/{0}/environments/discover?user={1}".FormatInvariant(apiVersion, HttpUtility.UrlEncode(email)); + var requestUri = new Uri(_discoveryBaseUri, relativeUri); - using var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); - httpResponse.EnsureSuccessStatusCode(); + using var httpRequest = new HttpRequestMessage(HttpMethod.Post, requestUri); + using var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); + if (httpResponse.StatusCode == HttpStatusCode.OK) + { var json = await httpResponse.Content.ReadAsStringAsync(cancellationToken); if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(CloudConfigurationService)}.{nameof(ResolveTenantClusterUriAsync)}", json); + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(CloudConfigurationService)}.{nameof(DiscoverEnvironmentsAsync)}()", content: json); - var tenantCluster = CloudContractJsonSerializer.Deserialize(json); - return tenantCluster.FixedClusterUri; + return CloudContractJsonSerializer.Deserialize(json); } - - private async Task DiscoverEnvironmentsAsync(string email, string apiVersion, CancellationToken cancellationToken) + else if (httpResponse.StatusCode == HttpStatusCode.NotFound) { - var relativeUri = "powerbi/globalservice/{0}/environments/discover?user={1}".FormatInvariant(apiVersion, HttpUtility.UrlEncode(email)); - var requestUri = new Uri(_discoveryBaseUri, relativeUri); - - using var httpRequest = new HttpRequestMessage(HttpMethod.Post, requestUri); - using var httpResponse = await _httpClient.SendAsync(httpRequest, cancellationToken); - - if (httpResponse.StatusCode == HttpStatusCode.OK) - { - var json = await httpResponse.Content.ReadAsStringAsync(cancellationToken); - - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(CloudConfigurationService)}.{nameof(DiscoverEnvironmentsAsync)}()", content: json); - - return CloudContractJsonSerializer.Deserialize(json); - } - else if (httpResponse.StatusCode == HttpStatusCode.NotFound) - { - return null; - } - - throw new HttpRequestException($"Unexpected response status code {(int)httpResponse.StatusCode} ({httpResponse.ReasonPhrase}) from environment discovery.", inner: null, httpResponse.StatusCode); + return null; } + + throw new HttpRequestException($"Unexpected response status code {(int)httpResponse.StatusCode} ({httpResponse.ReasonPhrase}) from environment discovery.", inner: null, httpResponse.StatusCode); } } diff --git a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudEnvironmentClientContract.cs b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudEnvironmentClientContract.cs index 33e612c5..7e55c607 100644 --- a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudEnvironmentClientContract.cs +++ b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudEnvironmentClientContract.cs @@ -1,23 +1,24 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts -{ - using System.Text.Json.Serialization; +using System; +using System.Diagnostics; +using System.Text.Json.Serialization; + +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; - [DebuggerDisplay("{Name}")] - internal sealed class CloudEnvironmentClientContract - { - [JsonPropertyName("name")] - public string Name { get; set; } = null!; +[DebuggerDisplay("{Name}")] +internal sealed class CloudEnvironmentClientContract +{ + [JsonPropertyName("name")] + public string Name { get; set; } = null!; - [JsonPropertyName("appId")] - public string AppId { get; set; } = null!; + [JsonPropertyName("appId")] + public string AppId { get; set; } = null!; - [JsonPropertyName("redirectUri")] - public string RedirectUri { get; set; } = null!; - } + [JsonPropertyName("redirectUri")] + public string RedirectUri { get; set; } = null!; +} - internal static class CloudEnvironmentClientContractExtension - { - public static bool IsPowerBIDesktop(this CloudEnvironmentClientContract client) - => client.Name.Equals("powerbi-desktop", StringComparison.Ordinal); - } -} \ No newline at end of file +internal static class CloudEnvironmentClientContractExtension +{ + public static bool IsPowerBIDesktop(this CloudEnvironmentClientContract client) + => client.Name.Equals("powerbi-desktop", StringComparison.Ordinal); +} diff --git a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudEnvironmentContract.cs b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudEnvironmentContract.cs index 36be5843..d9edcf69 100644 --- a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudEnvironmentContract.cs +++ b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudEnvironmentContract.cs @@ -1,42 +1,44 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts -{ - using System.Text.Json.Serialization; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text.Json.Serialization; - [DebuggerDisplay("{CloudName}")] - internal sealed class CloudEnvironmentContract - { - [JsonPropertyName("cloudName")] - public string CloudName { get; set; } = null!; +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; - [JsonPropertyName("clients")] - public CloudEnvironmentClientContract[] Clients { get; set; } = null!; +[DebuggerDisplay("{CloudName}")] +internal sealed class CloudEnvironmentContract +{ + [JsonPropertyName("cloudName")] + public string CloudName { get; set; } = null!; - [JsonPropertyName("services")] - public CloudEnvironmentServiceContract[] Services { get; set; } = null!; - } + [JsonPropertyName("clients")] + public CloudEnvironmentClientContract[] Clients { get; set; } = null!; - internal static class CloudEnvironmentContractExtension + [JsonPropertyName("services")] + public CloudEnvironmentServiceContract[] Services { get; set; } = null!; +} + +internal static class CloudEnvironmentContractExtension +{ + public static string GetDescription(this CloudEnvironmentContract environment) => environment.CloudName switch { - public static string GetDescription(this CloudEnvironmentContract environment) => environment.CloudName switch - { - "GlobalCloud" => "Power BI", - "ChinaCloud" => "Power BI operated by 21Vianet in China", - "USGovCloud" => "Power BI for US Government", // gcc - "USGovDoDL4Cloud" => "Power BI for US Government (L4)", // gcc_high - "USGovDoDL5Cloud" => "Power BI for US Government (L5)", // gcc_dod - _ => environment.CloudName, - }; + "GlobalCloud" => "Power BI", + "ChinaCloud" => "Power BI operated by 21Vianet in China", + "USGovCloud" => "Power BI for US Government", // gcc + "USGovDoDL4Cloud" => "Power BI for US Government (L4)", // gcc_high + "USGovDoDL5Cloud" => "Power BI for US Government (L5)", // gcc_dod + _ => environment.CloudName, + }; - public static bool IsMicrosoftInternalCloud(this CloudEnvironmentContract environment) - => s_microsoftInternalClouds.Contains(environment.CloudName); + public static bool IsMicrosoftInternalCloud(this CloudEnvironmentContract environment) + => s_microsoftInternalClouds.Contains(environment.CloudName); - private readonly static HashSet s_microsoftInternalClouds = new(StringComparer.OrdinalIgnoreCase) - { - "OneBox", - "DAILY", - "Int3", - "PpeCloud", // edog - "DXT" - }; - } + private readonly static HashSet s_microsoftInternalClouds = new(StringComparer.OrdinalIgnoreCase) + { + "OneBox", + "DAILY", + "Int3", + "PpeCloud", // edog + "DXT" + }; } diff --git a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudEnvironmentResponseContract.cs b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudEnvironmentResponseContract.cs index 07dea3da..68b8fb83 100644 --- a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudEnvironmentResponseContract.cs +++ b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudEnvironmentResponseContract.cs @@ -1,13 +1,12 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts -{ - using System.Text.Json.Serialization; +using System.Text.Json.Serialization; + +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; - // Sample response from the discover API: - // Invoke-RestMethod -Method POST -Uri "https://api.powerbi.com/powerbi/globalservice/v202003/environments/discover?client=powerbi-msolap" | ConvertTo-Json -Depth 10 +// Sample response from the discover API: +// Invoke-RestMethod -Method POST -Uri "https://api.powerbi.com/powerbi/globalservice/v202003/environments/discover?client=powerbi-msolap" | ConvertTo-Json -Depth 10 - internal sealed class CloudEnvironmentResponseContract - { - [JsonPropertyName("environments")] - public CloudEnvironmentContract[] Environments { get; set; } = null!; - } +internal sealed class CloudEnvironmentResponseContract +{ + [JsonPropertyName("environments")] + public CloudEnvironmentContract[] Environments { get; set; } = null!; } diff --git a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudEnvironmentServiceContract.cs b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudEnvironmentServiceContract.cs index 42bb7f56..53e383f3 100644 --- a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudEnvironmentServiceContract.cs +++ b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudEnvironmentServiceContract.cs @@ -1,29 +1,30 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts -{ - using System.Text.Json.Serialization; +using System; +using System.Diagnostics; +using System.Text.Json.Serialization; - [DebuggerDisplay("{Name}")] - internal sealed class CloudEnvironmentServiceContract - { - [JsonPropertyName("name")] - public string Name { get; set; } = null!; +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; - [JsonPropertyName("endpoint")] - public string Endpoint { get; set; } = null!; +[DebuggerDisplay("{Name}")] +internal sealed class CloudEnvironmentServiceContract +{ + [JsonPropertyName("name")] + public string Name { get; set; } = null!; - [JsonPropertyName("resourceId")] - public string ResourceId { get; set; } = null!; + [JsonPropertyName("endpoint")] + public string Endpoint { get; set; } = null!; - //[JsonPropertyName("allowedDomains")] - //public string[] AllowedDomains { get; set; } = null!; - } + [JsonPropertyName("resourceId")] + public string ResourceId { get; set; } = null!; - internal static class CloudEnvironmentServiceContractExtension - { - public static bool IsAad(this CloudEnvironmentServiceContract service) - => service.Name.Equals("aad", StringComparison.Ordinal); + //[JsonPropertyName("allowedDomains")] + //public string[] AllowedDomains { get; set; } = null!; +} + +internal static class CloudEnvironmentServiceContractExtension +{ + public static bool IsAad(this CloudEnvironmentServiceContract service) + => service.Name.Equals("aad", StringComparison.Ordinal); - public static bool IsPowerBIBackend(this CloudEnvironmentServiceContract service) - => service.Name.Equals("powerbi-backend", StringComparison.Ordinal); - } + public static bool IsPowerBIBackend(this CloudEnvironmentServiceContract service) + => service.Name.Equals("powerbi-backend", StringComparison.Ordinal); } diff --git a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudModel.cs b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudModel.cs index a094bdeb..233bfa7d 100644 --- a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudModel.cs +++ b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudModel.cs @@ -1,94 +1,93 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts -{ - using System; - using System.Text.Json.Serialization; +using System; +using System.Text.Json.Serialization; + +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; - public sealed class CloudModel - { - private const int PBIXProviderId = 7; +public sealed class CloudModel +{ + private const int PBIXProviderId = 7; - private const string ExcelModelResource = "ExcelModelResource"; + private const string ExcelModelResource = "ExcelModelResource"; - [JsonPropertyName("id")] - public long Id { get; set; } + [JsonPropertyName("id")] + public long Id { get; set; } - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } - [JsonPropertyName("description")] - public string? Description { get; set; } + [JsonPropertyName("description")] + public string? Description { get; set; } - [JsonPropertyName("dbName")] - public string? DBName { get; set; } + [JsonPropertyName("dbName")] + public string? DBName { get; set; } - [JsonPropertyName("vsName")] - public string? VSName { get; set; } + [JsonPropertyName("vsName")] + public string? VSName { get; set; } - [JsonPropertyName("permissions")] - public CloudPermissions Permissions { get; set; } + [JsonPropertyName("permissions")] + public CloudPermissions Permissions { get; set; } - [JsonPropertyName("resourceName")] - public string? ResourceName { get; set; } + [JsonPropertyName("resourceName")] + public string? ResourceName { get; set; } - [JsonPropertyName("nextRefreshTime")] - public DateTime NextRefreshTime { get; set; } + [JsonPropertyName("nextRefreshTime")] + public DateTime NextRefreshTime { get; set; } - [JsonPropertyName("LastRefreshTime")] - public DateTime LastRefreshTime { get; set; } + [JsonPropertyName("LastRefreshTime")] + public DateTime LastRefreshTime { get; set; } - //Ignored property - this is the old .NET JavaScriptSerializer/DataContractJsonSerializer date format e.g. /Date(1617810742483)/ - //[JsonPropertyName("lastRefreshTime")] - //public string lastRefreshTime { get; set; } + //Ignored property - this is the old .NET JavaScriptSerializer/DataContractJsonSerializer date format e.g. /Date(1617810742483)/ + //[JsonPropertyName("lastRefreshTime")] + //public string lastRefreshTime { get; set; } - [JsonPropertyName("creatorUser")] - public CloudUser? CreatorUser { get; set; } + [JsonPropertyName("creatorUser")] + public CloudUser? CreatorUser { get; set; } - [JsonPropertyName("insightsSupported")] - public bool InsightsSupported { get; set; } + [JsonPropertyName("insightsSupported")] + public bool InsightsSupported { get; set; } - [JsonPropertyName("cloudRlsEnabled")] - public bool CloudRLSEnabled { get; set; } + [JsonPropertyName("cloudRlsEnabled")] + public bool CloudRLSEnabled { get; set; } - [JsonPropertyName("onPremModelConnectionString")] - public string? OnPremModelConnectionString { get; set; } + [JsonPropertyName("onPremModelConnectionString")] + public string? OnPremModelConnectionString { get; set; } - [JsonPropertyName("directQueryMode")] - public bool DirectQueryMode { get; set; } + [JsonPropertyName("directQueryMode")] + public bool DirectQueryMode { get; set; } - [JsonPropertyName("pushDataVersion")] - public int PushDataVersion { get; set; } + [JsonPropertyName("pushDataVersion")] + public int PushDataVersion { get; set; } - [JsonPropertyName("realTimeMode")] - public int RealTimeMode { get; set; } + [JsonPropertyName("realTimeMode")] + public int RealTimeMode { get; set; } - [JsonPropertyName("contentProviderId")] - public int ContentProviderId { get; set; } + [JsonPropertyName("contentProviderId")] + public int ContentProviderId { get; set; } - [JsonPropertyName("originalModelId")] - public long? OriginalModelId { get; set; } + [JsonPropertyName("originalModelId")] + public long? OriginalModelId { get; set; } - [JsonPropertyName("isHidden")] - public bool IsHidden { get; set; } + [JsonPropertyName("isHidden")] + public bool IsHidden { get; set; } - [JsonPropertyName("__isCloudModel")] - public bool IsCloudModel => !DirectQueryMode && !IsPushDataEnabled && string.IsNullOrEmpty(OnPremModelConnectionString); + [JsonPropertyName("__isCloudModel")] + public bool IsCloudModel => !DirectQueryMode && !IsPushDataEnabled && string.IsNullOrEmpty(OnPremModelConnectionString); - [JsonPropertyName("__isOnPremModel")] - public bool IsOnPremModel => !string.IsNullOrEmpty(OnPremModelConnectionString); + [JsonPropertyName("__isOnPremModel")] + public bool IsOnPremModel => !string.IsNullOrEmpty(OnPremModelConnectionString); - [JsonPropertyName("__isExcelWorkbook")] - public bool IsExcelWorkbook => ExcelModelResource.Equals(ResourceName, StringComparison.OrdinalIgnoreCase); + [JsonPropertyName("__isExcelWorkbook")] + public bool IsExcelWorkbook => ExcelModelResource.Equals(ResourceName, StringComparison.OrdinalIgnoreCase); - [JsonPropertyName("__isWritablePbixModel")] - public bool IsWritablePbixModel => IsWriteableModel && ContentProviderId == PBIXProviderId; + [JsonPropertyName("__isWritablePbixModel")] + public bool IsWritablePbixModel => IsWriteableModel && ContentProviderId == PBIXProviderId; - [JsonPropertyName("__isWriteableModel")] - public bool IsWriteableModel => Permissions.HasFlag(CloudPermissions.Write); + [JsonPropertyName("__isWriteableModel")] + public bool IsWriteableModel => Permissions.HasFlag(CloudPermissions.Write); - [JsonPropertyName("__isPushDataEnabled")] - public bool IsPushDataEnabled => PushDataVersion != 0; + [JsonPropertyName("__isPushDataEnabled")] + public bool IsPushDataEnabled => PushDataVersion != 0; - [JsonPropertyName("__isPushStreaming")] - public bool IsPushStreaming => IsPushDataEnabled && RealTimeMode != 0; - } + [JsonPropertyName("__isPushStreaming")] + public bool IsPushStreaming => IsPushDataEnabled && RealTimeMode != 0; } diff --git a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudOrganizationalGalleryItem.cs b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudOrganizationalGalleryItem.cs index 30ff153a..0eb599d1 100644 --- a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudOrganizationalGalleryItem.cs +++ b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudOrganizationalGalleryItem.cs @@ -1,62 +1,61 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts -{ - using System; - using System.Text.Json.Serialization; +using System; +using System.Text.Json.Serialization; + +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; - public class CloudOrganizationalGalleryItem - { - [JsonPropertyName("id")] - public int Id { get; set; } +public class CloudOrganizationalGalleryItem +{ + [JsonPropertyName("id")] + public int Id { get; set; } - [JsonPropertyName("displayName")] - public string? DisplayName { get; set; } + [JsonPropertyName("displayName")] + public string? DisplayName { get; set; } - [JsonPropertyName("description")] - public string? Description { get; set; } + [JsonPropertyName("description")] + public string? Description { get; set; } - [JsonPropertyName("status")] - public CloudOrganizationalGalleryItemStatus Status { get; set; } + [JsonPropertyName("status")] + public CloudOrganizationalGalleryItemStatus Status { get; set; } - [JsonPropertyName("stage")] - public CloudPromotionalStage Stage { get; set; } + [JsonPropertyName("stage")] + public CloudPromotionalStage Stage { get; set; } - [JsonPropertyName("iconUrl")] - public string? IconUrl { get; set; } + [JsonPropertyName("iconUrl")] + public string? IconUrl { get; set; } - [JsonPropertyName("publishTime")] - public DateTime PublishTime { get; set; } + [JsonPropertyName("publishTime")] + public DateTime PublishTime { get; set; } - [JsonPropertyName("config")] - public string? Config { get; set; } + [JsonPropertyName("config")] + public string? Config { get; set; } - [JsonPropertyName("ownerGivenName")] - public string? OwnerGivenName { get; set; } + [JsonPropertyName("ownerGivenName")] + public string? OwnerGivenName { get; set; } - [JsonPropertyName("ownerFamilyName")] - public string? OwnerFamilyName { get; set; } + [JsonPropertyName("ownerFamilyName")] + public string? OwnerFamilyName { get; set; } - [JsonPropertyName("ownerEmailAddress")] - public string? OwnerEmailAddress { get; set; } + [JsonPropertyName("ownerEmailAddress")] + public string? OwnerEmailAddress { get; set; } - [JsonPropertyName("resourcePackageId")] - public int ResourcePackageId { get; set; } + [JsonPropertyName("resourcePackageId")] + public int ResourcePackageId { get; set; } - [JsonPropertyName("objectId")] - public Guid ObjectId { get; set; } + [JsonPropertyName("objectId")] + public Guid ObjectId { get; set; } - [JsonPropertyName("disabled")] - public bool? Disabled { get; set; } + [JsonPropertyName("disabled")] + public bool? Disabled { get; set; } - [JsonPropertyName("certifyingUser")] - public CloudUser? CertifyingUser { get; set; } + [JsonPropertyName("certifyingUser")] + public CloudUser? CertifyingUser { get; set; } - [JsonPropertyName("certificationTime")] - public DateTime? CertificationTime { get; set; } + [JsonPropertyName("certificationTime")] + public DateTime? CertificationTime { get; set; } - [JsonPropertyName("isOutOfBox")] - public bool? IsOutOfBox { get; set; } + [JsonPropertyName("isOutOfBox")] + public bool? IsOutOfBox { get; set; } - [JsonPropertyName("name")] - public string? Name { get; set; } - } -} \ No newline at end of file + [JsonPropertyName("name")] + public string? Name { get; set; } +} diff --git a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudOrganizationalGalleryItemStatus.cs b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudOrganizationalGalleryItemStatus.cs index b140c551..84108538 100644 --- a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudOrganizationalGalleryItemStatus.cs +++ b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudOrganizationalGalleryItemStatus.cs @@ -1,8 +1,7 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; + +public enum CloudOrganizationalGalleryItemStatus { - public enum CloudOrganizationalGalleryItemStatus - { - Enabled, - Disabled - } + Enabled, + Disabled } diff --git a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudPermissions.cs b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudPermissions.cs index 31a676af..0bb4839f 100644 --- a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudPermissions.cs +++ b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudPermissions.cs @@ -1,14 +1,13 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts -{ - using System; +using System; + +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; - [Flags] - public enum CloudPermissions - { - None = 0, - Read = 1, - Write = 2, - ReShared = 4, - Explore = 8 - } -} \ No newline at end of file +[Flags] +public enum CloudPermissions +{ + None = 0, + Read = 1, + Write = 2, + ReShared = 4, + Explore = 8 +} diff --git a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudPromotionalStage.cs b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudPromotionalStage.cs index 3c19de70..da1e72da 100644 --- a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudPromotionalStage.cs +++ b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudPromotionalStage.cs @@ -1,11 +1,10 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; + +public enum CloudPromotionalStage { - public enum CloudPromotionalStage - { - None, - Promoted, - Certified, - Master, - Recommended - } + None, + Promoted, + Certified, + Master, + Recommended } \ No newline at end of file diff --git a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudSharedModel.cs b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudSharedModel.cs index c88447fe..19a5575f 100644 --- a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudSharedModel.cs +++ b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudSharedModel.cs @@ -1,58 +1,57 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts -{ - using System; - using System.Text.Json.Serialization; +using System; +using System.Text.Json.Serialization; - public sealed class CloudSharedModel - { - [JsonPropertyName("modelId")] - public long ModelId { get; set; } +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; - [JsonPropertyName("workspaceId")] - public long WorkspaceId { get; set; } +public sealed class CloudSharedModel +{ + [JsonPropertyName("modelId")] + public long ModelId { get; set; } - [JsonPropertyName("workspaceObjectId")] - public string? WorkspaceObjectId { get; set; } + [JsonPropertyName("workspaceId")] + public long WorkspaceId { get; set; } - [JsonPropertyName("workspaceName")] - public string? WorkspaceName { get; set; } + [JsonPropertyName("workspaceObjectId")] + public string? WorkspaceObjectId { get; set; } - [JsonPropertyName("workspaceType")] - public CloudSharedModelWorkspaceType WorkspaceType { get; set; } + [JsonPropertyName("workspaceName")] + public string? WorkspaceName { get; set; } - [JsonPropertyName("permissions")] - public CloudPermissions Permissions { get; set; } + [JsonPropertyName("workspaceType")] + public CloudSharedModelWorkspaceType WorkspaceType { get; set; } - [JsonPropertyName("model")] - public CloudModel? Model { get; set; } + [JsonPropertyName("permissions")] + public CloudPermissions Permissions { get; set; } - [JsonPropertyName("galleryItem")] - public CloudOrganizationalGalleryItem? GalleryItem { get; set; } + [JsonPropertyName("model")] + public CloudModel? Model { get; set; } - //[JsonPropertyName("artifactInformationProtection")] - //public CloudArtifactInformationProtection ArtifactInformationProtection { get; set; } + [JsonPropertyName("galleryItem")] + public CloudOrganizationalGalleryItem? GalleryItem { get; set; } - [JsonPropertyName("snapshotId")] - public long? SnapshotId { get; set; } + //[JsonPropertyName("artifactInformationProtection")] + //public CloudArtifactInformationProtection ArtifactInformationProtection { get; set; } - [JsonPropertyName("lastVisitedTimeUTC")] - public DateTime? LastVisitedTimeUTC { get; set; } + [JsonPropertyName("snapshotId")] + public long? SnapshotId { get; set; } - [JsonPropertyName("__objectId")] - public string? ObjectId + [JsonPropertyName("lastVisitedTimeUTC")] + public DateTime? LastVisitedTimeUTC { get; set; } + + [JsonPropertyName("__objectId")] + public string? ObjectId + { + get { - get + if (IsOnPersonalWorkspace) { - if (IsOnPersonalWorkspace) - { - return CloudWorkspace.PersonalWorkspaceId; - } - - return WorkspaceObjectId; + return CloudWorkspace.PersonalWorkspaceId; } - } - [JsonPropertyName("__isOnPersonalWorkspace")] - public bool IsOnPersonalWorkspace => WorkspaceType == CloudSharedModelWorkspaceType.PersonalGroup; + return WorkspaceObjectId; + } } + + [JsonPropertyName("__isOnPersonalWorkspace")] + public bool IsOnPersonalWorkspace => WorkspaceType == CloudSharedModelWorkspaceType.PersonalGroup; } diff --git a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudSharedModelWorkspaceType.cs b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudSharedModelWorkspaceType.cs index 55cae42d..0dd16f5e 100644 --- a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudSharedModelWorkspaceType.cs +++ b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudSharedModelWorkspaceType.cs @@ -1,25 +1,24 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; + +public enum CloudSharedModelWorkspaceType { - public enum CloudSharedModelWorkspaceType - { - /// - /// ??? - /// - Personal = 0, + /// + /// ??? + /// + Personal = 0, - /// - /// A modern workspace (the 'new' decoupled Workspace experience) - /// - Workspace = 1, + /// + /// A modern workspace (the 'new' decoupled Workspace experience) + /// + Workspace = 1, - /// - /// A legacy workspace based on Office 365 groups (the 'old' Workspace experience where Workspaces are tightly coupled with O365 groups) - /// - Group = 2, + /// + /// A legacy workspace based on Office 365 groups (the 'old' Workspace experience where Workspaces are tightly coupled with O365 groups) + /// + Group = 2, - /// - /// Personal workspace of a Power BI user, a.k.a. "My Workspace" - /// - PersonalGroup = 3 - } + /// + /// Personal workspace of a Power BI user, a.k.a. "My Workspace" + /// + PersonalGroup = 3 } diff --git a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudUser.cs b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudUser.cs index 51392a52..26da78f4 100644 --- a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudUser.cs +++ b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudUser.cs @@ -1,19 +1,18 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts -{ - using System.Text.Json.Serialization; +using System.Text.Json.Serialization; + +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; - public sealed class CloudUser - { - [JsonPropertyName("id")] - public long Id { get; set; } +public sealed class CloudUser +{ + [JsonPropertyName("id")] + public long Id { get; set; } - [JsonPropertyName("givenName")] - public string? GivenName { get; set; } + [JsonPropertyName("givenName")] + public string? GivenName { get; set; } - [JsonPropertyName("familyName")] - public string? FamilyName { get; set; } + [JsonPropertyName("familyName")] + public string? FamilyName { get; set; } - [JsonPropertyName("emailAddress")] - public string? EmailAddress { get; set; } - } + [JsonPropertyName("emailAddress")] + public string? EmailAddress { get; set; } } diff --git a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudWorkspace.cs b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudWorkspace.cs index de7e9996..db3f0821 100644 --- a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudWorkspace.cs +++ b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudWorkspace.cs @@ -1,82 +1,81 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts -{ - using System; - using System.Text.Json.Serialization; +using System; +using System.Text.Json.Serialization; - public sealed class CloudWorkspace - { - internal static readonly string PersonalWorkspaceId = Guid.Empty.ToString(); +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; - [JsonPropertyName("id")] - public string? Id { get; set; } +public sealed class CloudWorkspace +{ + internal static readonly string PersonalWorkspaceId = Guid.Empty.ToString(); - [JsonPropertyName("name")] - public string? Name { get; set; } + [JsonPropertyName("id")] + public string? Id { get; set; } - [JsonPropertyName("type")] - public string? Type { get; set; } + [JsonPropertyName("name")] + public string? Name { get; set; } - [JsonPropertyName("capacitySku")] - public string? CapacitySku { get; set; } + [JsonPropertyName("type")] + public string? Type { get; set; } - [JsonPropertyName("capacityObjectId")] - public string? CapacityObjectId { get; set; } + [JsonPropertyName("capacitySku")] + public string? CapacitySku { get; set; } - [JsonPropertyName("capacityUri")] - public string? CapacityUri { get; set; } + [JsonPropertyName("capacityObjectId")] + public string? CapacityObjectId { get; set; } - [JsonPropertyName("isSharedOnPremium")] - public bool IsSharedOnPremium { get; set; } + [JsonPropertyName("capacityUri")] + public string? CapacityUri { get; set; } - [JsonPropertyName("__objectId")] - public string? ObjectId + [JsonPropertyName("isSharedOnPremium")] + public bool IsSharedOnPremium { get; set; } + + [JsonPropertyName("__objectId")] + public string? ObjectId + { + get { - get + if (IsPersonalWorkspace) { - if (IsPersonalWorkspace) - { - return PersonalWorkspaceId; - } - - return Id; + return PersonalWorkspaceId; } + + return Id; } + } - [JsonPropertyName("__isIsLegacyV1Workspace")] - public bool IsLegacyV1Workspace => WorkspaceType == CloudWorkspaceType.Group; + [JsonPropertyName("__isIsLegacyV1Workspace")] + public bool IsLegacyV1Workspace => WorkspaceType == CloudWorkspaceType.Group; - [JsonPropertyName("__isPersonalWorkspace")] - public bool IsPersonalWorkspace + [JsonPropertyName("__isPersonalWorkspace")] + public bool IsPersonalWorkspace + { + get { - get + if (string.IsNullOrEmpty(Name)) { - if (string.IsNullOrEmpty(Name)) - { - return WorkspaceType == CloudWorkspaceType.User; - } - - return false; + return WorkspaceType == CloudWorkspaceType.User; } + + return false; } + } - [JsonPropertyName("__isPremiumCapacity")] - public bool IsPremiumCapacity + [JsonPropertyName("__isPremiumCapacity")] + public bool IsPremiumCapacity + { + get { - get + if (CapacitySkuType == CloudWorkspaceCapacitySkuType.Premium) { - if (CapacitySkuType == CloudWorkspaceCapacitySkuType.Premium) - { - return !IsSharedOnPremium; - } - - return false; + return !IsSharedOnPremium; } + + return false; } + } - [JsonPropertyName("__workspaceType")] - public CloudWorkspaceType WorkspaceType => (CloudWorkspaceType)Enum.Parse(typeof(CloudWorkspaceType), Type!); // here we don't expect null, but in case we let the ArgumentNullException arise + [JsonPropertyName("__workspaceType")] + public CloudWorkspaceType WorkspaceType => (CloudWorkspaceType)Enum.Parse(typeof(CloudWorkspaceType), Type!); // here we don't expect null, but in case we let the ArgumentNullException arise - [JsonPropertyName("__capacitySkuType")] - public CloudWorkspaceCapacitySkuType CapacitySkuType => (CloudWorkspaceCapacitySkuType)Enum.Parse(typeof(CloudWorkspaceCapacitySkuType), CapacitySku!); // here we don't expect null, but in case we let the ArgumentNullException arise - } + [JsonPropertyName("__capacitySkuType")] + public CloudWorkspaceCapacitySkuType CapacitySkuType => (CloudWorkspaceCapacitySkuType)Enum.Parse(typeof(CloudWorkspaceCapacitySkuType), CapacitySku!); // here we don't expect null, but in case we let the ArgumentNullException arise } diff --git a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudWorkspaceCapacitySkuType.cs b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudWorkspaceCapacitySkuType.cs index 95733aef..bea08684 100644 --- a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudWorkspaceCapacitySkuType.cs +++ b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudWorkspaceCapacitySkuType.cs @@ -1,17 +1,16 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; + +public enum CloudWorkspaceCapacitySkuType { - public enum CloudWorkspaceCapacitySkuType - { - Unknown = 0, + Unknown = 0, - /// - /// PremiumCapacitySku - /// - Premium, + /// + /// PremiumCapacitySku + /// + Premium, - /// - /// SharedCapacitySku - /// - Shared, - } + /// + /// SharedCapacitySku + /// + Shared, } diff --git a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudWorkspaceType.cs b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudWorkspaceType.cs index d85264e4..ed870cc9 100644 --- a/src/Infrastructure/PowerBI/Cloud/Contracts/CloudWorkspaceType.cs +++ b/src/Infrastructure/PowerBI/Cloud/Contracts/CloudWorkspaceType.cs @@ -1,22 +1,21 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; + +public enum CloudWorkspaceType { - public enum CloudWorkspaceType - { - Unknown = 0, + Unknown = 0, - /// - /// PersonalWorkspaceType - /// - User, + /// + /// PersonalWorkspaceType + /// + User, - /// - /// GroupWorkspaceType - /// - Group, + /// + /// GroupWorkspaceType + /// + Group, - /// - /// FolderWorkspaceType - /// - Folder, - } + /// + /// FolderWorkspaceType + /// + Folder, } diff --git a/src/Infrastructure/PowerBI/Cloud/Contracts/TenantClusterContract.cs b/src/Infrastructure/PowerBI/Cloud/Contracts/TenantClusterContract.cs index 44031d9c..65fc3d69 100644 --- a/src/Infrastructure/PowerBI/Cloud/Contracts/TenantClusterContract.cs +++ b/src/Infrastructure/PowerBI/Cloud/Contracts/TenantClusterContract.cs @@ -1,20 +1,19 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts -{ - using System.Text.Json.Serialization; +using System.Text.Json.Serialization; + +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; - internal sealed class TenantClusterContract - { - [JsonPropertyName("FixedClusterUri")] - public string FixedClusterUri { get; set; } = null!; +internal sealed class TenantClusterContract +{ + [JsonPropertyName("FixedClusterUri")] + public string FixedClusterUri { get; set; } = null!; - //public string? PrivateLinkFixedClusterUri { get; set; } + //public string? PrivateLinkFixedClusterUri { get; set; } - //public string? NewTenantId { get; set; } + //public string? NewTenantId { get; set; } - //public string? RuleDescription { get; set; } + //public string? RuleDescription { get; set; } - //public int? TTLSeconds { get; set; } + //public int? TTLSeconds { get; set; } - //public string? TenantId { get; set; } - } -} \ No newline at end of file + //public string? TenantId { get; set; } +} diff --git a/src/Infrastructure/PowerBI/Cloud/Serialization/CloudContractJsonSerializer.cs b/src/Infrastructure/PowerBI/Cloud/Serialization/CloudContractJsonSerializer.cs index bb1df6d5..f11cd9a8 100644 --- a/src/Infrastructure/PowerBI/Cloud/Serialization/CloudContractJsonSerializer.cs +++ b/src/Infrastructure/PowerBI/Cloud/Serialization/CloudContractJsonSerializer.cs @@ -1,16 +1,18 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Serialization +using System; +using System.Text.Json; + +namespace Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Serialization; + +internal static class CloudContractJsonSerializer { - internal static class CloudContractJsonSerializer - { - private readonly static JsonSerializerOptions s_options - = new(JsonSerializerDefaults.Web); + private readonly static JsonSerializerOptions s_options + = new(JsonSerializerDefaults.Web); - public static T Deserialize(string json) where T : class - { - return JsonSerializer.Deserialize(json, s_options) - ?? throw new InvalidOperationException($"The JSON content deserialized to a null '{typeof(T)}' instance."); - } - public static string Serialize(T value) where T : class - => JsonSerializer.Serialize(value, s_options); + public static T Deserialize(string json) where T : class + { + return JsonSerializer.Deserialize(json, s_options) + ?? throw new InvalidOperationException($"The JSON content deserialized to a null '{typeof(T)}' instance."); } + public static string Serialize(T value) where T : class + => JsonSerializer.Serialize(value, s_options); } diff --git a/src/Infrastructure/PowerBI/LocalConfigurationReader.cs b/src/Infrastructure/PowerBI/LocalConfigurationReader.cs index 1be0f7f1..03c642ab 100644 --- a/src/Infrastructure/PowerBI/LocalConfigurationReader.cs +++ b/src/Infrastructure/PowerBI/LocalConfigurationReader.cs @@ -1,64 +1,64 @@ -namespace Sqlbi.Bravo.Infrastructure.PowerBI +using System; +using Microsoft.Win32; +using Sqlbi.Bravo.Infrastructure.Extensions; + +namespace Sqlbi.Bravo.Infrastructure.PowerBI; + +internal interface ILocalConfigurationReader { - using Microsoft.Win32; - using Sqlbi.Bravo.Infrastructure.Extensions; + Uri? GetPowerBIServiceDiscoveryBaseUri(); + Uri? GetPowerBIServiceFixedClusterUri(); +} - internal interface ILocalConfigurationReader - { - Uri? GetPowerBIServiceDiscoveryBaseUri(); - Uri? GetPowerBIServiceFixedClusterUri(); - } +internal sealed class LocalConfigurationReader : ILocalConfigurationReader +{ + private const string PowerBIDiscoveryUrlValueName = "PowerBIDiscoveryUrl"; + private const string PowerBISubkeyName = @"SOFTWARE\Microsoft\Microsoft Power BI\"; + private const string PowerBIPolicySubkeyName = @"SOFTWARE\Policies\Microsoft\Microsoft Power BI\"; - internal sealed class LocalConfigurationReader : ILocalConfigurationReader + /// + /// Gets the Power BI service discovery base URI from the local machine registry. + /// + /// See https://github.com/microsoft/Federal-Business-Applications/tree/main/whitepapers/power-bi-registry-settings + /// and https://docs.microsoft.com/en-us/power-bi/enterprise/service-govus-overview#sign-in-to-power-bi-for-us-government + /// + /// + public Uri? GetPowerBIServiceDiscoveryBaseUri() { - private const string PowerBIDiscoveryUrlValueName = "PowerBIDiscoveryUrl"; - private const string PowerBISubkeyName = @"SOFTWARE\Microsoft\Microsoft Power BI\"; - private const string PowerBIPolicySubkeyName = @"SOFTWARE\Policies\Microsoft\Microsoft Power BI\"; + var valueName = PowerBIDiscoveryUrlValueName; - /// - /// Gets the Power BI service discovery base URI from the local machine registry. - /// - /// See https://github.com/microsoft/Federal-Business-Applications/tree/main/whitepapers/power-bi-registry-settings - /// and https://docs.microsoft.com/en-us/power-bi/enterprise/service-govus-overview#sign-in-to-power-bi-for-us-government - /// - /// - public Uri? GetPowerBIServiceDiscoveryBaseUri() - { - var valueName = PowerBIDiscoveryUrlValueName; + // The value may have been written to either the 64-bit or the 32-bit (WOW6432Node) registry view, + // depending on the bitness of the Power BI Desktop build that set it. Check both views explicitly so + // the result does not depend on the bitness of this (Bravo) process. + var value = GetRegistryString(valueName, RegistryView.Registry64); + if (value is null) + value = GetRegistryString(valueName, RegistryView.Registry32); - // The value may have been written to either the 64-bit or the 32-bit (WOW6432Node) registry view, - // depending on the bitness of the Power BI Desktop build that set it. Check both views explicitly so - // the result does not depend on the bitness of this (Bravo) process. - var value = GetRegistryString(valueName, RegistryView.Registry64); - if (value is null) - value = GetRegistryString(valueName, RegistryView.Registry32); - - return value is null ? null : new Uri(value, UriKind.Absolute); - } + return value is null ? null : new Uri(value, UriKind.Absolute); + } - public Uri? GetPowerBIServiceFixedClusterUri() - { - // `PowerBIServiceUrl` is not a documented registry key, but it is used by the - // PBI Desktop to override the default service URL for fixed cluster scenarios. + public Uri? GetPowerBIServiceFixedClusterUri() + { + // `PowerBIServiceUrl` is not a documented registry key, but it is used by the + // PBI Desktop to override the default service URL for fixed cluster scenarios. - throw new NotImplementedException(); - } + throw new NotImplementedException(); + } - private static string? GetRegistryString(string valueName, RegistryView view) - { - using var key = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view); + private static string? GetRegistryString(string valueName, RegistryView view) + { + using var key = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view); - // Try the policy override first - var value = key.GetStringValue(PowerBIPolicySubkeyName, valueName); - if (!string.IsNullOrEmpty(value)) - return value; + // Try the policy override first + var value = key.GetStringValue(PowerBIPolicySubkeyName, valueName); + if (!string.IsNullOrEmpty(value)) + return value; - // Then try the standard subkey - value = key.GetStringValue(PowerBISubkeyName, valueName); - if (!string.IsNullOrEmpty(value)) - return value; + // Then try the standard subkey + value = key.GetStringValue(PowerBISubkeyName, valueName); + if (!string.IsNullOrEmpty(value)) + return value; - return null; - } + return null; } } diff --git a/src/Infrastructure/PowerBI/ServiceCollectionExtensions.cs b/src/Infrastructure/PowerBI/ServiceCollectionExtensions.cs index 113ef84d..5db26884 100644 --- a/src/Infrastructure/PowerBI/ServiceCollectionExtensions.cs +++ b/src/Infrastructure/PowerBI/ServiceCollectionExtensions.cs @@ -1,30 +1,30 @@ +using System; using Microsoft.Extensions.DependencyInjection; using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Authentication; using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Configuration; -namespace Sqlbi.Bravo.Infrastructure.PowerBI +namespace Sqlbi.Bravo.Infrastructure.PowerBI; + +internal static class ServiceCollectionExtensions { - internal static class ServiceCollectionExtensions - { - internal const string PowerBIApiHttpClientName = "PowerBIApi"; + internal const string PowerBIApiHttpClientName = "PowerBIApi"; - public static IServiceCollection AddPowerBI(this IServiceCollection services) + public static IServiceCollection AddPowerBI(this IServiceCollection services) + { + services.AddHttpClient(PowerBIApiHttpClientName, (client) => { - services.AddHttpClient(PowerBIApiHttpClientName, (client) => - { - client.DefaultRequestHeaders.Accept.Clear(); // No default Accept header required - client.Timeout = TimeSpan.FromMinutes(3); - }); + client.DefaultRequestHeaders.Accept.Clear(); // No default Accept header required + client.Timeout = TimeSpan.FromMinutes(3); + }); - services.AddSingleton(); + services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); - return services; - } + return services; } } diff --git a/src/Infrastructure/Security/CredentialDialog.cs b/src/Infrastructure/Security/CredentialDialog.cs index 49dbd19c..28f4f2c9 100644 --- a/src/Infrastructure/Security/CredentialDialog.cs +++ b/src/Infrastructure/Security/CredentialDialog.cs @@ -1,143 +1,142 @@ -namespace Sqlbi.Bravo.Infrastructure.Security +using Sqlbi.Bravo.Infrastructure.Windows.Interop; +using System; +using System.ComponentModel; +using System.Net; +using System.Runtime.InteropServices; +using System.Text; + +namespace Sqlbi.Bravo.Infrastructure.Security; + +internal static class CredentialDialog { - using Sqlbi.Bravo.Infrastructure.Windows.Interop; - using System; - using System.ComponentModel; - using System.Net; - using System.Runtime.InteropServices; - using System.Text; - - internal static class CredentialDialog + public static NetworkCredential? PromptForCredentials(CredentialDialogOptions options) { - public static NetworkCredential? PromptForCredentials(CredentialDialogOptions options) + var uiInfo = new Credui.CREDUI_INFO { - var uiInfo = new Credui.CREDUI_INFO - { - hwndParent = options.HwndParent, - pszMessageText = options.Message, - pszCaptionText = options.Caption, - //hbmBanner = options.HbmBanner, - }; - uiInfo.cbSize = Marshal.SizeOf(uiInfo); - - var authPackage = 0; - var inAuthBuffer = IntPtr.Zero; - var inAuthBufferSize = 0; - var flags = options.Flags; - var save = options.IsSaveChecked; - var authError = options.AuthError; - - var retval = Credui.CredUIPromptForWindowsCredentialsW(ref uiInfo, authError, ref authPackage, inAuthBuffer, inAuthBufferSize, out var outAuthBuffer, out var outAuthBufferSize, ref save, flags); - try + hwndParent = options.HwndParent, + pszMessageText = options.Message, + pszCaptionText = options.Caption, + //hbmBanner = options.HbmBanner, + }; + uiInfo.cbSize = Marshal.SizeOf(uiInfo); + + var authPackage = 0; + var inAuthBuffer = IntPtr.Zero; + var inAuthBufferSize = 0; + var flags = options.Flags; + var save = options.IsSaveChecked; + var authError = options.AuthError; + + var retval = Credui.CredUIPromptForWindowsCredentialsW(ref uiInfo, authError, ref authPackage, inAuthBuffer, inAuthBufferSize, out var outAuthBuffer, out var outAuthBufferSize, ref save, flags); + try + { + switch (retval) { - switch (retval) - { - case NativeMethods.ERROR_SUCCESS: - { - var networkCredential = CredUnPackNetworkCredential(outAuthBuffer, outAuthBufferSize); - return networkCredential; - } - case NativeMethods.ERROR_CANCELLED: - return null; - } - - throw new Win32Exception(retval); + case NativeMethods.ERROR_SUCCESS: + { + var networkCredential = CredUnPackNetworkCredential(outAuthBuffer, outAuthBufferSize); + return networkCredential; + } + case NativeMethods.ERROR_CANCELLED: + return null; } - finally + + throw new Win32Exception(retval); + } + finally + { + if (outAuthBuffer != IntPtr.Zero) { - if (outAuthBuffer != IntPtr.Zero) - { - // Make sure buffer is zeroed out. TODO: where is SecureZeroMem ?? - var zeroedBuffer = new byte[outAuthBufferSize]; - Marshal.Copy(zeroedBuffer, 0, outAuthBuffer, outAuthBufferSize); - } - - Marshal.FreeCoTaskMem(inAuthBuffer); - Marshal.FreeCoTaskMem(outAuthBuffer); + // Make sure buffer is zeroed out. TODO: where is SecureZeroMem ?? + var zeroedBuffer = new byte[outAuthBufferSize]; + Marshal.Copy(zeroedBuffer, 0, outAuthBuffer, outAuthBufferSize); } + + Marshal.FreeCoTaskMem(inAuthBuffer); + Marshal.FreeCoTaskMem(outAuthBuffer); } + } - private static NetworkCredential CredUnPackNetworkCredential(IntPtr authBufferPtr, int authBufferSize) + private static NetworkCredential CredUnPackNetworkCredential(IntPtr authBufferPtr, int authBufferSize) + { + var domainBuffer = new StringBuilder(Credui.CREDUI_MAX_DOMAIN_LENGTH); + var userNameBuffer = new StringBuilder(Credui.CREDUI_MAX_USERNAME_LENGTH); + var passwordBuffer = new StringBuilder(Credui.CREDUI_MAX_PASSWORD_LENGTH); + var domainBufferSize = domainBuffer.Capacity; + var userNameBufferSize = userNameBuffer.Capacity; + var passwordBufferSize = passwordBuffer.Capacity; + + //#define CRED_PACK_PROTECTED_CREDENTIALS 0x1 + //#define CRED_PACK_WOW_BUFFER 0x2 + //#define CRED_PACK_GENERIC_CREDENTIALS 0x4 + var dwFlags = 0x1; + + var result = Credui.CredUnPackAuthenticationBufferW(dwFlags, authBufferPtr, authBufferSize, userNameBuffer, ref userNameBufferSize, domainBuffer, ref domainBufferSize, passwordBuffer, ref passwordBufferSize); + if (result == false) { - var domainBuffer = new StringBuilder(Credui.CREDUI_MAX_DOMAIN_LENGTH); - var userNameBuffer = new StringBuilder(Credui.CREDUI_MAX_USERNAME_LENGTH); - var passwordBuffer = new StringBuilder(Credui.CREDUI_MAX_PASSWORD_LENGTH); - var domainBufferSize = domainBuffer.Capacity; - var userNameBufferSize = userNameBuffer.Capacity; - var passwordBufferSize = passwordBuffer.Capacity; - - //#define CRED_PACK_PROTECTED_CREDENTIALS 0x1 - //#define CRED_PACK_WOW_BUFFER 0x2 - //#define CRED_PACK_GENERIC_CREDENTIALS 0x4 - var dwFlags = 0x1; - - var result = Credui.CredUnPackAuthenticationBufferW(dwFlags, authBufferPtr, authBufferSize, userNameBuffer, ref userNameBufferSize, domainBuffer, ref domainBufferSize, passwordBuffer, ref passwordBufferSize); - if (result == false) - { - var win32Error = Marshal.GetLastWin32Error(); - throw new Win32Exception(win32Error); - } + var win32Error = Marshal.GetLastWin32Error(); + throw new Win32Exception(win32Error); + } - var networkCredential = new NetworkCredential - { - Domain = domainBuffer.ToString(), - UserName = userNameBuffer.ToString(), - Password = passwordBuffer.ToString() - }; + var networkCredential = new NetworkCredential + { + Domain = domainBuffer.ToString(), + UserName = userNameBuffer.ToString(), + Password = passwordBuffer.ToString() + }; + + if (string.IsNullOrWhiteSpace(networkCredential.Domain)) + { + userNameBuffer.Clear(); + domainBuffer.Clear(); - if (string.IsNullOrWhiteSpace(networkCredential.Domain)) + var retval = Credui.CredUIParseUserNameW(networkCredential.UserName, userNameBuffer, userNameBuffer.Capacity, domainBuffer, domainBuffer.Capacity); + + switch (retval) { - userNameBuffer.Clear(); - domainBuffer.Clear(); - - var retval = Credui.CredUIParseUserNameW(networkCredential.UserName, userNameBuffer, userNameBuffer.Capacity, domainBuffer, domainBuffer.Capacity); - - switch (retval) - { - case NativeMethods.ERROR_SUCCESS: - networkCredential.Domain = domainBuffer.ToString(); - networkCredential.UserName = userNameBuffer.ToString(); - break; - case NativeMethods.ERROR_INVALID_ACCOUNT_NAME: - break; - //case NativeMethods.ERROR_INSUFFICIENT_BUFFER: - //case NativeMethods.ERROR_INVALID_PARAMETER: - default: - throw new Win32Exception((int)retval); - } + case NativeMethods.ERROR_SUCCESS: + networkCredential.Domain = domainBuffer.ToString(); + networkCredential.UserName = userNameBuffer.ToString(); + break; + case NativeMethods.ERROR_INVALID_ACCOUNT_NAME: + break; + //case NativeMethods.ERROR_INSUFFICIENT_BUFFER: + //case NativeMethods.ERROR_INVALID_PARAMETER: + default: + throw new Win32Exception((int)retval); } - - return networkCredential; } + + return networkCredential; } +} - internal class CredentialDialogOptions +internal class CredentialDialogOptions +{ + public CredentialDialogOptions(string caption, string message) { - public CredentialDialogOptions(string caption, string message) - { - if (caption.Length > Credui.CREDUI_MAX_CAPTION_LENGTH) throw new ArgumentOutOfRangeException(nameof(caption)); - if (message.Length > Credui.CREDUI_MAX_MESSAGE_LENGTH) throw new ArgumentOutOfRangeException(nameof(message)); + if (caption.Length > Credui.CREDUI_MAX_CAPTION_LENGTH) throw new ArgumentOutOfRangeException(nameof(caption)); + if (message.Length > Credui.CREDUI_MAX_MESSAGE_LENGTH) throw new ArgumentOutOfRangeException(nameof(message)); - Caption = caption; - Message = message; + Caption = caption; + Message = message; - AuthError = 0; - IsSaveChecked = false; - Flags = Credui.CREDUIWIN.CREDUIWIN_GENERIC; - } + AuthError = 0; + IsSaveChecked = false; + Flags = Credui.CREDUIWIN.CREDUIWIN_GENERIC; + } - public string Caption { get; set; } + public string Caption { get; set; } - public string Message { get; set; } + public string Message { get; set; } - public IntPtr HwndParent { get; set; } + public IntPtr HwndParent { get; set; } - //public IntPtr HbmBanner { get; set; } + //public IntPtr HbmBanner { get; set; } - public bool IsSaveChecked { get; private set; } + public bool IsSaveChecked { get; private set; } - public int AuthError { get; private set; } + public int AuthError { get; private set; } - public Credui.CREDUIWIN Flags { get; private set; } - } + public Credui.CREDUIWIN Flags { get; private set; } } diff --git a/src/Infrastructure/Security/CredentialManager.cs b/src/Infrastructure/Security/CredentialManager.cs index 4425a90b..0844e538 100644 --- a/src/Infrastructure/Security/CredentialManager.cs +++ b/src/Infrastructure/Security/CredentialManager.cs @@ -1,138 +1,137 @@ -namespace Sqlbi.Bravo.Infrastructure.Security +using Sqlbi.Bravo.Infrastructure.Windows.Interop; +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Net; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json.Serialization; + +namespace Sqlbi.Bravo.Infrastructure.Security; + +internal class CredentialManager { - using Sqlbi.Bravo.Infrastructure.Windows.Interop; - using System; - using System.ComponentModel; - using System.Diagnostics.CodeAnalysis; - using System.Net; - using System.Runtime.InteropServices; - using System.Text; - using System.Text.Json.Serialization; - - internal class CredentialManager + public static bool TryGetCredential(string targetName, [MaybeNullWhen(false)] out GenericCredential credential) { - public static bool TryGetCredential(string targetName, [MaybeNullWhen(false)] out GenericCredential credential) - { - credential = null; + credential = null; - var retval = Advapi32.CredReadW(targetName, Advapi32.CREDENTIAL_TYPE.CRED_TYPE_GENERIC, flags: 0, out var handle); - try - { - if (retval == true) - { - var credentialStruct = handle.GetCredential(); - - var userName = Marshal.PtrToStringUni(credentialStruct.UserName); - var password = credentialStruct.CredentialBlob != IntPtr.Zero - ? Marshal.PtrToStringUni(credentialStruct.CredentialBlob, (int)credentialStruct.CredentialBlobSize / 2) - : null; - - credential = new GenericCredential(targetName, userName, password); - return true; - } - } - finally + var retval = Advapi32.CredReadW(targetName, Advapi32.CREDENTIAL_TYPE.CRED_TYPE_GENERIC, flags: 0, out var handle); + try + { + if (retval == true) { - handle.Dispose(); - } + var credentialStruct = handle.GetCredential(); - return false; - } + var userName = Marshal.PtrToStringUni(credentialStruct.UserName); + var password = credentialStruct.CredentialBlob != IntPtr.Zero + ? Marshal.PtrToStringUni(credentialStruct.CredentialBlob, (int)credentialStruct.CredentialBlobSize / 2) + : null; - public static void WriteCredential(string targetName, string userName, string password, Advapi32.CRED_PERSIST persist = Advapi32.CRED_PERSIST.CRED_PERSIST_LOCAL_MACHINE) + credential = new GenericCredential(targetName, userName, password); + return true; + } + } + finally { - var targetNameLength = targetName.Length * UnicodeEncoding.CharSize; - var userNameLength = userName.Length * UnicodeEncoding.CharSize; - var passwordLength = password.Length * UnicodeEncoding.CharSize; + handle.Dispose(); + } - if (targetNameLength > Advapi32.CRED_MAX_GENERIC_TARGET_NAME_LENGTH) throw new ArgumentOutOfRangeException(nameof(targetName)); - if (userNameLength > Advapi32.CRED_MAX_USERNAME_LENGTH) throw new ArgumentOutOfRangeException(nameof(userName)); - if (passwordLength > Advapi32.CRED_MAX_CREDENTIAL_BLOB_SIZE) throw new ArgumentOutOfRangeException(nameof(password)); + return false; + } - var credential = new Advapi32.CREDENTIAL - { - // Flags = - Type = Advapi32.CREDENTIAL_TYPE.CRED_TYPE_GENERIC, - TargetName = Marshal.StringToCoTaskMemUni(targetName), - Comment = IntPtr.Zero, - // LastWritten = - CredentialBlobSize = (uint)passwordLength, - CredentialBlob = Marshal.StringToCoTaskMemUni(password), - Persist = persist, - AttributeCount = 0, - Attributes = IntPtr.Zero, - TargetAlias = IntPtr.Zero, - UserName = Marshal.StringToCoTaskMemUni(userName), - }; - - try - { - var written = Advapi32.CredWriteW(ref credential, flags: 0); - if (written == false) - { - var error = Marshal.GetLastWin32Error(); - throw new Win32Exception(error); - } - } - finally - { - Marshal.FreeCoTaskMem(credential.TargetName); - Marshal.FreeCoTaskMem(credential.UserName); - Marshal.FreeCoTaskMem(credential.CredentialBlob); - } - } + public static void WriteCredential(string targetName, string userName, string password, Advapi32.CRED_PERSIST persist = Advapi32.CRED_PERSIST.CRED_PERSIST_LOCAL_MACHINE) + { + var targetNameLength = targetName.Length * UnicodeEncoding.CharSize; + var userNameLength = userName.Length * UnicodeEncoding.CharSize; + var passwordLength = password.Length * UnicodeEncoding.CharSize; + + if (targetNameLength > Advapi32.CRED_MAX_GENERIC_TARGET_NAME_LENGTH) throw new ArgumentOutOfRangeException(nameof(targetName)); + if (userNameLength > Advapi32.CRED_MAX_USERNAME_LENGTH) throw new ArgumentOutOfRangeException(nameof(userName)); + if (passwordLength > Advapi32.CRED_MAX_CREDENTIAL_BLOB_SIZE) throw new ArgumentOutOfRangeException(nameof(password)); - public static bool DeleteCredential(string targetName) + var credential = new Advapi32.CREDENTIAL { - var success = Advapi32.CredDeleteW(targetName, Advapi32.CREDENTIAL_TYPE.CRED_TYPE_GENERIC, flags: 0); - if (success == false) + // Flags = + Type = Advapi32.CREDENTIAL_TYPE.CRED_TYPE_GENERIC, + TargetName = Marshal.StringToCoTaskMemUni(targetName), + Comment = IntPtr.Zero, + // LastWritten = + CredentialBlobSize = (uint)passwordLength, + CredentialBlob = Marshal.StringToCoTaskMemUni(password), + Persist = persist, + AttributeCount = 0, + Attributes = IntPtr.Zero, + TargetAlias = IntPtr.Zero, + UserName = Marshal.StringToCoTaskMemUni(userName), + }; + + try + { + var written = Advapi32.CredWriteW(ref credential, flags: 0); + if (written == false) { var error = Marshal.GetLastWin32Error(); - if (error == NativeMethods.ERROR_NOT_FOUND) - return false; - throw new Win32Exception(error); } - - return success; + } + finally + { + Marshal.FreeCoTaskMem(credential.TargetName); + Marshal.FreeCoTaskMem(credential.UserName); + Marshal.FreeCoTaskMem(credential.CredentialBlob); } } - /// - /// The credential is a generic credential. The credential will not be used by any particular authentication package. The credential will be stored securely but has no other significant characteristics. - /// - internal class GenericCredential + public static bool DeleteCredential(string targetName) { - public GenericCredential(string targetName, string? userName, string? password) + var success = Advapi32.CredDeleteW(targetName, Advapi32.CREDENTIAL_TYPE.CRED_TYPE_GENERIC, flags: 0); + if (success == false) { - TargetName = targetName; - UserName = userName; - Password = password; + var error = Marshal.GetLastWin32Error(); + if (error == NativeMethods.ERROR_NOT_FOUND) + return false; + + throw new Win32Exception(error); } - public string TargetName { get; init; } + return success; + } +} - public string? UserName { get; init; } +/// +/// The credential is a generic credential. The credential will not be used by any particular authentication package. The credential will be stored securely but has no other significant characteristics. +/// +internal class GenericCredential +{ + public GenericCredential(string targetName, string? userName, string? password) + { + TargetName = targetName; + UserName = userName; + Password = password; + } - [JsonIgnore] - public string? Password { get; init; } + public string TargetName { get; init; } - public NetworkCredential? ToNetworkCredential() - { - var userName = UserName; - var password = Password; - var domain = (string?)null; + public string? UserName { get; init; } - var userNameTokens = UserName?.Split('\\'); - if (userNameTokens?.Length == 2) - { - // DOMAIN\USER - domain = userNameTokens[0]; - userName = userNameTokens[1]; - } + [JsonIgnore] + public string? Password { get; init; } - var credential = new NetworkCredential(userName, password, domain); - return credential; + public NetworkCredential? ToNetworkCredential() + { + var userName = UserName; + var password = Password; + var domain = (string?)null; + + var userNameTokens = UserName?.Split('\\'); + if (userNameTokens?.Length == 2) + { + // DOMAIN\USER + domain = userNameTokens[0]; + userName = userNameTokens[1]; } + + var credential = new NetworkCredential(userName, password, domain); + return credential; } -} \ No newline at end of file +} diff --git a/src/Infrastructure/Security/Cryptography.cs b/src/Infrastructure/Security/Cryptography.cs index 1b263d4b..3fbe455e 100644 --- a/src/Infrastructure/Security/Cryptography.cs +++ b/src/Infrastructure/Security/Cryptography.cs @@ -1,157 +1,156 @@ -namespace Sqlbi.Bravo.Infrastructure.Security +using System; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Cryptography; +using System.Text; + +namespace Sqlbi.Bravo.Infrastructure.Security; + +internal static class Cryptography { - using System; - using System.Linq; - using System.Runtime.InteropServices; - using System.Security; - using System.Security.Cryptography; - using System.Text; - - internal static class Cryptography + // !!! + // !!! Do not use Environment.MachineName for entropy string as the DataProtectionScope is DataProtectionScope.CurrentUser only. + // !!! + // This prevent an error where a user with a roaming profile cannot decrypt the data from another computer on the network (e.g. MSAL.NET token cache) + // See CryptProtectData function (dpapi.h) https://docs.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata + private static readonly byte[] DefaultEntropy = Encoding.Unicode.GetBytes($"{Environment.UserName}|3ae*f-4aew1/L22"); + + /// + /// Encrypts the data in a specified byte array. + /// + /// The protected data is associated with the current user and only threads running under the current user context can unprotect the data. + public static byte[] Protect(byte[] userData, byte[]? entropy = null) => ProtectedData.Protect(userData, entropy ?? DefaultEntropy, DataProtectionScope.CurrentUser); + + /// + /// Decrypts the data in a specified byte array. + /// + /// The protected data is associated with the current user and only threads running under the current user context can unprotect the data. + public static byte[] Unprotect(byte[] encryptedData, byte[]? entropy = null) => ProtectedData.Unprotect(encryptedData, entropy ?? DefaultEntropy, DataProtectionScope.CurrentUser); + + /// + /// Encodes the provided buffers into an MD5 hashed string + /// + public static string MD5Hash(byte[][] buffers) { - // !!! - // !!! Do not use Environment.MachineName for entropy string as the DataProtectionScope is DataProtectionScope.CurrentUser only. - // !!! - // This prevent an error where a user with a roaming profile cannot decrypt the data from another computer on the network (e.g. MSAL.NET token cache) - // See CryptProtectData function (dpapi.h) https://docs.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata - private static readonly byte[] DefaultEntropy = Encoding.Unicode.GetBytes($"{Environment.UserName}|3ae*f-4aew1/L22"); - - /// - /// Encrypts the data in a specified byte array. - /// - /// The protected data is associated with the current user and only threads running under the current user context can unprotect the data. - public static byte[] Protect(byte[] userData, byte[]? entropy = null) => ProtectedData.Protect(userData, entropy ?? DefaultEntropy, DataProtectionScope.CurrentUser); - - /// - /// Decrypts the data in a specified byte array. - /// - /// The protected data is associated with the current user and only threads running under the current user context can unprotect the data. - public static byte[] Unprotect(byte[] encryptedData, byte[]? entropy = null) => ProtectedData.Unprotect(encryptedData, entropy ?? DefaultEntropy, DataProtectionScope.CurrentUser); - - /// - /// Encodes the provided buffers into an MD5 hashed string - /// - public static string MD5Hash(byte[][] buffers) - { - var buffer = buffers.SelectMany((x) => x).ToArray(); // flatten byte arrays into a one-byte array (concatenate) - var hash = MD5Hash(buffer); + var buffer = buffers.SelectMany((x) => x).ToArray(); // flatten byte arrays into a one-byte array (concatenate) + var hash = MD5Hash(buffer); - return hash; - } + return hash; + } - /// - /// Encodes the provided buffers into an MD5 hashed string - /// - public static string MD5Hash(byte[] buffer) - { - using var md5 = MD5.Create(); - md5.Initialize(); + /// + /// Encodes the provided buffers into an MD5 hashed string + /// + public static string MD5Hash(byte[] buffer) + { + using var md5 = MD5.Create(); + md5.Initialize(); - var hashBytes = md5.ComputeHash(buffer); - var hash = BitConverter.ToString(hashBytes).Replace("-", string.Empty); + var hashBytes = md5.ComputeHash(buffer); + var hash = BitConverter.ToString(hashBytes).Replace("-", string.Empty); - return hash; - } + return hash; + } - public static string SHA256Hash(string value) - { - ArgumentNullException.ThrowIfNull(value); + public static string SHA256Hash(string value) + { + ArgumentNullException.ThrowIfNull(value); - using var algorithm = SHA256.Create(); - var stringBuilder = new StringBuilder(); + using var algorithm = SHA256.Create(); + var stringBuilder = new StringBuilder(); - var buffer = Encoding.UTF8.GetBytes(value); - var count = Encoding.UTF8.GetByteCount(value); - var bytes = algorithm.ComputeHash(buffer, offset: 0, count); + var buffer = Encoding.UTF8.GetBytes(value); + var count = Encoding.UTF8.GetByteCount(value); + var bytes = algorithm.ComputeHash(buffer, offset: 0, count); - foreach (var @byte in bytes) - stringBuilder.Append(@byte.ToString("x2")); + foreach (var @byte in bytes) + stringBuilder.Append(@byte.ToString("x2")); - return stringBuilder.ToString(); - } + return stringBuilder.ToString(); + } - public static string GenerateSimpleToken() - { - var token = $"{Guid.NewGuid()}-{Guid.NewGuid()}"; - var tokenBytes = Encoding.UTF8.GetBytes(token); + public static string GenerateSimpleToken() + { + var token = $"{Guid.NewGuid()}-{Guid.NewGuid()}"; + var tokenBytes = Encoding.UTF8.GetBytes(token); - token = Convert.ToBase64String(tokenBytes, Base64FormattingOptions.None); - return token; - } + token = Convert.ToBase64String(tokenBytes, Base64FormattingOptions.None); + return token; } +} - internal static class CriptographyExtensions - { - public static string ToSHA256Hash(this string value) => Cryptography.SHA256Hash(value); +internal static class CriptographyExtensions +{ + public static string ToSHA256Hash(this string value) => Cryptography.SHA256Hash(value); - public static string ToProtectedString(this SecureString secureString) + public static string ToProtectedString(this SecureString secureString) + { + var unsecuredChars = new char[secureString.Length]; + try { - var unsecuredChars = new char[secureString.Length]; + var gch = GCHandle.Alloc(unsecuredChars, GCHandleType.Pinned); try { - var gch = GCHandle.Alloc(unsecuredChars, GCHandleType.Pinned); + var ptr = IntPtr.Zero; try { - var ptr = IntPtr.Zero; + ptr = Marshal.SecureStringToGlobalAllocUnicode(secureString); + Marshal.Copy(ptr, unsecuredChars, 0, unsecuredChars.Length); + var unsecuredBytes = Encoding.Unicode.GetBytes(unsecuredChars); try { - ptr = Marshal.SecureStringToGlobalAllocUnicode(secureString); - Marshal.Copy(ptr, unsecuredChars, 0, unsecuredChars.Length); - var unsecuredBytes = Encoding.Unicode.GetBytes(unsecuredChars); - try - { - var protectedBytes = Cryptography.Protect(unsecuredBytes); - var protectedString = Convert.ToBase64String(protectedBytes); - - return protectedString; - } - finally - { - Array.Clear(unsecuredBytes, 0, unsecuredBytes.Length); - } + var protectedBytes = Cryptography.Protect(unsecuredBytes); + var protectedString = Convert.ToBase64String(protectedBytes); + + return protectedString; } finally { - Marshal.ZeroFreeGlobalAllocUnicode(ptr); + Array.Clear(unsecuredBytes, 0, unsecuredBytes.Length); } } finally { - gch.Free(); + Marshal.ZeroFreeGlobalAllocUnicode(ptr); } } finally { - Array.Clear(unsecuredChars, 0, unsecuredChars.Length); + gch.Free(); } } + finally + { + Array.Clear(unsecuredChars, 0, unsecuredChars.Length); + } + } - public static SecureString ToSecureString(this string protectedString) + public static SecureString ToSecureString(this string protectedString) + { + var protectedBytes = Convert.FromBase64String(protectedString); + var unprotectedBytes = Cryptography.Unprotect(protectedBytes); + try { - var protectedBytes = Convert.FromBase64String(protectedString); - var unprotectedBytes = Cryptography.Unprotect(protectedBytes); + var unprotectedChars = Encoding.Unicode.GetChars(unprotectedBytes); try { - var unprotectedChars = Encoding.Unicode.GetChars(unprotectedBytes); - try - { - var secureString = new SecureString(); + var secureString = new SecureString(); - foreach (var @char in unprotectedChars) - secureString.AppendChar(@char); + foreach (var @char in unprotectedChars) + secureString.AppendChar(@char); - secureString.MakeReadOnly(); - return secureString; - } - finally - { - Array.Clear(unprotectedChars, 0, unprotectedChars.Length); - } + secureString.MakeReadOnly(); + return secureString; } finally { - Array.Clear(unprotectedBytes, 0, unprotectedBytes.Length); + Array.Clear(unprotectedChars, 0, unprotectedChars.Length); } } + finally + { + Array.Clear(unprotectedBytes, 0, unprotectedBytes.Length); + } } } diff --git a/src/Infrastructure/Services/ConnectionWrapper.cs b/src/Infrastructure/Services/ConnectionWrapper.cs index aabc432c..8e23d692 100644 --- a/src/Infrastructure/Services/ConnectionWrapper.cs +++ b/src/Infrastructure/Services/ConnectionWrapper.cs @@ -1,156 +1,154 @@ -namespace Sqlbi.Bravo.Infrastructure.Services +using System; +using System.Text.Json.Nodes; +using Microsoft.AnalysisServices.AdomdClient; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Models; +using TOM = Microsoft.AnalysisServices.Tabular; + +namespace Sqlbi.Bravo.Infrastructure.Services; + +internal class TabularConnectionWrapper : IDisposable { - using Microsoft.AnalysisServices.AdomdClient; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Security; - using Sqlbi.Bravo.Models; - using System; - using System.Text.Json.Nodes; - using TOM = Microsoft.AnalysisServices.Tabular; - - internal class TabularConnectionWrapper : IDisposable - { - private readonly string _connectionString; + private readonly string _connectionString; - private TabularConnectionWrapper(string connectionString, string databaseIdOrName, bool findById) - { - _connectionString = connectionString; + private TabularConnectionWrapper(string connectionString, string databaseIdOrName, bool findById) + { + _connectionString = connectionString; - Server = new TOM.Server(); - ProcessHelper.RunOnUISynchronizationContext(() => Server.Connect(connectionString)); - Database = findById ? Server.Databases.Find(databaseIdOrName) : Server.Databases.FindByName(databaseIdOrName); + Server = new TOM.Server(); + ProcessHelper.RunOnUISynchronizationContext(() => Server.Connect(connectionString)); + Database = findById ? Server.Databases.Find(databaseIdOrName) : Server.Databases.FindByName(databaseIdOrName); - if (Database is null) + if (Database is null) + { + if (AppEnvironment.IsDiagnosticLevelVerbose) { - if (AppEnvironment.IsDiagnosticLevelVerbose) + var properties = Server.SerializeDiagnosticProperties(); + var content = new JsonObject { - var properties = Server.SerializeDiagnosticProperties(); - var content = new JsonObject - { - { nameof(databaseIdOrName), databaseIdOrName }, - { nameof(findById), findById }, - { nameof(properties), properties } - }; - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(TabularConnectionWrapper)}.ctor", content.ToJsonString()); - } - - throw new BravoException(BravoProblem.TOMDatabaseDatabaseNotFound, databaseIdOrName); + { nameof(databaseIdOrName), databaseIdOrName }, + { nameof(findById), findById }, + { nameof(properties), properties } + }; + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(TabularConnectionWrapper)}.ctor", content.ToJsonString()); } - Model = Database.Model; + throw new BravoException(BravoProblem.TOMDatabaseDatabaseNotFound, databaseIdOrName); } - public TOM.Server Server { get; } - - public TOM.Database Database { get; } + Model = Database.Model; + } - public TOM.Model Model { get; } + public TOM.Server Server { get; } - public AdomdConnection CreateAdomdConnection(bool open = true) - { - var connection = new AdomdConnection(_connectionString); + public TOM.Database Database { get; } - if (open) - { - connection.Open(); - connection.ChangeDatabase(Database.Name); - } + public TOM.Model Model { get; } - return connection; - } + public AdomdConnection CreateAdomdConnection(bool open = true) + { + var connection = new AdomdConnection(_connectionString); - public void Dispose() + if (open) { - Database?.Dispose(); - Server?.Dispose(); + connection.Open(); + connection.ChangeDatabase(Database.Name); } - public static TabularConnectionWrapper ConnectTo(PBICloudDataset dataset, string accessToken) - { - BravoUnexpectedException.ThrowIfNull(dataset.DatabaseName); + return connection; + } - var connectionString = ConnectionStringHelper.BuildFor(dataset, accessToken); - var connection = new TabularConnectionWrapper(connectionString, dataset.DatabaseName, findById: true); + public void Dispose() + { + Database?.Dispose(); + Server?.Dispose(); + } - return connection; - } + public static TabularConnectionWrapper ConnectTo(PBICloudDataset dataset, string accessToken) + { + BravoUnexpectedException.ThrowIfNull(dataset.DatabaseName); - public static TabularConnectionWrapper ConnectTo(PBIDesktopReport report) - { - BravoUnexpectedException.ThrowIfNull(report.DatabaseName); + var connectionString = ConnectionStringHelper.BuildFor(dataset, accessToken); + var connection = new TabularConnectionWrapper(connectionString, dataset.DatabaseName, findById: true); - var connectionString = ConnectionStringHelper.BuildFor(report); - var connection = new TabularConnectionWrapper(connectionString, report.DatabaseName, findById: false); + return connection; + } - return connection; - } + public static TabularConnectionWrapper ConnectTo(PBIDesktopReport report) + { + BravoUnexpectedException.ThrowIfNull(report.DatabaseName); + + var connectionString = ConnectionStringHelper.BuildFor(report); + var connection = new TabularConnectionWrapper(connectionString, report.DatabaseName, findById: false); + + return connection; } +} - internal class AdomdConnectionWrapper : IDisposable +internal class AdomdConnectionWrapper : IDisposable +{ + private AdomdConnectionWrapper(string connectionString, string databaseName) { - private AdomdConnectionWrapper(string connectionString, string databaseName) - { - Connection = new AdomdConnection(connectionString); - ProcessHelper.RunOnUISynchronizationContext(() => Connection.Open()); - Connection.ChangeDatabase(databaseName); + Connection = new AdomdConnection(connectionString); + ProcessHelper.RunOnUISynchronizationContext(() => Connection.Open()); + Connection.ChangeDatabase(databaseName); - IsServerVersion13OrGreater = Version.TryParse(Connection.ServerVersion, out var version) && version >= new Version(13, 0); - } + IsServerVersion13OrGreater = Version.TryParse(Connection.ServerVersion, out var version) && version >= new Version(13, 0); + } - public AdomdConnection Connection { get; } + public AdomdConnection Connection { get; } - public bool IsServerVersion13OrGreater { get; } + public bool IsServerVersion13OrGreater { get; } - public AdomdCommand CreateAdomdCommand() - { - var command = Connection.CreateCommand(); - return command; - } + public AdomdCommand CreateAdomdCommand() + { + var command = Connection.CreateCommand(); + return command; + } - public AdomdCommand CreateDmvTablesCommand() + public AdomdCommand CreateDmvTablesCommand() + { + var command = Connection.CreateCommand(); { - var command = Connection.CreateCommand(); - { - command.CommandText = "SELECT [DIMENSION_UNIQUE_NAME], [DIMENSION_TYPE], [DIMENSION_CARDINALITY] FROM $SYSTEM.MDSCHEMA_DIMENSIONS WHERE [CATALOG_NAME] = @catalogName AND [DIMENSION_TYPE] <> 2"; // MD_DIMTYPE_MEASURE = 2 - command.Parameters.Add(new AdomdParameter(parameterName: "catalogName", value: Connection.Database)); - } - return command; + command.CommandText = "SELECT [DIMENSION_UNIQUE_NAME], [DIMENSION_TYPE], [DIMENSION_CARDINALITY] FROM $SYSTEM.MDSCHEMA_DIMENSIONS WHERE [CATALOG_NAME] = @catalogName AND [DIMENSION_TYPE] <> 2"; // MD_DIMTYPE_MEASURE = 2 + command.Parameters.Add(new AdomdParameter(parameterName: "catalogName", value: Connection.Database)); } + return command; + } - public AdomdCommand CreateDmvTablesWithColumnsCommand() + public AdomdCommand CreateDmvTablesWithColumnsCommand() + { + var command = Connection.CreateCommand(); { - var command = Connection.CreateCommand(); - { - command.CommandText = "SELECT DISTINCT [DIMENSION_UNIQUE_NAME] FROM $SYSTEM.MDSCHEMA_HIERARCHIES WHERE [CATALOG_NAME] = @catalogName AND [DIMENSION_TYPE] <> 2"; // MD_DIMTYPE_MEASURE = 2 - command.Parameters.Add(new AdomdParameter(parameterName: "catalogName", value: Connection.Database)); - } - return command; + command.CommandText = "SELECT DISTINCT [DIMENSION_UNIQUE_NAME] FROM $SYSTEM.MDSCHEMA_HIERARCHIES WHERE [CATALOG_NAME] = @catalogName AND [DIMENSION_TYPE] <> 2"; // MD_DIMTYPE_MEASURE = 2 + command.Parameters.Add(new AdomdParameter(parameterName: "catalogName", value: Connection.Database)); } + return command; + } - public void Dispose() - { - Connection.Dispose(); - } + public void Dispose() + { + Connection.Dispose(); + } - public static AdomdConnectionWrapper ConnectTo(PBICloudDataset dataset, string accessToken) - { - BravoUnexpectedException.ThrowIfNull(dataset.ExternalDatabaseName); + public static AdomdConnectionWrapper ConnectTo(PBICloudDataset dataset, string accessToken) + { + BravoUnexpectedException.ThrowIfNull(dataset.ExternalDatabaseName); - var connectionString = ConnectionStringHelper.BuildFor(dataset, accessToken); - var connection = new AdomdConnectionWrapper(connectionString, dataset.ExternalDatabaseName); + var connectionString = ConnectionStringHelper.BuildFor(dataset, accessToken); + var connection = new AdomdConnectionWrapper(connectionString, dataset.ExternalDatabaseName); - return connection; - } + return connection; + } - public static AdomdConnectionWrapper ConnectTo(PBIDesktopReport report) - { - BravoUnexpectedException.ThrowIfNull(report.DatabaseName); + public static AdomdConnectionWrapper ConnectTo(PBIDesktopReport report) + { + BravoUnexpectedException.ThrowIfNull(report.DatabaseName); - var connectionString = ConnectionStringHelper.BuildFor(report); - var connection = new AdomdConnectionWrapper(connectionString, report.DatabaseName); + var connectionString = ConnectionStringHelper.BuildFor(report); + var connection = new AdomdConnectionWrapper(connectionString, report.DatabaseName); - return connection; - } + return connection; } } diff --git a/src/Infrastructure/Services/DaxTemplate/DaxTemplateManager.cs b/src/Infrastructure/Services/DaxTemplate/DaxTemplateManager.cs index 03a8bf57..3aa470bd 100644 --- a/src/Infrastructure/Services/DaxTemplate/DaxTemplateManager.cs +++ b/src/Infrastructure/Services/DaxTemplate/DaxTemplateManager.cs @@ -1,165 +1,168 @@ -namespace Sqlbi.Bravo.Infrastructure.Services.DaxTemplate +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using Dax.Template; +using Dax.Template.Model; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Policies; +using Sqlbi.Bravo.Models.ManageDates; + +namespace Sqlbi.Bravo.Infrastructure.Services.DaxTemplate; + +internal class DaxTemplateManager { - using Dax.Template; - using Dax.Template.Model; - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Policies; - using Sqlbi.Bravo.Models.ManageDates; - - internal class DaxTemplateManager - { - private static bool CacheInitialized = false; + private static bool CacheInitialized = false; - public const string SqlbiTemplateAnnotation = "SQLBI_Template"; - public const string SqlbiTemplateAnnotationDatesValue = "Dates"; - public const string SqlbiTemplateAnnotationHolidaysValue = "Holidays"; - public const string SqlbiTemplateTableAnnotation = "SQLBI_TemplateTable"; - public const string SqlbiTemplateTableAnnotationDateValue = "Date"; - public const string SqlbiTemplateTableAnnotationDateAutoTemplateValue = "DateAutoTemplate"; - public const string SqlbiTemplateTableAnnotationHolidaysValue = "Holidays"; - public const string SqlbiTemplateTableAnnotationHolidaysDefinitionValue = "HolidaysDefinition"; + public const string SqlbiTemplateAnnotation = "SQLBI_Template"; + public const string SqlbiTemplateAnnotationDatesValue = "Dates"; + public const string SqlbiTemplateAnnotationHolidaysValue = "Holidays"; + public const string SqlbiTemplateTableAnnotation = "SQLBI_TemplateTable"; + public const string SqlbiTemplateTableAnnotationDateValue = "Date"; + public const string SqlbiTemplateTableAnnotationDateAutoTemplateValue = "DateAutoTemplate"; + public const string SqlbiTemplateTableAnnotationHolidaysValue = "Holidays"; + public const string SqlbiTemplateTableAnnotationHolidaysDefinitionValue = "HolidaysDefinition"; - private const string TemplateEmbeddedResourcePrefix = "Sqlbi.Bravo.Assets.ManageDates.Templates."; - private const string SchemaEmbeddedResourcePrefix = "Sqlbi.Bravo.Assets.TemplateDevelopment.Schemas."; + private const string TemplateEmbeddedResourcePrefix = "Sqlbi.Bravo.Assets.ManageDates.Templates."; + private const string SchemaEmbeddedResourcePrefix = "Sqlbi.Bravo.Assets.TemplateDevelopment.Schemas."; - internal static readonly string CachePath = Path.Combine(AppEnvironment.ApplicationTempPath, @"ManageDates\Templates"); - internal static readonly string UserPath = Path.Combine(AppEnvironment.ApplicationDataPath, @"ManageDates\Templates"); + internal static readonly string CachePath = Path.Combine(AppEnvironment.ApplicationTempPath, @"ManageDates\Templates"); + internal static readonly string UserPath = Path.Combine(AppEnvironment.ApplicationDataPath, @"ManageDates\Templates"); - private readonly object _cacheSyncLock = new(); - private readonly IPolicies _policies; + private readonly object _cacheSyncLock = new(); + private readonly IPolicies _policies; - public DaxTemplateManager(IPolicies policies) - { - _policies = policies; - InitializeCache(); - } + public DaxTemplateManager(IPolicies policies) + { + _policies = policies; + InitializeCache(); + } - public Package GetPackage(string path) - { - return Package.LoadFromFile(path); - } + public Package GetPackage(string path) + { + return Package.LoadFromFile(path); + } - public IEnumerable GetPackages() + public IEnumerable GetPackages() + { + if (_policies.BuiltInTemplatesEnabled is false) { - if (_policies.BuiltInTemplatesEnabled is false) - { - return Array.Empty(); - } - - var files = Package.FindTemplateFiles(CachePath); - return files.Select(Package.LoadFromFile).ToArray(); + return Array.Empty(); } - public ModelChanges GetPreviewChanges(DateConfiguration configuration, int previewRows, TabularConnectionWrapper connection, CancellationToken cancellationToken) - { - var package = configuration.LoadPackage(); - return GetPreviewChanges(package, previewRows, connection, cancellationToken); - } + var files = Package.FindTemplateFiles(CachePath); + return files.Select(Package.LoadFromFile).ToArray(); + } - public ModelChanges GetPreviewChanges(Package package, int previewRows, TabularConnectionWrapper connection, CancellationToken cancellationToken) - { - var engine = new Engine(package); + public ModelChanges GetPreviewChanges(DateConfiguration configuration, int previewRows, TabularConnectionWrapper connection, CancellationToken cancellationToken) + { + var package = configuration.LoadPackage(); + return GetPreviewChanges(package, previewRows, connection, cancellationToken); + } - engine.ApplyTemplates(connection.Model, cancellationToken); - try - { - var modelChanges = Engine.GetModelChanges(connection.Model, cancellationToken); + public ModelChanges GetPreviewChanges(Package package, int previewRows, TabularConnectionWrapper connection, CancellationToken cancellationToken) + { + var engine = new Engine(package); - if (previewRows > 0) - { - using var adomdConnection = connection.CreateAdomdConnection(); - modelChanges.PopulatePreview(adomdConnection, connection.Model, previewRows, cancellationToken); - } + engine.ApplyTemplates(connection.Model, cancellationToken); + try + { + var modelChanges = Engine.GetModelChanges(connection.Model, cancellationToken); - return modelChanges; - } - finally + if (previewRows > 0) { - connection.Model.UndoLocalChanges(); + using var adomdConnection = connection.CreateAdomdConnection(); + modelChanges.PopulatePreview(adomdConnection, connection.Model, previewRows, cancellationToken); } - } - public void ApplyConfiguration(DateConfiguration configuration, TabularConnectionWrapper connection, CancellationToken cancellationToken) - { - var package = configuration.LoadPackage(); - var engine = new Engine(package); - - engine.ApplyTemplates(connection.Model, cancellationToken); - configuration.SerializeTo(connection.Model); - connection.Model.SaveChanges().ThrowOnError(); + return modelChanges; } - - private IEnumerable<(string Name, Stream Content)> GetSchemaFiles() + finally { - var files = GetResourceFiles(resourcePrefix: SchemaEmbeddedResourcePrefix); - return files; + connection.Model.UndoLocalChanges(); } + } - private IEnumerable<(string Name, Stream Content)> GetTemplateFiles() - { - var files = GetResourceFiles(resourcePrefix: TemplateEmbeddedResourcePrefix); - return files; - } + public void ApplyConfiguration(DateConfiguration configuration, TabularConnectionWrapper connection, CancellationToken cancellationToken) + { + var package = configuration.LoadPackage(); + var engine = new Engine(package); - private IEnumerable<(string Name, Stream Content)> GetResourceFiles(string resourcePrefix) - { - var assembly = typeof(Program).Assembly; - var resourceNames = assembly.GetManifestResourceNames(); + engine.ApplyTemplates(connection.Model, cancellationToken); + configuration.SerializeTo(connection.Model); + connection.Model.SaveChanges().ThrowOnError(); + } - foreach (var resourceName in resourceNames) + private IEnumerable<(string Name, Stream Content)> GetSchemaFiles() + { + var files = GetResourceFiles(resourcePrefix: SchemaEmbeddedResourcePrefix); + return files; + } + + private IEnumerable<(string Name, Stream Content)> GetTemplateFiles() + { + var files = GetResourceFiles(resourcePrefix: TemplateEmbeddedResourcePrefix); + return files; + } + + private IEnumerable<(string Name, Stream Content)> GetResourceFiles(string resourcePrefix) + { + var assembly = typeof(Program).Assembly; + var resourceNames = assembly.GetManifestResourceNames(); + + foreach (var resourceName in resourceNames) + { + if (resourceName.StartsWith(resourcePrefix)) { - if (resourceName.StartsWith(resourcePrefix)) + var stream = assembly.GetManifestResourceStream(resourceName); + if (stream is not null) { - var stream = assembly.GetManifestResourceStream(resourceName); - if (stream is not null) - { - var name = resourceName.Remove(0, resourcePrefix.Length); - yield return (Name: name, Content: stream); - } + var name = resourceName.Remove(0, resourcePrefix.Length); + yield return (Name: name, Content: stream); } } } + } - private void InitializeCache() + private void InitializeCache() + { + if (CacheInitialized == false) { - if (CacheInitialized == false) + lock (_cacheSyncLock) { - lock (_cacheSyncLock) + if (CacheInitialized == false) { - if (CacheInitialized == false) - { - if (Directory.Exists(CachePath)) - Directory.Delete(CachePath, recursive: true); + if (Directory.Exists(CachePath)) + Directory.Delete(CachePath, recursive: true); + + Directory.CreateDirectory(CachePath); - Directory.CreateDirectory(CachePath); - - if (Directory.Exists(UserPath)) + if (Directory.Exists(UserPath)) + { + // If the path exists then we use the templates from this folder instead of using the built-in default templates - this is for testing/debug purpose only + foreach (var assetFile in Directory.EnumerateFiles(UserPath)) { - // If the path exists then we use the templates from this folder instead of using the built-in default templates - this is for testing/debug purpose only - foreach (var assetFile in Directory.EnumerateFiles(UserPath)) - { - var assetFileName = Path.GetFileName(assetFile); - var cacheFile = Path.Combine(CachePath, assetFileName); - - File.Copy(assetFile, cacheFile); - } + var assetFileName = Path.GetFileName(assetFile); + var cacheFile = Path.Combine(CachePath, assetFileName); + + File.Copy(assetFile, cacheFile); } - else - { - var templateFiles = GetTemplateFiles(); + } + else + { + var templateFiles = GetTemplateFiles(); - foreach (var templateFile in templateFiles) - { - var templateFilePath = Path.Combine(CachePath, templateFile.Name); + foreach (var templateFile in templateFiles) + { + var templateFilePath = Path.Combine(CachePath, templateFile.Name); - using var fileStream = File.Create(templateFilePath); - templateFile.Content.CopyTo(fileStream); - } + using var fileStream = File.Create(templateFilePath); + templateFile.Content.CopyTo(fileStream); } - - CacheInitialized = true; } + + CacheInitialized = true; } } } diff --git a/src/Infrastructure/Services/ExportData/ExportDataJobMap.cs b/src/Infrastructure/Services/ExportData/ExportDataJobMap.cs index 92b7c4ab..c1a3484d 100644 --- a/src/Infrastructure/Services/ExportData/ExportDataJobMap.cs +++ b/src/Infrastructure/Services/ExportData/ExportDataJobMap.cs @@ -1,43 +1,42 @@ -namespace Sqlbi.Bravo.Infrastructure.Services.ExportData +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using Sqlbi.Bravo.Infrastructure.Models; +using Sqlbi.Bravo.Models.ExportData; + +namespace Sqlbi.Bravo.Infrastructure.Services.ExportData; + +internal class ExportDataJobMap where T : class, IDataModel { - using Sqlbi.Bravo.Infrastructure.Models; - using Sqlbi.Bravo.Models.ExportData; - using System.Collections.Concurrent; - using System.Diagnostics.CodeAnalysis; + private readonly ConcurrentDictionary _jobs; + + public ExportDataJobMap() + { + _jobs = new ConcurrentDictionary(); + } + + public ExportDataJob AddNew(T datamodel, ExportDataSettings settings) + { + var job = ExportDataJob.CreateFrom(settings); + + var jobAdded = _jobs.TryAdd(datamodel, job); - internal class ExportDataJobMap where T : class, IDataModel + // Each IPBIDataModel is not allowed to start more than a single export job at a time + BravoUnexpectedException.Assert(jobAdded); + + job.SetRunning(); + + return job; + } + + public bool TryGet(T datamodel, [MaybeNullWhen(false)] out ExportDataJob job) { - private readonly ConcurrentDictionary _jobs; - - public ExportDataJobMap() - { - _jobs = new ConcurrentDictionary(); - } - - public ExportDataJob AddNew(T datamodel, ExportDataSettings settings) - { - var job = ExportDataJob.CreateFrom(settings); - - var jobAdded = _jobs.TryAdd(datamodel, job); - - // Each IPBIDataModel is not allowed to start more than a single export job at a time - BravoUnexpectedException.Assert(jobAdded); - - job.SetRunning(); - - return job; - } - - public bool TryGet(T datamodel, [MaybeNullWhen(false)] out ExportDataJob job) - { - return _jobs.TryGetValue(datamodel, out job); - } - - public void Remove(T datamodel) - { - var jobRemoved = _jobs.TryRemove(datamodel, out _); - - BravoUnexpectedException.Assert(jobRemoved); - } + return _jobs.TryGetValue(datamodel, out job); + } + + public void Remove(T datamodel) + { + var jobRemoved = _jobs.TryRemove(datamodel, out _); + + BravoUnexpectedException.Assert(jobRemoved); } } diff --git a/src/Infrastructure/Services/PowerBI/PBIDesktopService.cs b/src/Infrastructure/Services/PowerBI/PBIDesktopService.cs index b4dffedb..58b7bfe3 100644 --- a/src/Infrastructure/Services/PowerBI/PBIDesktopService.cs +++ b/src/Infrastructure/Services/PowerBI/PBIDesktopService.cs @@ -1,41 +1,39 @@ -namespace Sqlbi.Bravo.Infrastructure.Services.PowerBI -{ - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Models; - using System; - using System.Collections.Concurrent; - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Models; - public interface IPBIDesktopService - { - IEnumerable GetReports(CancellationToken cancellationToken); - } +namespace Sqlbi.Bravo.Infrastructure.Services.PowerBI; + +public interface IPBIDesktopService +{ + IEnumerable GetReports(CancellationToken cancellationToken); +} - internal class PBIDesktopService : IPBIDesktopService +internal class PBIDesktopService : IPBIDesktopService +{ + public IEnumerable GetReports(CancellationToken cancellationToken) { - public IEnumerable GetReports(CancellationToken cancellationToken) + var processes = ProcessHelper.GetProcessesByName(AppEnvironment.PBIDesktopProcessName); + try { - var processes = ProcessHelper.GetProcessesByName(AppEnvironment.PBIDesktopProcessName); - try + var reports = new ConcurrentBag(); + var parallelOptions = new ParallelOptions { CancellationToken = cancellationToken }; + var parallelLoop = Parallel.ForEach(processes, parallelOptions, (process) => { - var reports = new ConcurrentBag(); - var parallelOptions = new ParallelOptions { CancellationToken = cancellationToken }; - var parallelLoop = Parallel.ForEach(processes, parallelOptions, (process) => - { - var report = PBIDesktopReport.CreateFrom(process); - reports.Add(report); - }); + var report = PBIDesktopReport.CreateFrom(process); + reports.Add(report); + }); - return parallelLoop.IsCompleted ? reports : Array.Empty(); - } - finally - { - processes.ForEach((p) => p.Dispose()); - } + return parallelLoop.IsCompleted ? reports : Array.Empty(); + } + finally + { + processes.ForEach((p) => p.Dispose()); } } -} \ No newline at end of file +} diff --git a/src/Infrastructure/Services/ServerAddressProvider.cs b/src/Infrastructure/Services/ServerAddressProvider.cs index 9d2b8034..935e592c 100644 --- a/src/Infrastructure/Services/ServerAddressProvider.cs +++ b/src/Infrastructure/Services/ServerAddressProvider.cs @@ -1,8 +1,10 @@ -namespace Sqlbi.Bravo.Infrastructure.Services; - +using System; +using System.Linq; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; +namespace Sqlbi.Bravo.Infrastructure.Services; + /// /// Provides the listening address used by the Kestrel HTTP server /// @@ -35,4 +37,4 @@ public string GetListeningAddress() return feature.Addresses.First(); } -} \ No newline at end of file +} diff --git a/src/Infrastructure/Services/WebProxyWrapper.cs b/src/Infrastructure/Services/WebProxyWrapper.cs index 6c9b7003..91cb0142 100644 --- a/src/Infrastructure/Services/WebProxyWrapper.cs +++ b/src/Infrastructure/Services/WebProxyWrapper.cs @@ -1,165 +1,164 @@ -namespace Sqlbi.Bravo.Infrastructure.Services +using System; +using System.Net; +using System.Net.Http; +using Sqlbi.Bravo.Infrastructure.Configuration; +using Sqlbi.Bravo.Infrastructure.Configuration.Settings; + +namespace Sqlbi.Bravo.Infrastructure.Services; + +/// +/// Implements a wrapper that supports the system proxy on the machine or a single manual configured proxy server +/// +internal sealed class WebProxyWrapper : IWebProxy { - using Sqlbi.Bravo.Infrastructure.Configuration; - using Sqlbi.Bravo.Infrastructure.Configuration.Settings; - using System; - using System.Net; - using System.Net.Http; - - /// - /// Implements a wrapper that supports the system proxy on the machine or a single manual configured proxy server - /// - internal sealed class WebProxyWrapper : IWebProxy - { - public static readonly WebProxyWrapper Current = new(); - - private static readonly object _proxyLock = new(); - private readonly IWebProxy _defaultProxy; - private IWebProxy? _systemProxy; - private IWebProxy? _customProxy; - private IWebProxy? _noProxy; + public static readonly WebProxyWrapper Current = new(); - private WebProxyWrapper() - { - // For .NET Core The initial value of the static property HttpClient.DefaultProxy represents the system proxy on the machine. - // DO NOT USE WebRequest.GetSystemWebProxy() or WebProxy.GetDefaultProxy() as these legacy methods return a proxy configured with the Internet Explorer settings - _defaultProxy = HttpClient.DefaultProxy; - } + private static readonly object _proxyLock = new(); + private readonly IWebProxy _defaultProxy; + private IWebProxy? _systemProxy; + private IWebProxy? _customProxy; + private IWebProxy? _noProxy; - public IWebProxy DefaultSystemProxy => _defaultProxy; + private WebProxyWrapper() + { + // For .NET Core The initial value of the static property HttpClient.DefaultProxy represents the system proxy on the machine. + // DO NOT USE WebRequest.GetSystemWebProxy() or WebProxy.GetDefaultProxy() as these legacy methods return a proxy configured with the Internet Explorer settings + _defaultProxy = HttpClient.DefaultProxy; + } - #region IWebProxy + public IWebProxy DefaultSystemProxy => _defaultProxy; - public ICredentials? Credentials - { - get - { - var webProxy = GetWebProxy(); - return webProxy?.Credentials; - } - set - { - var webProxy = GetWebProxy(); - if (webProxy is not null) - webProxy.Credentials = value; - } - } + #region IWebProxy - public Uri? GetProxy(Uri destination) + public ICredentials? Credentials + { + get { var webProxy = GetWebProxy(); - var proxyUri = webProxy?.GetProxy(destination); - - return proxyUri; + return webProxy?.Credentials; } - - public bool IsBypassed(Uri host) + set { var webProxy = GetWebProxy(); - var isBypassed = webProxy.IsBypassed(host); - - return isBypassed; + if (webProxy is not null) + webProxy.Credentials = value; } + } - #endregion + public Uri? GetProxy(Uri destination) + { + var webProxy = GetWebProxy(); + var proxyUri = webProxy?.GetProxy(destination); + + return proxyUri; + } + + public bool IsBypassed(Uri host) + { + var webProxy = GetWebProxy(); + var isBypassed = webProxy.IsBypassed(host); + + return isBypassed; + } + + #endregion - private IWebProxy GetWebProxy() + private IWebProxy GetWebProxy() + { + var proxy = UserPreferences.Current.Proxy; + if (proxy?.Type == ProxyType.None) { - var proxy = UserPreferences.Current.Proxy; - if (proxy?.Type == ProxyType.None) + if (_noProxy is null) { - if (_noProxy is null) + lock (_proxyLock) { - lock (_proxyLock) + if (_noProxy is null) { - if (_noProxy is null) - { - _noProxy = new HttpNoProxy(); - } + _noProxy = new HttpNoProxy(); } } - - return _noProxy; } - else if (proxy?.Type == ProxyType.Custom) + + return _noProxy; + } + else if (proxy?.Type == ProxyType.Custom) + { + if (_customProxy is null) { - if (_customProxy is null) + lock (_proxyLock) { - lock (_proxyLock) + if (_customProxy is null) { - if (_customProxy is null) - { - var credentials = proxy.GetCredentials(); - var bypassList = ProxySettings.GetSafeBypassList(proxy.BypassList, includeLoopback: false); + var credentials = proxy.GetCredentials(); + var bypassList = ProxySettings.GetSafeBypassList(proxy.BypassList, includeLoopback: false); - _customProxy = new WebProxy(proxy.Address, proxy.BypassOnLocal, bypassList, credentials); - } + _customProxy = new WebProxy(proxy.Address, proxy.BypassOnLocal, bypassList, credentials); } } - - return _customProxy; } - else + + return _customProxy; + } + else + { + if (_systemProxy is null) { - if (_systemProxy is null) + lock (_proxyLock) { - lock (_proxyLock) + if (_systemProxy is null) { - if (_systemProxy is null) - { - _systemProxy = new HttpSystemProxy(_defaultProxy); + _systemProxy = new HttpSystemProxy(_defaultProxy); - if (proxy?.UseDefaultCredentials == false) + if (proxy?.UseDefaultCredentials == false) + { + var credentials = proxy.GetCredentials(); + if (credentials is not null) { - var credentials = proxy.GetCredentials(); - if (credentials is not null) - { - _systemProxy.Credentials = credentials; - } + _systemProxy.Credentials = credentials; } } } } - - return _systemProxy; } + + return _systemProxy; } } +} - internal sealed class HttpSystemProxy : IWebProxy - { - private readonly IWebProxy _defaultProxy; - - public HttpSystemProxy(IWebProxy defaultProxy) - { - _defaultProxy = defaultProxy; - } +internal sealed class HttpSystemProxy : IWebProxy +{ + private readonly IWebProxy _defaultProxy; - public ICredentials? Credentials - { - get => _defaultProxy.Credentials; - set => _defaultProxy.Credentials = value; - } + public HttpSystemProxy(IWebProxy defaultProxy) + { + _defaultProxy = defaultProxy; + } - public Uri? GetProxy(Uri destination) - { - var proxyUri = _defaultProxy.GetProxy(destination); - return proxyUri; - } + public ICredentials? Credentials + { + get => _defaultProxy.Credentials; + set => _defaultProxy.Credentials = value; + } - public bool IsBypassed(Uri host) - { - var isBypassed = _defaultProxy.IsBypassed(host); - return isBypassed; - } + public Uri? GetProxy(Uri destination) + { + var proxyUri = _defaultProxy.GetProxy(destination); + return proxyUri; } - internal sealed class HttpNoProxy : IWebProxy + public bool IsBypassed(Uri host) { - public ICredentials? Credentials { get; set; } + var isBypassed = _defaultProxy.IsBypassed(host); + return isBypassed; + } +} - public Uri? GetProxy(Uri destination) => null; +internal sealed class HttpNoProxy : IWebProxy +{ + public ICredentials? Credentials { get; set; } - public bool IsBypassed(Uri host) => true; // always bypassed - } + public Uri? GetProxy(Uri destination) => null; + + public bool IsBypassed(Uri host) => true; // always bypassed } diff --git a/src/Infrastructure/Services/WebView2ProxyAuthHandler.cs b/src/Infrastructure/Services/WebView2ProxyAuthHandler.cs index 13461bee..dbe05c35 100644 --- a/src/Infrastructure/Services/WebView2ProxyAuthHandler.cs +++ b/src/Infrastructure/Services/WebView2ProxyAuthHandler.cs @@ -1,11 +1,13 @@ -namespace Sqlbi.Bravo.Infrastructure.Services; - +using System; +using System.Net; using Microsoft.Web.WebView2.Core; using Sqlbi.Bravo.Infrastructure.Configuration; using Sqlbi.Bravo.Infrastructure.Configuration.Settings; using Sqlbi.Bravo.Infrastructure.Security; using Sqlbi.Bravo.Infrastructure.Telemetry; +namespace Sqlbi.Bravo.Infrastructure.Services; + /// /// Handles WebView2 proxy authentication challenges (HTTP 407). /// diff --git a/src/Infrastructure/Telemetry/DefaultTelemetryInitializer.cs b/src/Infrastructure/Telemetry/DefaultTelemetryInitializer.cs index 6af3d84b..aa6bf15a 100644 --- a/src/Infrastructure/Telemetry/DefaultTelemetryInitializer.cs +++ b/src/Infrastructure/Telemetry/DefaultTelemetryInitializer.cs @@ -1,8 +1,8 @@ -namespace Sqlbi.Bravo.Infrastructure.Telemetry; - -using Microsoft.ApplicationInsights.Channel; +using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.Extensibility; +namespace Sqlbi.Bravo.Infrastructure.Telemetry; + internal sealed class DefaultTelemetryInitializer : ITelemetryInitializer { public void Initialize(ITelemetry telemetry) diff --git a/src/Infrastructure/Telemetry/DefaultTelemetryProcessor.cs b/src/Infrastructure/Telemetry/DefaultTelemetryProcessor.cs index a859acb8..8910e7c6 100644 --- a/src/Infrastructure/Telemetry/DefaultTelemetryProcessor.cs +++ b/src/Infrastructure/Telemetry/DefaultTelemetryProcessor.cs @@ -1,9 +1,9 @@ -namespace Sqlbi.Bravo.Infrastructure.Telemetry; - +using System.Text.RegularExpressions; using Microsoft.ApplicationInsights.Channel; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; -using System.Text.RegularExpressions; + +namespace Sqlbi.Bravo.Infrastructure.Telemetry; internal sealed class DefaultTelemetryProcessor : ITelemetryProcessor { diff --git a/src/Infrastructure/Telemetry/TelemetryService.cs b/src/Infrastructure/Telemetry/TelemetryService.cs index 26bd7923..23d5031f 100644 --- a/src/Infrastructure/Telemetry/TelemetryService.cs +++ b/src/Infrastructure/Telemetry/TelemetryService.cs @@ -1,10 +1,12 @@ -namespace Sqlbi.Bravo.Infrastructure.Telemetry; - +using System; +using System.Diagnostics; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; using Sqlbi.Bravo.Infrastructure.Configuration; using Sqlbi.Bravo.Infrastructure.Policies; +namespace Sqlbi.Bravo.Infrastructure.Telemetry; + public interface ITelemetryService : IDisposable { bool TelemetryEnabled { get; set; } @@ -59,7 +61,7 @@ public bool TelemetryEnabled set { var telemetryEnabled = _policies.TelemetryEnabled ?? value; - _configuration.DisableTelemetry = !telemetryEnabled; + _configuration.DisableTelemetry = !telemetryEnabled; } } diff --git a/src/Infrastructure/Telemetry/TelemetrySessionInfo.cs b/src/Infrastructure/Telemetry/TelemetrySessionInfo.cs index 4c609562..f6b9ce4c 100644 --- a/src/Infrastructure/Telemetry/TelemetrySessionInfo.cs +++ b/src/Infrastructure/Telemetry/TelemetrySessionInfo.cs @@ -1,7 +1,9 @@ -namespace Sqlbi.Bravo.Infrastructure.Telemetry; - +using System; +using System.Collections.Generic; using Sqlbi.Bravo.Infrastructure.Security; +namespace Sqlbi.Bravo.Infrastructure.Telemetry; + internal static class TelemetrySessionInfo { /// See diff --git a/src/Infrastructure/Windows/Interop/Advapi32.cs b/src/Infrastructure/Windows/Interop/Advapi32.cs index 8b7c165f..23628bbc 100644 --- a/src/Infrastructure/Windows/Interop/Advapi32.cs +++ b/src/Infrastructure/Windows/Interop/Advapi32.cs @@ -1,91 +1,90 @@ -namespace Sqlbi.Bravo.Infrastructure.Windows.Interop +using System; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace Sqlbi.Bravo.Infrastructure.Windows.Interop; + +internal static class Advapi32 { - using Microsoft.Win32.SafeHandles; - using System; - using System.Runtime.InteropServices; + /// + /// https://docs.microsoft.com/en-us/windows/win32/api/wincred/ns-wincred-credentialw + /// + public const uint CRED_MAX_STRING_LENGTH = 256; + public const uint CRED_MAX_USERNAME_LENGTH = 513; + public const uint CRED_MAX_GENERIC_TARGET_NAME_LENGTH = 32767; + public const uint CRED_MAX_CREDENTIAL_BLOB_SIZE = 5 * 512; - internal static class Advapi32 + /// + /// https://docs.microsoft.com/en-us/windows/win32/api/wincred/ns-wincred-credentiala + /// + public enum CREDENTIAL_TYPE { - /// - /// https://docs.microsoft.com/en-us/windows/win32/api/wincred/ns-wincred-credentialw - /// - public const uint CRED_MAX_STRING_LENGTH = 256; - public const uint CRED_MAX_USERNAME_LENGTH = 513; - public const uint CRED_MAX_GENERIC_TARGET_NAME_LENGTH = 32767; - public const uint CRED_MAX_CREDENTIAL_BLOB_SIZE = 5 * 512; + CRED_TYPE_GENERIC = 1, + CRED_TYPE_DOMAIN_PASSWORD = 2, + CRED_TYPE_DOMAIN_CERTIFICATE = 3, + CRED_TYPE_DOMAIN_VISIBLE_PASSWORD = 4, + CRED_TYPE_GENERIC_CERTIFICATE = 5, + CRED_TYPE_DOMAIN_EXTENDED = 6, + CRED_TYPE_MAXIMUM = 7, + CRED_TYPE_MAXIMUM_EX = CRED_TYPE_MAXIMUM + 1000, + } - /// - /// https://docs.microsoft.com/en-us/windows/win32/api/wincred/ns-wincred-credentiala - /// - public enum CREDENTIAL_TYPE - { - CRED_TYPE_GENERIC = 1, - CRED_TYPE_DOMAIN_PASSWORD = 2, - CRED_TYPE_DOMAIN_CERTIFICATE = 3, - CRED_TYPE_DOMAIN_VISIBLE_PASSWORD = 4, - CRED_TYPE_GENERIC_CERTIFICATE = 5, - CRED_TYPE_DOMAIN_EXTENDED = 6, - CRED_TYPE_MAXIMUM = 7, - CRED_TYPE_MAXIMUM_EX = CRED_TYPE_MAXIMUM + 1000, - } + public enum CRED_PERSIST : uint + { + CRED_PERSIST_SESSION = 1, + CRED_PERSIST_LOCAL_MACHINE = 2, + CRED_PERSIST_ENTERPRISE = 3, + } - public enum CRED_PERSIST : uint - { - CRED_PERSIST_SESSION = 1, - CRED_PERSIST_LOCAL_MACHINE = 2, - CRED_PERSIST_ENTERPRISE = 3, - } + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct CREDENTIAL + { + public uint Flags; + public CREDENTIAL_TYPE Type; + public IntPtr TargetName; + public IntPtr Comment; + public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten; + public uint CredentialBlobSize; + public IntPtr CredentialBlob; + public CRED_PERSIST Persist; + public uint AttributeCount; + public IntPtr Attributes; + public IntPtr TargetAlias; + public IntPtr UserName; + } - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct CREDENTIAL - { - public uint Flags; - public CREDENTIAL_TYPE Type; - public IntPtr TargetName; - public IntPtr Comment; - public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten; - public uint CredentialBlobSize; - public IntPtr CredentialBlob; - public CRED_PERSIST Persist; - public uint AttributeCount; - public IntPtr Attributes; - public IntPtr TargetAlias; - public IntPtr UserName; - } + [DllImport(ExternDll.Advapi32, CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool CredReadW(string targetName, CREDENTIAL_TYPE type, int flags, out CredentialSafeHandle handle); - [DllImport(ExternDll.Advapi32, CharSet = CharSet.Unicode, SetLastError = true)] - public static extern bool CredReadW(string targetName, CREDENTIAL_TYPE type, int flags, out CredentialSafeHandle handle); + [DllImport(ExternDll.Advapi32, CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool CredWriteW([In] ref CREDENTIAL credential, [In] uint flags); - [DllImport(ExternDll.Advapi32, CharSet = CharSet.Unicode, SetLastError = true)] - public static extern bool CredWriteW([In] ref CREDENTIAL credential, [In] uint flags); + [DllImport(ExternDll.Advapi32, CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool CredDeleteW(string targetName, CREDENTIAL_TYPE type, int flags); - [DllImport(ExternDll.Advapi32, CharSet = CharSet.Unicode, SetLastError = true)] - public static extern bool CredDeleteW(string targetName, CREDENTIAL_TYPE type, int flags); + [DllImport(ExternDll.Advapi32, SetLastError = true)] + public static extern bool CredFree([In] IntPtr buffer); +} - [DllImport(ExternDll.Advapi32, SetLastError = true)] - public static extern bool CredFree([In] IntPtr buffer); +internal sealed class CredentialSafeHandle : SafeHandleZeroOrMinusOneIsInvalid +{ + public CredentialSafeHandle() + : base(ownsHandle: true) + { } - internal sealed class CredentialSafeHandle : SafeHandleZeroOrMinusOneIsInvalid + public Advapi32.CREDENTIAL GetCredential() { - public CredentialSafeHandle() - : base(ownsHandle: true) - { - } + if (IsInvalid) + throw new InvalidOperationException("Invalid handle"); - public Advapi32.CREDENTIAL GetCredential() - { - if (IsInvalid) - throw new InvalidOperationException("Invalid handle"); - - var credential = Marshal.PtrToStructure(handle); - return credential; - } + var credential = Marshal.PtrToStructure(handle); + return credential; + } - protected override bool ReleaseHandle() - { - var released = Advapi32.CredFree(handle); - return released; - } + protected override bool ReleaseHandle() + { + var released = Advapi32.CredFree(handle); + return released; } } diff --git a/src/Infrastructure/Windows/Interop/COLORREF.cs b/src/Infrastructure/Windows/Interop/COLORREF.cs index 004d1b19..d20f5040 100644 --- a/src/Infrastructure/Windows/Interop/COLORREF.cs +++ b/src/Infrastructure/Windows/Interop/COLORREF.cs @@ -1,69 +1,68 @@ -namespace Sqlbi.Bravo.Infrastructure.Windows.Interop +using System.Drawing; +using System.Runtime.InteropServices; + +namespace Sqlbi.Bravo.Infrastructure.Windows.Interop; + +[StructLayout(LayoutKind.Explicit, Size = 4)] +internal struct COLORREF { - using System.Drawing; - using System.Runtime.InteropServices; + [FieldOffset(0)] + private readonly uint Value; - [StructLayout(LayoutKind.Explicit, Size = 4)] - internal struct COLORREF - { - [FieldOffset(0)] - private readonly uint Value; + [FieldOffset(0)] + public byte R; - [FieldOffset(0)] - public byte R; + [FieldOffset(1)] + public byte G; - [FieldOffset(1)] - public byte G; + [FieldOffset(2)] + public byte B; - [FieldOffset(2)] - public byte B; + //private const uint CLR_NONE = uint.MaxValue; - //private const uint CLR_NONE = uint.MaxValue; + //private const uint CLR_DEFAULT = 4278190080u; - //private const uint CLR_DEFAULT = 4278190080u; + //public static COLORREF None = new(uint.MaxValue); - //public static COLORREF None = new(uint.MaxValue); + //public static COLORREF Default = new(4278190080u); - //public static COLORREF Default = new(4278190080u); + public COLORREF(byte r, byte g, byte b) + { + Value = 0u; + R = r; + G = g; + B = b; + } - public COLORREF(byte r, byte g, byte b) - { - Value = 0u; - R = r; - G = g; - B = b; - } + public COLORREF(uint value) + { + R = 0; + G = 0; + B = 0; + Value = (value & 0xFFFFFF); + } - public COLORREF(uint value) + public COLORREF(Color color) + : this(color.R, color.G, color.B) + { + if (color == Color.Transparent) { - R = 0; - G = 0; - B = 0; - Value = (value & 0xFFFFFF); + Value = uint.MaxValue; } + } - public COLORREF(Color color) - : this(color.R, color.G, color.B) + public static implicit operator Color(COLORREF colorRef) + { + if (colorRef.Value != uint.MaxValue) { - if (color == Color.Transparent) - { - Value = uint.MaxValue; - } + return Color.FromArgb(colorRef.R, colorRef.G, colorRef.B); } - public static implicit operator Color(COLORREF colorRef) - { - if (colorRef.Value != uint.MaxValue) - { - return Color.FromArgb(colorRef.R, colorRef.G, colorRef.B); - } - - return Color.Transparent; - } + return Color.Transparent; + } - public static implicit operator COLORREF(Color color) - { - return new COLORREF(color); - } + public static implicit operator COLORREF(Color color) + { + return new COLORREF(color); } } diff --git a/src/Infrastructure/Windows/Interop/Comctl32.cs b/src/Infrastructure/Windows/Interop/Comctl32.cs index 7d71a24b..59f2f84a 100644 --- a/src/Infrastructure/Windows/Interop/Comctl32.cs +++ b/src/Infrastructure/Windows/Interop/Comctl32.cs @@ -1,22 +1,21 @@ -namespace Sqlbi.Bravo.Infrastructure.Windows.Interop -{ - using System; - using System.Runtime.InteropServices; +using System; +using System.Runtime.InteropServices; + +namespace Sqlbi.Bravo.Infrastructure.Windows.Interop; - internal static class Comctl32 - { - public delegate IntPtr SUBCLASSPROC(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam, IntPtr uIdSubclass, IntPtr dwRefData); +internal static class Comctl32 +{ + public delegate IntPtr SUBCLASSPROC(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam, IntPtr uIdSubclass, IntPtr dwRefData); - [DllImport(ExternDll.Comctl32, CharSet = CharSet.Auto, SetLastError = true)] - public static extern bool GetWindowSubclass(IntPtr hWnd, SUBCLASSPROC pfnSubclass, IntPtr uIdSubclass, ref IntPtr dwRefData); + [DllImport(ExternDll.Comctl32, CharSet = CharSet.Auto, SetLastError = true)] + public static extern bool GetWindowSubclass(IntPtr hWnd, SUBCLASSPROC pfnSubclass, IntPtr uIdSubclass, ref IntPtr dwRefData); - [DllImport(ExternDll.Comctl32, CharSet = CharSet.Auto, SetLastError = true)] - public static extern bool SetWindowSubclass(IntPtr hWnd, SUBCLASSPROC pfnSubclass, IntPtr uIdSubclass, IntPtr dwRefData); + [DllImport(ExternDll.Comctl32, CharSet = CharSet.Auto, SetLastError = true)] + public static extern bool SetWindowSubclass(IntPtr hWnd, SUBCLASSPROC pfnSubclass, IntPtr uIdSubclass, IntPtr dwRefData); - [DllImport(ExternDll.Comctl32, CharSet = CharSet.Auto, SetLastError = true)] - public static extern bool RemoveWindowSubclass(IntPtr hWnd, SUBCLASSPROC pfnSubclass, IntPtr uIdSubclass); + [DllImport(ExternDll.Comctl32, CharSet = CharSet.Auto, SetLastError = true)] + public static extern bool RemoveWindowSubclass(IntPtr hWnd, SUBCLASSPROC pfnSubclass, IntPtr uIdSubclass); - [DllImport(ExternDll.Comctl32, CharSet = CharSet.Auto, SetLastError = true)] - public static extern IntPtr DefSubclassProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam); - } + [DllImport(ExternDll.Comctl32, CharSet = CharSet.Auto, SetLastError = true)] + public static extern IntPtr DefSubclassProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam); } diff --git a/src/Infrastructure/Windows/Interop/Comdlg32.cs b/src/Infrastructure/Windows/Interop/Comdlg32.cs index 75611b5f..15330212 100644 --- a/src/Infrastructure/Windows/Interop/Comdlg32.cs +++ b/src/Infrastructure/Windows/Interop/Comdlg32.cs @@ -3,72 +3,71 @@ #nullable disable -namespace Sqlbi.Bravo.Infrastructure.Windows.Interop +namespace Sqlbi.Bravo.Infrastructure.Windows.Interop; + +internal static class Comdlg32 { - internal static class Comdlg32 + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public class OPENFILENAME { - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] - public class OPENFILENAME - { - public int lStructSize; - public IntPtr hwndOwner; - public IntPtr hInstance; - public string lpstrFilter; - public string lpstrCustomFilter; - public int nMaxCustFilter; - public int nFilterIndex; - public string lpstrFile; - public int nMaxFile; - public string lpstrFileTitle; - public int nMaxFileTitle; - public string lpstrInitialDir; - public string lpstrTitle; - public int Flags; - public short nFileOffset; - public short nFileExtension; - public string lpstrDefExt; - public IntPtr lCustData; - public IntPtr lpfnHook; - public string lpTemplateName; - public IntPtr pvReserved; - public int dwReserved; - public int flagsEx; - } + public int lStructSize; + public IntPtr hwndOwner; + public IntPtr hInstance; + public string lpstrFilter; + public string lpstrCustomFilter; + public int nMaxCustFilter; + public int nFilterIndex; + public string lpstrFile; + public int nMaxFile; + public string lpstrFileTitle; + public int nMaxFileTitle; + public string lpstrInitialDir; + public string lpstrTitle; + public int Flags; + public short nFileOffset; + public short nFileExtension; + public string lpstrDefExt; + public IntPtr lCustData; + public IntPtr lpfnHook; + public string lpTemplateName; + public IntPtr pvReserved; + public int dwReserved; + public int flagsEx; + } - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] - public class CHOOSECOLOR - { - public int lStructSize; - public IntPtr hwndOwner; - public IntPtr hInstance; - public int rgbResult; - public IntPtr lpCustColors; - public int Flags; - public IntPtr lCustData; - public IntPtr lpfnHook; - public string lpTemplateName; - } + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] + public class CHOOSECOLOR + { + public int lStructSize; + public IntPtr hwndOwner; + public IntPtr hInstance; + public int rgbResult; + public IntPtr lpCustColors; + public int Flags; + public IntPtr lCustData; + public IntPtr lpfnHook; + public string lpTemplateName; + } - public static class CHOOSECOLORFLAGS - { - public const int CC_RGBINIT = 0x00000001; - public const int CC_FULLOPEN = 0x00000002; - public const int CC_PREVENTFULLOPEN = 0x00000004; - public const int CC_SHOWHELP = 0x00000008; - public const int CC_ENABLEHOOK = 0x00000010; - public const int CC_ENABLETEMPLATE = 0x00000020; - public const int CC_ENABLETEMPLATEHANDLE = 0x00000040; - public const int CC_SOLIDCOLOR = 0x00000080; - public const int CC_ANYCOLOR = 0x00000100; - } + public static class CHOOSECOLORFLAGS + { + public const int CC_RGBINIT = 0x00000001; + public const int CC_FULLOPEN = 0x00000002; + public const int CC_PREVENTFULLOPEN = 0x00000004; + public const int CC_SHOWHELP = 0x00000008; + public const int CC_ENABLEHOOK = 0x00000010; + public const int CC_ENABLETEMPLATE = 0x00000020; + public const int CC_ENABLETEMPLATEHANDLE = 0x00000040; + public const int CC_SOLIDCOLOR = 0x00000080; + public const int CC_ANYCOLOR = 0x00000100; + } - [DllImport(ExternDll.Comdlg32, CharSet = CharSet.Auto)] - public static extern bool GetOpenFileName([In, Out] OPENFILENAME ofn); + [DllImport(ExternDll.Comdlg32, CharSet = CharSet.Auto)] + public static extern bool GetOpenFileName([In, Out] OPENFILENAME ofn); - [DllImport(ExternDll.Comdlg32, CharSet = CharSet.Auto)] - public static extern bool GetSaveFileName([In, Out] OPENFILENAME ofn); + [DllImport(ExternDll.Comdlg32, CharSet = CharSet.Auto)] + public static extern bool GetSaveFileName([In, Out] OPENFILENAME ofn); - [DllImport(ExternDll.Comdlg32, CharSet = CharSet.Auto)] - public static extern bool ChooseColor([In, Out] CHOOSECOLOR cc); - } + [DllImport(ExternDll.Comdlg32, CharSet = CharSet.Auto)] + public static extern bool ChooseColor([In, Out] CHOOSECOLOR cc); } diff --git a/src/Infrastructure/Windows/Interop/Credui.cs b/src/Infrastructure/Windows/Interop/Credui.cs index 0d791f3b..a3405cd0 100644 --- a/src/Infrastructure/Windows/Interop/Credui.cs +++ b/src/Infrastructure/Windows/Interop/Credui.cs @@ -1,56 +1,55 @@ -namespace Sqlbi.Bravo.Infrastructure.Windows.Interop +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Sqlbi.Bravo.Infrastructure.Windows.Interop; + +internal static class Credui { - using System; - using System.Runtime.InteropServices; - using System.Text; + public const int CREDUI_MAX_MESSAGE_LENGTH = 32767; + public const int CREDUI_MAX_CAPTION_LENGTH = 128; + public const int CREDUI_MAX_DOMAIN_LENGTH = 256; + public const int CREDUI_MAX_USERNAME_LENGTH = 256; + public const int CREDUI_MAX_PASSWORD_LENGTH = (512 / 2); + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct CREDUI_INFO + { + public int cbSize; + public IntPtr hwndParent; + public string? pszMessageText; + public string? pszCaptionText; + public IntPtr hbmBanner; + } - internal static class Credui + /// + /// https://docs.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-creduipromptforwindowscredentialsa + /// + [Flags] + public enum CREDUIWIN : uint { - public const int CREDUI_MAX_MESSAGE_LENGTH = 32767; - public const int CREDUI_MAX_CAPTION_LENGTH = 128; - public const int CREDUI_MAX_DOMAIN_LENGTH = 256; - public const int CREDUI_MAX_USERNAME_LENGTH = 256; - public const int CREDUI_MAX_PASSWORD_LENGTH = (512 / 2); - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct CREDUI_INFO - { - public int cbSize; - public IntPtr hwndParent; - public string? pszMessageText; - public string? pszCaptionText; - public IntPtr hbmBanner; - } - - /// - /// https://docs.microsoft.com/en-us/windows/win32/api/wincred/nf-wincred-creduipromptforwindowscredentialsa - /// - [Flags] - public enum CREDUIWIN : uint - { - CREDUIWIN_GENERIC = 0x1, - CREDUIWIN_CHECKBOX = 0x2, - CREDUIWIN_AUTHPACKAGE_ONLY = 0x10, - CREDUIWIN_IN_CRED_ONLY = 0x20, - CREDUIWIN_ENUMERATE_ADMINS = 0x100, - CREDUIWIN_ENUMERATE_CURRENT_USER = 0x200, - CREDUIWIN_SECURE_PROMPT = 0x1000, - CREDUIWIN_PREPROMPTING = 0x2000, - // 0x40000 - CREDUIWIN_PACK_32_WOW = 0x10000000, - // 0x80000000 - } - - [DllImport(ExternDll.Credui, CharSet = CharSet.Unicode, SetLastError = true)] - public static extern uint CredUIParseUserNameW(string userName, StringBuilder user, int userBufferSize, StringBuilder domain, int domainBufferSize); - - [DllImport(ExternDll.Credui, CharSet = CharSet.Unicode, SetLastError = true)] - public static extern bool CredPackAuthenticationBuffer(int dwFlags, IntPtr pszUserName, IntPtr pszPassword, IntPtr pPackedCredentials, ref int pcbPackedCredentials); - - [DllImport(ExternDll.Credui, CharSet = CharSet.Unicode, ExactSpelling = true)] - public static extern bool CredUnPackAuthenticationBufferW(int dwFlags, IntPtr pAuthBuffer, int cbAuthBuffer, StringBuilder pszUserName, ref int pcchMaxUserName, StringBuilder pszDomainName, ref int pcchMaxDomainame, StringBuilder pszPassword, ref int pcchMaxPassword); - - [DllImport(ExternDll.Credui, CharSet = CharSet.Unicode, SetLastError = true)] - public static extern int CredUIPromptForWindowsCredentialsW(ref CREDUI_INFO pUiInfo, int dwAuthError, ref int pulAuthPackage, IntPtr pvInAuthBuffer, int ulInAuthBufferSize, out IntPtr ppvOutAuthBuffer, out int pulOutAuthBufferSize, ref bool pfSave, CREDUIWIN dwFlags); + CREDUIWIN_GENERIC = 0x1, + CREDUIWIN_CHECKBOX = 0x2, + CREDUIWIN_AUTHPACKAGE_ONLY = 0x10, + CREDUIWIN_IN_CRED_ONLY = 0x20, + CREDUIWIN_ENUMERATE_ADMINS = 0x100, + CREDUIWIN_ENUMERATE_CURRENT_USER = 0x200, + CREDUIWIN_SECURE_PROMPT = 0x1000, + CREDUIWIN_PREPROMPTING = 0x2000, + // 0x40000 + CREDUIWIN_PACK_32_WOW = 0x10000000, + // 0x80000000 } + + [DllImport(ExternDll.Credui, CharSet = CharSet.Unicode, SetLastError = true)] + public static extern uint CredUIParseUserNameW(string userName, StringBuilder user, int userBufferSize, StringBuilder domain, int domainBufferSize); + + [DllImport(ExternDll.Credui, CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool CredPackAuthenticationBuffer(int dwFlags, IntPtr pszUserName, IntPtr pszPassword, IntPtr pPackedCredentials, ref int pcbPackedCredentials); + + [DllImport(ExternDll.Credui, CharSet = CharSet.Unicode, ExactSpelling = true)] + public static extern bool CredUnPackAuthenticationBufferW(int dwFlags, IntPtr pAuthBuffer, int cbAuthBuffer, StringBuilder pszUserName, ref int pcchMaxUserName, StringBuilder pszDomainName, ref int pcchMaxDomainame, StringBuilder pszPassword, ref int pcchMaxPassword); + + [DllImport(ExternDll.Credui, CharSet = CharSet.Unicode, SetLastError = true)] + public static extern int CredUIPromptForWindowsCredentialsW(ref CREDUI_INFO pUiInfo, int dwAuthError, ref int pulAuthPackage, IntPtr pvInAuthBuffer, int ulInAuthBufferSize, out IntPtr ppvOutAuthBuffer, out int pulOutAuthBufferSize, ref bool pfSave, CREDUIWIN dwFlags); } diff --git a/src/Infrastructure/Windows/Interop/Dwmapi.cs b/src/Infrastructure/Windows/Interop/Dwmapi.cs index df4c787e..26ca739d 100644 --- a/src/Infrastructure/Windows/Interop/Dwmapi.cs +++ b/src/Infrastructure/Windows/Interop/Dwmapi.cs @@ -1,39 +1,38 @@ -namespace Sqlbi.Bravo.Infrastructure.Windows.Interop -{ - using System; - using System.Runtime.InteropServices; +using System; +using System.Runtime.InteropServices; - internal static class Dwmapi - { - public enum DWMWINDOWATTRIBUTE - { - DWMWA_NCRENDERING_ENABLED = 1, - DWMWA_NCRENDERING_POLICY, - DWMWA_TRANSITIONS_FORCEDISABLED, - DWMWA_ALLOW_NCPAINT, - DWMWA_CAPTION_BUTTON_BOUNDS, - DWMWA_NONCLIENT_RTL_LAYOUT, - DWMWA_FORCE_ICONIC_REPRESENTATION, - DWMWA_FLIP3D_POLICY, - DWMWA_EXTENDED_FRAME_BOUNDS, - DWMWA_HAS_ICONIC_BITMAP, - DWMWA_DISALLOW_PEEK, - DWMWA_EXCLUDED_FROM_PEEK, - DWMWA_CLOAK, - DWMWA_CLOAKED, - DWMWA_FREEZE_REPRESENTATION, - DWMWA_PASSIVE_UPDATE_MODE, - DWMWA_USE_HOSTBACKDROPBRUSH, - DWMWA_USE_IMMERSIVE_DARK_MODE, - DWMWA_WINDOW_CORNER_PREFERENCE, - DWMWA_BORDER_COLOR, - DWMWA_CAPTION_COLOR, - DWMWA_TEXT_COLOR, - DWMWA_VISIBLE_FRAME_BORDER_THICKNESS, - DWMWA_LAST - } +namespace Sqlbi.Bravo.Infrastructure.Windows.Interop; - [DllImport(ExternDll.Dwmapi, ExactSpelling = true)] - public static extern int DwmSetWindowAttribute(IntPtr hWnd, DWMWINDOWATTRIBUTE dwAttribute, [In] IntPtr pvAttribute, int cbAttribute); +internal static class Dwmapi +{ + public enum DWMWINDOWATTRIBUTE + { + DWMWA_NCRENDERING_ENABLED = 1, + DWMWA_NCRENDERING_POLICY, + DWMWA_TRANSITIONS_FORCEDISABLED, + DWMWA_ALLOW_NCPAINT, + DWMWA_CAPTION_BUTTON_BOUNDS, + DWMWA_NONCLIENT_RTL_LAYOUT, + DWMWA_FORCE_ICONIC_REPRESENTATION, + DWMWA_FLIP3D_POLICY, + DWMWA_EXTENDED_FRAME_BOUNDS, + DWMWA_HAS_ICONIC_BITMAP, + DWMWA_DISALLOW_PEEK, + DWMWA_EXCLUDED_FROM_PEEK, + DWMWA_CLOAK, + DWMWA_CLOAKED, + DWMWA_FREEZE_REPRESENTATION, + DWMWA_PASSIVE_UPDATE_MODE, + DWMWA_USE_HOSTBACKDROPBRUSH, + DWMWA_USE_IMMERSIVE_DARK_MODE, + DWMWA_WINDOW_CORNER_PREFERENCE, + DWMWA_BORDER_COLOR, + DWMWA_CAPTION_COLOR, + DWMWA_TEXT_COLOR, + DWMWA_VISIBLE_FRAME_BORDER_THICKNESS, + DWMWA_LAST } + + [DllImport(ExternDll.Dwmapi, ExactSpelling = true)] + public static extern int DwmSetWindowAttribute(IntPtr hWnd, DWMWINDOWATTRIBUTE dwAttribute, [In] IntPtr pvAttribute, int cbAttribute); } diff --git a/src/Infrastructure/Windows/Interop/ExternDll.cs b/src/Infrastructure/Windows/Interop/ExternDll.cs index 8b9390d1..8a54e318 100644 --- a/src/Infrastructure/Windows/Interop/ExternDll.cs +++ b/src/Infrastructure/Windows/Interop/ExternDll.cs @@ -1,89 +1,88 @@ -namespace Sqlbi.Bravo.Infrastructure.Windows.Interop +namespace Sqlbi.Bravo.Infrastructure.Windows.Interop; + +internal static class ExternDll { - internal static class ExternDll - { - //public const string Activeds = "activeds.dll"; + //public const string Activeds = "activeds.dll"; - public const string Advapi32 = "advapi32.dll"; + public const string Advapi32 = "advapi32.dll"; - public const string Comctl32 = "comctl32.dll"; + public const string Comctl32 = "comctl32.dll"; - public const string Comdlg32 = "comdlg32.dll"; + public const string Comdlg32 = "comdlg32.dll"; - public const string Credui = "credui.dll"; + public const string Credui = "credui.dll"; - public const string Dwmapi = "dwmapi.dll"; + public const string Dwmapi = "dwmapi.dll"; - //public const string Gdi32 = "gdi32.dll"; + //public const string Gdi32 = "gdi32.dll"; - public const string Gdiplus = "gdiplus.dll"; + public const string Gdiplus = "gdiplus.dll"; - //public const string Hhctrl = "hhctrl.ocx"; + //public const string Hhctrl = "hhctrl.ocx"; - //public const string Imm32 = "imm32.dll"; + //public const string Imm32 = "imm32.dll"; - public const string Iphlpapi = "iphlpapi.dll"; + public const string Iphlpapi = "iphlpapi.dll"; - public const string Kernel32 = "kernel32.dll"; + public const string Kernel32 = "kernel32.dll"; - //public const string Loadperf = "Loadperf.dll"; + //public const string Loadperf = "Loadperf.dll"; - //public const string Mqrt = "mqrt.dll"; + //public const string Mqrt = "mqrt.dll"; - //public const string Mscoree = "mscoree.dll"; + //public const string Mscoree = "mscoree.dll"; - //public const string MsDrm = "msdrm.dll"; + //public const string MsDrm = "msdrm.dll"; - //public const string Mshwgst = "mshwgst.dll"; + //public const string Mshwgst = "mshwgst.dll"; - //public const string Msi = "msi.dll"; + //public const string Msi = "msi.dll"; - //public const string NaturalLanguage6 = "naturallanguage6.dll"; + //public const string NaturalLanguage6 = "naturallanguage6.dll"; - public const string Ntdll = "ntdll.dll"; + public const string Ntdll = "ntdll.dll"; - //public const string Ole32 = "ole32.dll"; + //public const string Ole32 = "ole32.dll"; - //public const string Oleacc = "oleacc.dll"; + //public const string Oleacc = "oleacc.dll"; - //public const string Oleaut32 = "oleaut32.dll"; + //public const string Oleaut32 = "oleaut32.dll"; - //public const string Olepro32 = "olepro32.dll"; + //public const string Olepro32 = "olepro32.dll"; - //public const string Penimc = "penimc2_v0400.dll"; + //public const string Penimc = "penimc2_v0400.dll"; - //public const string PresentationHostDll = "PresentationHost_v0400.dll"; + //public const string PresentationHostDll = "PresentationHost_v0400.dll"; - //public const string PresentationNativeDll = "PresentationNative_v0400.dll"; + //public const string PresentationNativeDll = "PresentationNative_v0400.dll"; - //public const string Psapi = "psapi.dll"; + //public const string Psapi = "psapi.dll"; - public const string Shcore = "shcore.dll"; + public const string Shcore = "shcore.dll"; - //public const string Shell32 = "shell32.dll"; + //public const string Shell32 = "shell32.dll"; - //public const string Shfolder = "shfolder.dll"; + //public const string Shfolder = "shfolder.dll"; - //public const string Urlmon = "urlmon.dll"; + //public const string Urlmon = "urlmon.dll"; - public const string User32 = "user32.dll"; + public const string User32 = "user32.dll"; - public const string Uxtheme = "uxtheme.dll"; + public const string Uxtheme = "uxtheme.dll"; - //public const string Version = "version.dll"; + //public const string Version = "version.dll"; - //public const string Vsassert = "vsassert.dll"; + //public const string Vsassert = "vsassert.dll"; - //public const string WebView2Loader = "WebView2Loader.dll"; + //public const string WebView2Loader = "WebView2Loader.dll"; - //public const string Wininet = "wininet.dll"; + //public const string Wininet = "wininet.dll"; - //public const string Winmm = "winmm.dll"; + //public const string Winmm = "winmm.dll"; - //public const string Winspool = "winspool.drv"; + //public const string Winspool = "winspool.drv"; - //public const string Wldp = "wldp.dll"; + //public const string Wldp = "wldp.dll"; - //public const string WtsApi32 = "wtsapi32.dll"; - } + //public const string WtsApi32 = "wtsapi32.dll"; } diff --git a/src/Infrastructure/Windows/Interop/HRESULT.cs b/src/Infrastructure/Windows/Interop/HRESULT.cs index a43ad364..78e07073 100644 --- a/src/Infrastructure/Windows/Interop/HRESULT.cs +++ b/src/Infrastructure/Windows/Interop/HRESULT.cs @@ -1,19 +1,18 @@ -namespace Sqlbi.Bravo.Infrastructure.Windows.Interop +namespace Sqlbi.Bravo.Infrastructure.Windows.Interop; + +/// +/// https://github.com/dahall/Vanara/blob/master/PInvoke/Shared/WinError/HRESULT.Values.cs +/// https://www.magnumdb.com/ +/// +internal class HRESULT { - /// - /// https://github.com/dahall/Vanara/blob/master/PInvoke/Shared/WinError/HRESULT.Values.cs - /// https://www.magnumdb.com/ - /// - internal class HRESULT - { - public const int S_OK = 0; + public const int S_OK = 0; - //public const int ERROR_FILE_NOT_FOUND = -2147024894; + //public const int ERROR_FILE_NOT_FOUND = -2147024894; - public const int ERROR_INVALID_DATA = unchecked((int)0x8007000D); + public const int ERROR_INVALID_DATA = unchecked((int)0x8007000D); - public const int E_NOINTERFACE = unchecked((int)0x80004002); + public const int E_NOINTERFACE = unchecked((int)0x80004002); - public const int NTE_BAD_KEY_STATE = unchecked((int)0x8009000B); - } + public const int NTE_BAD_KEY_STATE = unchecked((int)0x8009000B); } diff --git a/src/Infrastructure/Windows/Interop/Iphlpapi.cs b/src/Infrastructure/Windows/Interop/Iphlpapi.cs index 31a09007..a7aabb77 100644 --- a/src/Infrastructure/Windows/Interop/Iphlpapi.cs +++ b/src/Infrastructure/Windows/Interop/Iphlpapi.cs @@ -5,88 +5,87 @@ using System.Net.Sockets; using System.Runtime.InteropServices; -namespace Sqlbi.Bravo.Infrastructure.Windows.Interop +namespace Sqlbi.Bravo.Infrastructure.Windows.Interop; + +internal static class Iphlpapi { - internal static class Iphlpapi + public const uint AF_INET = (uint)AddressFamily.InterNetwork; + public const uint AF_INET6 = (uint)AddressFamily.InterNetworkV6; + + public enum TCP_TABLE_CLASS + { + TCP_TABLE_BASIC_LISTENER, + TCP_TABLE_BASIC_CONNECTIONS, + TCP_TABLE_BASIC_ALL, + TCP_TABLE_OWNER_PID_LISTENER, + TCP_TABLE_OWNER_PID_CONNECTIONS, + TCP_TABLE_OWNER_PID_ALL, + TCP_TABLE_OWNER_MODULE_LISTENER, + TCP_TABLE_OWNER_MODULE_CONNECTIONS, + TCP_TABLE_OWNER_MODULE_ALL + } + + [StructLayout(LayoutKind.Sequential)] + public struct MIB_TCPTABLE_OWNER_PID + { + public uint dwNumEntries; + MIB_TCPROW_OWNER_PID table; + } + + [StructLayout(LayoutKind.Sequential)] + public struct MIB_TCPROW_OWNER_PID { - public const uint AF_INET = (uint)AddressFamily.InterNetwork; - public const uint AF_INET6 = (uint)AddressFamily.InterNetworkV6; - - public enum TCP_TABLE_CLASS - { - TCP_TABLE_BASIC_LISTENER, - TCP_TABLE_BASIC_CONNECTIONS, - TCP_TABLE_BASIC_ALL, - TCP_TABLE_OWNER_PID_LISTENER, - TCP_TABLE_OWNER_PID_CONNECTIONS, - TCP_TABLE_OWNER_PID_ALL, - TCP_TABLE_OWNER_MODULE_LISTENER, - TCP_TABLE_OWNER_MODULE_CONNECTIONS, - TCP_TABLE_OWNER_MODULE_ALL - } - - [StructLayout(LayoutKind.Sequential)] - public struct MIB_TCPTABLE_OWNER_PID - { - public uint dwNumEntries; - MIB_TCPROW_OWNER_PID table; - } - - [StructLayout(LayoutKind.Sequential)] - public struct MIB_TCPROW_OWNER_PID - { - public uint state; - public uint localAddress; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] - public byte[] localPort; - public uint remoteAddress; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] - public byte[] remotePort; - public uint owningPid; - - public IPEndPoint LocalEndPoint => new(localAddress, port: BitConverter.ToUInt16(new byte[2] { localPort[1], localPort[0] }, 0)); - - public IPEndPoint RemoteEndPoint => new(remoteAddress, port: BitConverter.ToUInt16(new byte[2] { remotePort[1], remotePort[0] }, 0)); - - public TcpState TcpState => (state > 0 && state < 13) ? (TcpState)state : TcpState.Unknown; - - public int ProcessId => (int)owningPid; - } - - [StructLayout(LayoutKind.Sequential)] - public struct MIB_TCP6TABLE_OWNER_PID - { - public uint dwNumEntries; - [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.Struct, SizeConst = 1)] - public MIB_TCP6ROW_OWNER_PID[] table; - } - - [StructLayout(LayoutKind.Sequential)] - public struct MIB_TCP6ROW_OWNER_PID - { - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] - public byte[] localAddr; - public uint localScopeId; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] - public byte[] localPort; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] - public byte[] remoteAddr; - public uint remoteScopeId; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] - public byte[] remotePort; - public uint state; - public uint owningPid; - - public IPEndPoint LocalEndPoint => new(address: new IPAddress(localAddr, localScopeId), port: BitConverter.ToUInt16(localPort.Take(2).Reverse().ToArray(), 0)); - - public IPEndPoint RemoteEndPoint => new(address: new IPAddress(remoteAddr, remoteScopeId), port: BitConverter.ToUInt16(remotePort.Take(2).Reverse().ToArray(), 0)); - - public TcpState TcpState => (state > 0 && state < 13) ? (TcpState)state : TcpState.Unknown; - - public int ProcessId => (int)owningPid; - } - - [DllImport(ExternDll.Iphlpapi)] - public static extern uint GetExtendedTcpTable(IntPtr pTcpTable, ref uint dwOutBufLen, bool order, uint IPVersion, TCP_TABLE_CLASS tableClass, uint reserved = 0u); + public uint state; + public uint localAddress; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] localPort; + public uint remoteAddress; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] remotePort; + public uint owningPid; + + public IPEndPoint LocalEndPoint => new(localAddress, port: BitConverter.ToUInt16(new byte[2] { localPort[1], localPort[0] }, 0)); + + public IPEndPoint RemoteEndPoint => new(remoteAddress, port: BitConverter.ToUInt16(new byte[2] { remotePort[1], remotePort[0] }, 0)); + + public TcpState TcpState => (state > 0 && state < 13) ? (TcpState)state : TcpState.Unknown; + + public int ProcessId => (int)owningPid; } + + [StructLayout(LayoutKind.Sequential)] + public struct MIB_TCP6TABLE_OWNER_PID + { + public uint dwNumEntries; + [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.Struct, SizeConst = 1)] + public MIB_TCP6ROW_OWNER_PID[] table; + } + + [StructLayout(LayoutKind.Sequential)] + public struct MIB_TCP6ROW_OWNER_PID + { + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public byte[] localAddr; + public uint localScopeId; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] localPort; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)] + public byte[] remoteAddr; + public uint remoteScopeId; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] + public byte[] remotePort; + public uint state; + public uint owningPid; + + public IPEndPoint LocalEndPoint => new(address: new IPAddress(localAddr, localScopeId), port: BitConverter.ToUInt16(localPort.Take(2).Reverse().ToArray(), 0)); + + public IPEndPoint RemoteEndPoint => new(address: new IPAddress(remoteAddr, remoteScopeId), port: BitConverter.ToUInt16(remotePort.Take(2).Reverse().ToArray(), 0)); + + public TcpState TcpState => (state > 0 && state < 13) ? (TcpState)state : TcpState.Unknown; + + public int ProcessId => (int)owningPid; + } + + [DllImport(ExternDll.Iphlpapi)] + public static extern uint GetExtendedTcpTable(IntPtr pTcpTable, ref uint dwOutBufLen, bool order, uint IPVersion, TCP_TABLE_CLASS tableClass, uint reserved = 0u); } diff --git a/src/Infrastructure/Windows/Interop/Kernel32.cs b/src/Infrastructure/Windows/Interop/Kernel32.cs index 9a713cc5..8fcc714c 100644 --- a/src/Infrastructure/Windows/Interop/Kernel32.cs +++ b/src/Infrastructure/Windows/Interop/Kernel32.cs @@ -1,20 +1,19 @@ -namespace Sqlbi.Bravo.Infrastructure.Windows.Interop -{ - using System; - using System.Runtime.InteropServices; - using System.Text; +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Sqlbi.Bravo.Infrastructure.Windows.Interop; - internal static class Kernel32 - { - public delegate IntPtr SUBCLASSPROC(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam, IntPtr id, IntPtr data); +internal static class Kernel32 +{ + public delegate IntPtr SUBCLASSPROC(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam, IntPtr id, IntPtr data); - [DllImport(ExternDll.Kernel32, SetLastError = true)] - public static extern int GetCurrentProcessId(); + [DllImport(ExternDll.Kernel32, SetLastError = true)] + public static extern int GetCurrentProcessId(); - [DllImport(ExternDll.Kernel32, SetLastError = true)] - public static extern int GetCurrentThreadId(); + [DllImport(ExternDll.Kernel32, SetLastError = true)] + public static extern int GetCurrentThreadId(); - [DllImport(ExternDll.Kernel32, CharSet = CharSet.Unicode, SetLastError = true)] - public static extern int GetCurrentPackageFullName(ref int packageFullNameLength, StringBuilder packageFullName); - } + [DllImport(ExternDll.Kernel32, CharSet = CharSet.Unicode, SetLastError = true)] + public static extern int GetCurrentPackageFullName(ref int packageFullNameLength, StringBuilder packageFullName); } diff --git a/src/Infrastructure/Windows/Interop/NativeMethods.cs b/src/Infrastructure/Windows/Interop/NativeMethods.cs index cfc185a7..5387c819 100644 --- a/src/Infrastructure/Windows/Interop/NativeMethods.cs +++ b/src/Infrastructure/Windows/Interop/NativeMethods.cs @@ -1,111 +1,110 @@ -namespace Sqlbi.Bravo.Infrastructure.Windows.Interop +namespace Sqlbi.Bravo.Infrastructure.Windows.Interop; + +internal static class NativeMethods { - internal static class NativeMethods - { - public const int ERROR_SUCCESS = 0; - public const int ERROR_INVALID_PARAMETER = 87; - public const int ERROR_INSUFFICIENT_BUFFER = 122; - public const int ERROR_NO_DATA = 232; - public const int ERROR_INVALID_FLAGS = 1004; - public const int ERROR_NOT_FOUND = 1168; - public const int ERROR_CANCELLED = 1223; - public const int ERROR_NO_SUCH_LOGON_SESSION = 1312; - public const int ERROR_INVALID_ACCOUNT_NAME = 1315; - - //public const int SPI_GETWORKAREA = 48; - //public const int SM_CXSCREEN = 0; - //public const int SM_CYSCREEN = 1; - //public const int SM_XVIRTUALSCREEN = 76; - //public const int SM_YXVIRTUALSCREEN = 77; - //public const int SM_CXVIRTUALSCREEN = 78; - //public const int SM_CYXVIRTUALSCREEN = 79; - //public const int SM_CMONITORS = 80; - - //public delegate bool MonitorEnumProc(IntPtr monitor, IntPtr hdc, IntPtr lprcMonitor, IntPtr lParam); - - //public static readonly HandleRef NullHandleRef = new HandleRef(null, IntPtr.Zero); - - //[DllImport(ExternDll.User32, CharSet = CharSet.Auto)] - //[ResourceExposure(ResourceScope.None)] - //public static extern bool GetMonitorInfo(HandleRef hmonitor, [In, Out] MONITORINFOEX info); - - //[DllImport(ExternDll.User32, ExactSpelling = true)] - //[ResourceExposure(ResourceScope.None)] - //public static extern bool EnumDisplayMonitors(HandleRef hdc, COMRECT rcClip, MonitorEnumProc lpfnEnum, IntPtr dwData); - - //[DllImport(ExternDll.User32, ExactSpelling = true)] - //[ResourceExposure(ResourceScope.None)] - //public static extern IntPtr MonitorFromWindow(HandleRef handle, int flags); - - //[DllImport(ExternDll.User32, ExactSpelling = true)] - //[ResourceExposure(ResourceScope.None)] - //public static extern IntPtr MonitorFromPoint(POINTSTRUCT pt, int flags); - - //[DllImport(ExternDll.User32, ExactSpelling = true)] - //[ResourceExposure(ResourceScope.None)] - //public static extern IntPtr MonitorFromRect(ref RECT rect, int flags); - - //[DllImport(ExternDll.User32, ExactSpelling = true, CharSet = CharSet.Auto)] - //[ResourceExposure(ResourceScope.None)] - //public static extern int GetSystemMetrics(int nIndex); - - //[DllImport(ExternDll.User32, CharSet = CharSet.Auto)] - //[ResourceExposure(ResourceScope.None)] - //public static extern bool SystemParametersInfo(int nAction, int nParam, ref RECT rc, int nUpdate); - - //[StructLayout(LayoutKind.Sequential)] - //public struct RECT - //{ - // public int Left; - // public int Top; - // public int Right; - // public int Bottom; - - // public RECT(int left, int top, int right, int bottom) - // { - // Left = left; - // Top = top; - // Right = right; - // Bottom = bottom; - // } - - // public static RECT FromXYWH(int x, int y, int width, int height) - // { - // return new RECT(x, y, x + width, y + height); - // } - //} - - //[StructLayout(LayoutKind.Sequential)] - //public struct POINTSTRUCT - //{ - // public int X; - // public int Y; - - // public POINTSTRUCT(int x, int y) - // { - // X = x; - // Y = y; - // } - //} - - //[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 4)] - //public class MONITORINFOEX - //{ - // internal int cbSize = Marshal.SizeOf(typeof(MONITORINFOEX)); - // internal RECT rcMonitor = new RECT(); - // internal RECT rcWork = new RECT(); - // internal int dwFlags = 0; - // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] - // internal char[] szDevice = new char[32]; - //} - - //[StructLayout(LayoutKind.Sequential)] - //public class COMRECT - //{ - // public int Left; - // public int Top; - // public int Right; - // public int Bottom; - //} - } + public const int ERROR_SUCCESS = 0; + public const int ERROR_INVALID_PARAMETER = 87; + public const int ERROR_INSUFFICIENT_BUFFER = 122; + public const int ERROR_NO_DATA = 232; + public const int ERROR_INVALID_FLAGS = 1004; + public const int ERROR_NOT_FOUND = 1168; + public const int ERROR_CANCELLED = 1223; + public const int ERROR_NO_SUCH_LOGON_SESSION = 1312; + public const int ERROR_INVALID_ACCOUNT_NAME = 1315; + + //public const int SPI_GETWORKAREA = 48; + //public const int SM_CXSCREEN = 0; + //public const int SM_CYSCREEN = 1; + //public const int SM_XVIRTUALSCREEN = 76; + //public const int SM_YXVIRTUALSCREEN = 77; + //public const int SM_CXVIRTUALSCREEN = 78; + //public const int SM_CYXVIRTUALSCREEN = 79; + //public const int SM_CMONITORS = 80; + + //public delegate bool MonitorEnumProc(IntPtr monitor, IntPtr hdc, IntPtr lprcMonitor, IntPtr lParam); + + //public static readonly HandleRef NullHandleRef = new HandleRef(null, IntPtr.Zero); + + //[DllImport(ExternDll.User32, CharSet = CharSet.Auto)] + //[ResourceExposure(ResourceScope.None)] + //public static extern bool GetMonitorInfo(HandleRef hmonitor, [In, Out] MONITORINFOEX info); + + //[DllImport(ExternDll.User32, ExactSpelling = true)] + //[ResourceExposure(ResourceScope.None)] + //public static extern bool EnumDisplayMonitors(HandleRef hdc, COMRECT rcClip, MonitorEnumProc lpfnEnum, IntPtr dwData); + + //[DllImport(ExternDll.User32, ExactSpelling = true)] + //[ResourceExposure(ResourceScope.None)] + //public static extern IntPtr MonitorFromWindow(HandleRef handle, int flags); + + //[DllImport(ExternDll.User32, ExactSpelling = true)] + //[ResourceExposure(ResourceScope.None)] + //public static extern IntPtr MonitorFromPoint(POINTSTRUCT pt, int flags); + + //[DllImport(ExternDll.User32, ExactSpelling = true)] + //[ResourceExposure(ResourceScope.None)] + //public static extern IntPtr MonitorFromRect(ref RECT rect, int flags); + + //[DllImport(ExternDll.User32, ExactSpelling = true, CharSet = CharSet.Auto)] + //[ResourceExposure(ResourceScope.None)] + //public static extern int GetSystemMetrics(int nIndex); + + //[DllImport(ExternDll.User32, CharSet = CharSet.Auto)] + //[ResourceExposure(ResourceScope.None)] + //public static extern bool SystemParametersInfo(int nAction, int nParam, ref RECT rc, int nUpdate); + + //[StructLayout(LayoutKind.Sequential)] + //public struct RECT + //{ + // public int Left; + // public int Top; + // public int Right; + // public int Bottom; + + // public RECT(int left, int top, int right, int bottom) + // { + // Left = left; + // Top = top; + // Right = right; + // Bottom = bottom; + // } + + // public static RECT FromXYWH(int x, int y, int width, int height) + // { + // return new RECT(x, y, x + width, y + height); + // } + //} + + //[StructLayout(LayoutKind.Sequential)] + //public struct POINTSTRUCT + //{ + // public int X; + // public int Y; + + // public POINTSTRUCT(int x, int y) + // { + // X = x; + // Y = y; + // } + //} + + //[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto, Pack = 4)] + //public class MONITORINFOEX + //{ + // internal int cbSize = Marshal.SizeOf(typeof(MONITORINFOEX)); + // internal RECT rcMonitor = new RECT(); + // internal RECT rcWork = new RECT(); + // internal int dwFlags = 0; + // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] + // internal char[] szDevice = new char[32]; + //} + + //[StructLayout(LayoutKind.Sequential)] + //public class COMRECT + //{ + // public int Left; + // public int Top; + // public int Right; + // public int Bottom; + //} } diff --git a/src/Infrastructure/Windows/Interop/Ntdll.cs b/src/Infrastructure/Windows/Interop/Ntdll.cs index 14f2ad67..5091283f 100644 --- a/src/Infrastructure/Windows/Interop/Ntdll.cs +++ b/src/Infrastructure/Windows/Interop/Ntdll.cs @@ -1,41 +1,40 @@ -namespace Sqlbi.Bravo.Infrastructure.Windows.Interop -{ - using System; - using System.Runtime.InteropServices; +using System; +using System.Runtime.InteropServices; - internal static class Ntdll - { - internal enum NTSTATUS - { - STATUS_SUCCESS = 0 - } +namespace Sqlbi.Bravo.Infrastructure.Windows.Interop; - internal enum PROCESSINFOCLASS - { - ProcessBasicInformation = 0, - //ProcessDebugPort = 7, - //ProcessWow64Information = 26, - //ProcessImageFileName = 27, - //ProcessBreakOnTermination = 29, - //ProcessSubsystemInformation = 75 - } +internal static class Ntdll +{ + internal enum NTSTATUS + { + STATUS_SUCCESS = 0 + } - internal struct PROCESS_BASIC_INFORMATION - { - public uint ExitStatus; + internal enum PROCESSINFOCLASS + { + ProcessBasicInformation = 0, + //ProcessDebugPort = 7, + //ProcessWow64Information = 26, + //ProcessImageFileName = 27, + //ProcessBreakOnTermination = 29, + //ProcessSubsystemInformation = 75 + } - public IntPtr PebBaseAddress; + internal struct PROCESS_BASIC_INFORMATION + { + public uint ExitStatus; - public UIntPtr AffinityMask; + public IntPtr PebBaseAddress; - public int BasePriority; + public UIntPtr AffinityMask; - public UIntPtr UniqueProcessId; + public int BasePriority; - public UIntPtr InheritedFromUniqueProcessId; - } + public UIntPtr UniqueProcessId; - [DllImport(ExternDll.Ntdll, ExactSpelling = true, SetLastError = true)] - internal static extern int NtQueryInformationProcess(IntPtr processHandle, PROCESSINFOCLASS processInformationClass, out PROCESS_BASIC_INFORMATION processInformation, uint processInformationLength, out int returnLength); + public UIntPtr InheritedFromUniqueProcessId; } + + [DllImport(ExternDll.Ntdll, ExactSpelling = true, SetLastError = true)] + internal static extern int NtQueryInformationProcess(IntPtr processHandle, PROCESSINFOCLASS processInformationClass, out PROCESS_BASIC_INFORMATION processInformation, uint processInformationLength, out int returnLength); } diff --git a/src/Infrastructure/Windows/Interop/Ole32.cs b/src/Infrastructure/Windows/Interop/Ole32.cs index 3420e996..0c8f305e 100644 --- a/src/Infrastructure/Windows/Interop/Ole32.cs +++ b/src/Infrastructure/Windows/Interop/Ole32.cs @@ -1,11 +1,10 @@ using System; using System.Runtime.InteropServices; -namespace Sqlbi.Bravo.Infrastructure.Windows.Interop +namespace Sqlbi.Bravo.Infrastructure.Windows.Interop; + +internal static class Ole32 { - internal static class Ole32 - { - [DllImport("ole32.dll", ExactSpelling = true)] - public static extern HRESULT RevokeDragDrop(IntPtr hWnd); - } + [DllImport("ole32.dll", ExactSpelling = true)] + public static extern HRESULT RevokeDragDrop(IntPtr hWnd); } diff --git a/src/Infrastructure/Windows/Interop/User32.cs b/src/Infrastructure/Windows/Interop/User32.cs index dfad9589..06f80cba 100644 --- a/src/Infrastructure/Windows/Interop/User32.cs +++ b/src/Infrastructure/Windows/Interop/User32.cs @@ -1,181 +1,180 @@ -namespace Sqlbi.Bravo.Infrastructure.Windows.Interop +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Sqlbi.Bravo.Infrastructure.Windows.Interop; + +internal static class User32 { - using System; - using System.Runtime.InteropServices; - using System.Text; + [Flags] + public enum KeyState + { + None = 0, + Down = 1, + Toggled = 2 + } + + [StructLayout(LayoutKind.Sequential)] + public struct COPYDATASTRUCT + { + public IntPtr dwData; + public int cbData; + [MarshalAs(UnmanagedType.LPWStr)] + public string lpData; + } + + [Flags] + public enum MODEKEY : uint + { + MOD_NONE = 0x0000, + MOD_ALT = 0x0001, + MOD_CONTROL = 0x0002, + MOD_SHIFT = 0x0004, + MOD_WIN = 0x0008, + //MOD_NOREPEAT = 0x4000 + } - internal static class User32 + /// + /// https://docs.microsoft.com/en-us/windows/win32/api/shellscalingapi/ne-shellscalingapi-process_dpi_awareness + /// + public enum PROCESS_DPI_AWARENESS { - [Flags] - public enum KeyState - { - None = 0, - Down = 1, - Toggled = 2 - } - - [StructLayout(LayoutKind.Sequential)] - public struct COPYDATASTRUCT - { - public IntPtr dwData; - public int cbData; - [MarshalAs(UnmanagedType.LPWStr)] - public string lpData; - } - - [Flags] - public enum MODEKEY : uint - { - MOD_NONE = 0x0000, - MOD_ALT = 0x0001, - MOD_CONTROL = 0x0002, - MOD_SHIFT = 0x0004, - MOD_WIN = 0x0008, - //MOD_NOREPEAT = 0x4000 - } - - /// - /// https://docs.microsoft.com/en-us/windows/win32/api/shellscalingapi/ne-shellscalingapi-process_dpi_awareness - /// - public enum PROCESS_DPI_AWARENESS - { - PROCESS_DPI_UNAWARE = 0, - PROCESS_SYSTEM_DPI_AWARE = 1, - PROCESS_PER_MONITOR_DPI_AWARE = 2 - } - - /// - /// https://docs.microsoft.com/en-us/windows/win32/hidpi/dpi-awareness-context - /// - public enum DPI_AWARENESS_CONTEXT - { - DPI_AWARENESS_CONTEXT_UNAWARE = -1, - DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = -2, - DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = -3, - DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4 - } - - - [StructLayout(LayoutKind.Sequential)] - public struct WINDOWCOMPOSITIONATTRIBDATA - { - public WINDOWCOMPOSITIONATTRIB Attrib; - public IntPtr pvData; - public int cbData; - } - - public enum WINDOWCOMPOSITIONATTRIB - { - WCA_UNDEFINED = 0, - WCA_NCRENDERING_ENABLED = 1, - WCA_NCRENDERING_POLICY = 2, - WCA_TRANSITIONS_FORCEDISABLED = 3, - WCA_ALLOW_NCPAINT = 4, - WCA_CAPTION_BUTTON_BOUNDS = 5, - WCA_NONCLIENT_RTL_LAYOUT = 6, - WCA_FORCE_ICONIC_REPRESENTATION = 7, - WCA_EXTENDED_FRAME_BOUNDS = 8, - WCA_HAS_ICONIC_BITMAP = 9, - WCA_THEME_ATTRIBUTES = 10, - WCA_NCRENDERING_EXILED = 11, - WCA_NCADORNMENTINFO = 12, - WCA_EXCLUDED_FROM_LIVEPREVIEW = 13, - WCA_VIDEO_OVERLAY_ACTIVE = 14, - WCA_FORCE_ACTIVEWINDOW_APPEARANCE = 15, - WCA_DISALLOW_PEEK = 16, - WCA_CLOAK = 17, - WCA_CLOAKED = 18, - WCA_ACCENT_POLICY = 19, - WCA_FREEZE_REPRESENTATION = 20, - WCA_EVER_UNCLOAKED = 21, - WCA_VISUAL_OWNER = 22, - WCA_HOLOGRAPHIC = 23, - WCA_EXCLUDED_FROM_DDA = 24, - WCA_PASSIVEUPDATEMODE = 25, - WCA_USEDARKMODECOLORS = 26, - WCA_LAST = 27, - }; - - public const int SW_HIDE = 0; - public const int SW_NORMAL = 1; - public const int SW_SHOWNORMAL = SW_NORMAL; - public const int SW_SHOWMINIMIZED = 2; - public const int SW_SHOWMAXIMIZED = SW_MAXIMIZE; - public const int SW_MAXIMIZE = 3; - public const int SW_SHOWNOACTIVATE = 4; - public const int SW_SHOW = 5; - public const int SW_MINIMIZE = 6; - public const int SW_SHOWMINNOACTIVE = 7; - public const int SW_SHOWNA = 8; - public const int SW_RESTORE = 9; - public const int SW_SHOWDEFAULT = 10; - public const int SW_FORCEMINIMIZE = 11; - - public delegate bool EnumThreadDelegate(IntPtr hWnd, IntPtr lParam); - - public delegate IntPtr WndProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam); - - [DllImport(ExternDll.User32, SetLastError = true)] - public static extern bool RegisterHotKey(IntPtr hWnd, int id, MODEKEY modifiers, System.Windows.Forms.Keys keys); - - [DllImport(ExternDll.User32, SetLastError = true)] - public static extern bool UnregisterHotKey(IntPtr hWnd, int id); - - [DllImport(ExternDll.User32, CharSet = CharSet.Auto)] - public static extern IntPtr SendMessage(IntPtr hWnd, WindowMessage uMsg, int wParam, ref COPYDATASTRUCT lParam); - - [DllImport(ExternDll.User32, CharSet = CharSet.Unicode)] - public static extern IntPtr SendMessage(IntPtr hWnd, WindowMessage uMsg, int wParam, StringBuilder lParam); - - [DllImport(ExternDll.User32, CharSet = CharSet.Auto)] - public static extern IntPtr SendMessage(IntPtr hWnd, WindowMessage uMsg, IntPtr wParam, IntPtr lParam); - - [DllImport(ExternDll.User32, CharSet = CharSet.Auto)] - public static extern IntPtr SendMessage(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam); - - [DllImport(ExternDll.User32, CharSet = CharSet.Unicode)] - public static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType); - - [DllImport(ExternDll.User32)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool IsWindowVisible(IntPtr hWnd); - - [DllImport(ExternDll.User32)] - public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); - - [DllImport(ExternDll.User32)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool SetForegroundWindow(IntPtr hWnd); - - [DllImport(ExternDll.User32, SetLastError = true)] - public static extern bool SetProcessDPIAware(); - - [DllImport(ExternDll.User32, SetLastError = true)] - public static extern bool SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT value); - - [DllImport(ExternDll.Shcore, SetLastError = true)] - internal static extern bool SetProcessDpiAwareness(PROCESS_DPI_AWARENESS awareness); - - [DllImport(ExternDll.User32, SetLastError = true)] - private static extern bool EnableNonClientDpiScaling(IntPtr hWnd); - - [DllImport(ExternDll.User32)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool EnumThreadWindows(int dwThreadId, EnumThreadDelegate lpfn, IntPtr lParam); - - [DllImport(ExternDll.User32, SetLastError = true, CharSet = CharSet.Unicode)] - public static extern IntPtr FindWindow(string? lpClassName, string lpWindowName); - - [DllImport(ExternDll.User32, CharSet = CharSet.Unicode, ExactSpelling = true)] - public static extern int RegisterWindowMessageW(string lpString); - - [DllImport(ExternDll.User32, CharSet = CharSet.Auto, BestFitMapping = false, SetLastError = true)] - public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int processId); - - [DllImport(ExternDll.User32, CharSet = CharSet.Auto, ExactSpelling = true)] - public static extern short GetKeyState(int nVirtKey); - - [DllImport(ExternDll.User32)] - public static extern int SetWindowCompositionAttribute(IntPtr hWnd, ref WINDOWCOMPOSITIONATTRIBDATA data); + PROCESS_DPI_UNAWARE = 0, + PROCESS_SYSTEM_DPI_AWARE = 1, + PROCESS_PER_MONITOR_DPI_AWARE = 2 } + + /// + /// https://docs.microsoft.com/en-us/windows/win32/hidpi/dpi-awareness-context + /// + public enum DPI_AWARENESS_CONTEXT + { + DPI_AWARENESS_CONTEXT_UNAWARE = -1, + DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = -2, + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = -3, + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4 + } + + + [StructLayout(LayoutKind.Sequential)] + public struct WINDOWCOMPOSITIONATTRIBDATA + { + public WINDOWCOMPOSITIONATTRIB Attrib; + public IntPtr pvData; + public int cbData; + } + + public enum WINDOWCOMPOSITIONATTRIB + { + WCA_UNDEFINED = 0, + WCA_NCRENDERING_ENABLED = 1, + WCA_NCRENDERING_POLICY = 2, + WCA_TRANSITIONS_FORCEDISABLED = 3, + WCA_ALLOW_NCPAINT = 4, + WCA_CAPTION_BUTTON_BOUNDS = 5, + WCA_NONCLIENT_RTL_LAYOUT = 6, + WCA_FORCE_ICONIC_REPRESENTATION = 7, + WCA_EXTENDED_FRAME_BOUNDS = 8, + WCA_HAS_ICONIC_BITMAP = 9, + WCA_THEME_ATTRIBUTES = 10, + WCA_NCRENDERING_EXILED = 11, + WCA_NCADORNMENTINFO = 12, + WCA_EXCLUDED_FROM_LIVEPREVIEW = 13, + WCA_VIDEO_OVERLAY_ACTIVE = 14, + WCA_FORCE_ACTIVEWINDOW_APPEARANCE = 15, + WCA_DISALLOW_PEEK = 16, + WCA_CLOAK = 17, + WCA_CLOAKED = 18, + WCA_ACCENT_POLICY = 19, + WCA_FREEZE_REPRESENTATION = 20, + WCA_EVER_UNCLOAKED = 21, + WCA_VISUAL_OWNER = 22, + WCA_HOLOGRAPHIC = 23, + WCA_EXCLUDED_FROM_DDA = 24, + WCA_PASSIVEUPDATEMODE = 25, + WCA_USEDARKMODECOLORS = 26, + WCA_LAST = 27, + }; + + public const int SW_HIDE = 0; + public const int SW_NORMAL = 1; + public const int SW_SHOWNORMAL = SW_NORMAL; + public const int SW_SHOWMINIMIZED = 2; + public const int SW_SHOWMAXIMIZED = SW_MAXIMIZE; + public const int SW_MAXIMIZE = 3; + public const int SW_SHOWNOACTIVATE = 4; + public const int SW_SHOW = 5; + public const int SW_MINIMIZE = 6; + public const int SW_SHOWMINNOACTIVE = 7; + public const int SW_SHOWNA = 8; + public const int SW_RESTORE = 9; + public const int SW_SHOWDEFAULT = 10; + public const int SW_FORCEMINIMIZE = 11; + + public delegate bool EnumThreadDelegate(IntPtr hWnd, IntPtr lParam); + + public delegate IntPtr WndProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam); + + [DllImport(ExternDll.User32, SetLastError = true)] + public static extern bool RegisterHotKey(IntPtr hWnd, int id, MODEKEY modifiers, System.Windows.Forms.Keys keys); + + [DllImport(ExternDll.User32, SetLastError = true)] + public static extern bool UnregisterHotKey(IntPtr hWnd, int id); + + [DllImport(ExternDll.User32, CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, WindowMessage uMsg, int wParam, ref COPYDATASTRUCT lParam); + + [DllImport(ExternDll.User32, CharSet = CharSet.Unicode)] + public static extern IntPtr SendMessage(IntPtr hWnd, WindowMessage uMsg, int wParam, StringBuilder lParam); + + [DllImport(ExternDll.User32, CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, WindowMessage uMsg, IntPtr wParam, IntPtr lParam); + + [DllImport(ExternDll.User32, CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam); + + [DllImport(ExternDll.User32, CharSet = CharSet.Unicode)] + public static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType); + + [DllImport(ExternDll.User32)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool IsWindowVisible(IntPtr hWnd); + + [DllImport(ExternDll.User32)] + public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); + + [DllImport(ExternDll.User32)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool SetForegroundWindow(IntPtr hWnd); + + [DllImport(ExternDll.User32, SetLastError = true)] + public static extern bool SetProcessDPIAware(); + + [DllImport(ExternDll.User32, SetLastError = true)] + public static extern bool SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT value); + + [DllImport(ExternDll.Shcore, SetLastError = true)] + internal static extern bool SetProcessDpiAwareness(PROCESS_DPI_AWARENESS awareness); + + [DllImport(ExternDll.User32, SetLastError = true)] + private static extern bool EnableNonClientDpiScaling(IntPtr hWnd); + + [DllImport(ExternDll.User32)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool EnumThreadWindows(int dwThreadId, EnumThreadDelegate lpfn, IntPtr lParam); + + [DllImport(ExternDll.User32, SetLastError = true, CharSet = CharSet.Unicode)] + public static extern IntPtr FindWindow(string? lpClassName, string lpWindowName); + + [DllImport(ExternDll.User32, CharSet = CharSet.Unicode, ExactSpelling = true)] + public static extern int RegisterWindowMessageW(string lpString); + + [DllImport(ExternDll.User32, CharSet = CharSet.Auto, BestFitMapping = false, SetLastError = true)] + public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int processId); + + [DllImport(ExternDll.User32, CharSet = CharSet.Auto, ExactSpelling = true)] + public static extern short GetKeyState(int nVirtKey); + + [DllImport(ExternDll.User32)] + public static extern int SetWindowCompositionAttribute(IntPtr hWnd, ref WINDOWCOMPOSITIONATTRIBDATA data); } diff --git a/src/Infrastructure/Windows/Interop/Uxtheme.cs b/src/Infrastructure/Windows/Interop/Uxtheme.cs index 4dd547d5..716be855 100644 --- a/src/Infrastructure/Windows/Interop/Uxtheme.cs +++ b/src/Infrastructure/Windows/Interop/Uxtheme.cs @@ -1,83 +1,82 @@ -namespace Sqlbi.Bravo.Infrastructure.Windows.Interop -{ - using System; - using System.Runtime.InteropServices; +using System; +using System.Runtime.InteropServices; + +namespace Sqlbi.Bravo.Infrastructure.Windows.Interop; - ///*************************************************************************** - ///*************************************************************************** - /// - /// This class uses undocumented APIs introduced in Windows 10 build 1809 - /// APIs are not documented and exported by ordinal only. - /// - /// Discussion: Dark mode for applications - /// https://github.com/microsoft/WindowsAppSDK/issues/41#issue-622186032 - /// - ///*************************************************************************** - ///*************************************************************************** - internal static class Uxtheme +///*************************************************************************** +///*************************************************************************** +/// +/// This class uses undocumented APIs introduced in Windows 10 build 1809 +/// APIs are not documented and exported by ordinal only. +/// +/// Discussion: Dark mode for applications +/// https://github.com/microsoft/WindowsAppSDK/issues/41#issue-622186032 +/// +///*************************************************************************** +///*************************************************************************** +internal static class Uxtheme +{ + public enum PreferredAppMode { - public enum PreferredAppMode - { - Default = 0, - AllowDark = 1, - ForceDark = 2, - ForceLight = 3, - Max = 4 - }; + Default = 0, + AllowDark = 1, + ForceDark = 2, + ForceLight = 3, + Max = 4 + }; - /* - * - Mark boolean P/Invoke arguments with MarshalAs - * The Boolean representation that is required by the unmanaged method should be determined and matched to the appropriate System.Runtime.InteropServices.UnmanagedType. - * UnmanagedType.Bool is the Win32 BOOL type, which is always 4 bytes. UnmanagedType.U1 should be used for C++ bool or other 1-byte types - * See https://docs.microsoft.com/en-us/visualstudio/code-quality/ca1414?view=vs-2022#rule-description - */ + /* + * - Mark boolean P/Invoke arguments with MarshalAs + * The Boolean representation that is required by the unmanaged method should be determined and matched to the appropriate System.Runtime.InteropServices.UnmanagedType. + * UnmanagedType.Bool is the Win32 BOOL type, which is always 4 bytes. UnmanagedType.U1 should be used for C++ bool or other 1-byte types + * See https://docs.microsoft.com/en-us/visualstudio/code-quality/ca1414?view=vs-2022#rule-description + */ - /* - * OS version 1809 ( OS build 17763 ) - */ + /* + * OS version 1809 ( OS build 17763 ) + */ - /// - /// This undocumented API apparently triggers a refresh/repaint after changing the dark mode of a window - /// - [DllImport(ExternDll.Uxtheme, EntryPoint = "#104")] - public static extern void RefreshImmersiveColorPolicyState(); + /// + /// This undocumented API apparently triggers a refresh/repaint after changing the dark mode of a window + /// + [DllImport(ExternDll.Uxtheme, EntryPoint = "#104")] + public static extern void RefreshImmersiveColorPolicyState(); - /// - /// Returns the 'deafault app mode' on 'Custom' color settings - /// - [DllImport(ExternDll.Uxtheme, EntryPoint = "#132")] - [return: MarshalAs(UnmanagedType.U1)] - public static extern bool ShouldAppsUseDarkMode(); + /// + /// Returns the 'deafault app mode' on 'Custom' color settings + /// + [DllImport(ExternDll.Uxtheme, EntryPoint = "#132")] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool ShouldAppsUseDarkMode(); - [DllImport(ExternDll.Uxtheme, EntryPoint = "#133")] - [return: MarshalAs(UnmanagedType.U1)] - public static extern bool AllowDarkModeForWindow(IntPtr hWnd, [MarshalAs(UnmanagedType.U1)] bool allow); + [DllImport(ExternDll.Uxtheme, EntryPoint = "#133")] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool AllowDarkModeForWindow(IntPtr hWnd, [MarshalAs(UnmanagedType.U1)] bool allow); - [DllImport(ExternDll.Uxtheme, EntryPoint = "#135")] - [return: MarshalAs(UnmanagedType.U1)] - public static extern bool AllowDarkModeForApp([MarshalAs(UnmanagedType.U1)] bool allow); // 135 in both 1903 and 1809 + [DllImport(ExternDll.Uxtheme, EntryPoint = "#135")] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool AllowDarkModeForApp([MarshalAs(UnmanagedType.U1)] bool allow); // 135 in both 1903 and 1809 - [DllImport(ExternDll.Uxtheme, EntryPoint = "#137")] - [return: MarshalAs(UnmanagedType.U1)] - public static extern bool IsDarkModeAllowedForWindow(IntPtr hWnd); + [DllImport(ExternDll.Uxtheme, EntryPoint = "#137")] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool IsDarkModeAllowedForWindow(IntPtr hWnd); - /* - * OS version 1903 ( OS build 18362 ) - */ + /* + * OS version 1903 ( OS build 18362 ) + */ - [DllImport(ExternDll.Uxtheme, EntryPoint = "#135")] - [return: MarshalAs(UnmanagedType.U4)] - public static extern PreferredAppMode SetPreferredAppMode(PreferredAppMode mode); // 135 in both 1903 and 1809 + [DllImport(ExternDll.Uxtheme, EntryPoint = "#135")] + [return: MarshalAs(UnmanagedType.U4)] + public static extern PreferredAppMode SetPreferredAppMode(PreferredAppMode mode); // 135 in both 1903 and 1809 - /// - /// Returns the 'deafault Windows mode' on 'Custom' color settings - /// - [DllImport(ExternDll.Uxtheme, EntryPoint = "#138")] - [return: MarshalAs(UnmanagedType.U1)] - public static extern bool ShouldSystemUseDarkMode(); + /// + /// Returns the 'deafault Windows mode' on 'Custom' color settings + /// + [DllImport(ExternDll.Uxtheme, EntryPoint = "#138")] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool ShouldSystemUseDarkMode(); - [DllImport(ExternDll.Uxtheme, EntryPoint = "#139")] - [return: MarshalAs(UnmanagedType.U1)] - public static extern bool IsDarkModeAllowedForApp(IntPtr hWnd); - } + [DllImport(ExternDll.Uxtheme, EntryPoint = "#139")] + [return: MarshalAs(UnmanagedType.U1)] + public static extern bool IsDarkModeAllowedForApp(IntPtr hWnd); } diff --git a/src/Infrastructure/Windows/Interop/Win32Constant.cs b/src/Infrastructure/Windows/Interop/Win32Constant.cs index 03038894..fa0ef54f 100644 --- a/src/Infrastructure/Windows/Interop/Win32Constant.cs +++ b/src/Infrastructure/Windows/Interop/Win32Constant.cs @@ -1,13 +1,12 @@ -namespace Sqlbi.Bravo.Infrastructure.Windows.Interop +namespace Sqlbi.Bravo.Infrastructure.Windows.Interop; + +internal static class Win32Constant { - internal static class Win32Constant - { - public const int MAX_PATH = 260; + public const int MAX_PATH = 260; - public const int INFOTIPSIZE = 1024; + public const int INFOTIPSIZE = 1024; - public const int TRUE = 1; + public const int TRUE = 1; - public const int FALSE = 0; - } + public const int FALSE = 0; } diff --git a/src/Infrastructure/Windows/Interop/WindowMessage.cs b/src/Infrastructure/Windows/Interop/WindowMessage.cs index 9140dec0..7b59463d 100644 --- a/src/Infrastructure/Windows/Interop/WindowMessage.cs +++ b/src/Infrastructure/Windows/Interop/WindowMessage.cs @@ -1,247 +1,246 @@ -namespace Sqlbi.Bravo.Infrastructure.Windows.Interop +namespace Sqlbi.Bravo.Infrastructure.Windows.Interop; + +// MS.Internal.Interop.WindowMessage +internal enum WindowMessage: uint { - // MS.Internal.Interop.WindowMessage - internal enum WindowMessage: uint - { - //WM_NULL = 0, - WM_CREATE = 1, - //WM_DESTROY = 2, - //WM_MOVE = 3, - //WM_SIZE = 5, - //WM_ACTIVATE = 6, - //WM_SETFOCUS = 7, - //WM_KILLFOCUS = 8, - //WM_ENABLE = 10, - //WM_SETREDRAW = 11, - //WM_SETTEXT = 12, - WM_GETTEXT = 13, - //WM_GETTEXTLENGTH = 14, - //WM_PAINT = 0xF, - //WM_CLOSE = 0x10, - //WM_QUERYENDSESSION = 17, - //WM_QUIT = 18, - //WM_QUERYOPEN = 19, - //WM_ERASEBKGND = 20, - //WM_SYSCOLORCHANGE = 21, - //WM_ENDSESSION = 22, - //WM_SHOWWINDOW = 24, - //WM_CTLCOLOR = 25, - //WM_WININICHANGE = 26, - //WM_SETTINGCHANGE = 26, - //WM_DEVMODECHANGE = 27, - //WM_ACTIVATEAPP = 28, - //WM_FONTCHANGE = 29, - //WM_TIMECHANGE = 30, - //WM_CANCELMODE = 0x1F, - //WM_SETCURSOR = 0x20, - //WM_MOUSEACTIVATE = 33, - //WM_CHILDACTIVATE = 34, - //WM_QUEUESYNC = 35, - //WM_GETMINMAXINFO = 36, - //WM_PAINTICON = 38, - //WM_ICONERASEBKGND = 39, - //WM_NEXTDLGCTL = 40, - //WM_SPOOLERSTATUS = 42, - //WM_DRAWITEM = 43, - //WM_MEASUREITEM = 44, - //WM_DELETEITEM = 45, - //WM_VKEYTOITEM = 46, - //WM_CHARTOITEM = 47, - //WM_SETFONT = 48, - //WM_GETFONT = 49, - //WM_SETHOTKEY = 50, - //WM_GETHOTKEY = 51, - //WM_QUERYDRAGICON = 55, - //WM_COMPAREITEM = 57, - //WM_GETOBJECT = 61, - //WM_COMPACTING = 65, - //WM_COMMNOTIFY = 68, - //WM_WINDOWPOSCHANGING = 70, - //WM_WINDOWPOSCHANGED = 71, - //WM_POWER = 72, - WM_COPYDATA = 74, - //WM_CANCELJOURNAL = 75, - //WM_NOTIFY = 78, - //WM_INPUTLANGCHANGEREQUEST = 80, - //WM_INPUTLANGCHANGE = 81, - //WM_TCARD = 82, - //WM_HELP = 83, - //WM_USERCHANGED = 84, - //WM_NOTIFYFORMAT = 85, - //WM_CONTEXTMENU = 123, - //WM_STYLECHANGING = 124, - //WM_STYLECHANGED = 125, - //WM_DISPLAYCHANGE = 126, - //WM_GETICON = 0x7F, - //WM_SETICON = 0x80, - //WM_NCCREATE = 129, - WM_NCDESTROY = 130, - //WM_NCCALCSIZE = 131, - //WM_NCHITTEST = 132, - //WM_NCPAINT = 133, - WM_NCACTIVATE = 134, - //WM_GETDLGCODE = 135, - //WM_SYNCPAINT = 136, - //WM_MOUSEQUERY = 155, - //WM_NCMOUSEMOVE = 160, - //WM_NCLBUTTONDOWN = 161, - //WM_NCLBUTTONUP = 162, - //WM_NCLBUTTONDBLCLK = 163, - //WM_NCRBUTTONDOWN = 164, - //WM_NCRBUTTONUP = 165, - //WM_NCRBUTTONDBLCLK = 166, - //WM_NCMBUTTONDOWN = 167, - //WM_NCMBUTTONUP = 168, - //WM_NCMBUTTONDBLCLK = 169, - //WM_NCXBUTTONDOWN = 171, - //WM_NCXBUTTONUP = 172, - //WM_NCXBUTTONDBLCLK = 173, - //WM_INPUT = 0xFF, - //WM_KEYFIRST = 0x100, - //WM_KEYDOWN = 0x100, - //WM_KEYUP = 257, - //WM_CHAR = 258, - //WM_DEADCHAR = 259, - //WM_SYSKEYDOWN = 260, - //WM_SYSKEYUP = 261, - //WM_SYSCHAR = 262, - //WM_SYSDEADCHAR = 263, - //WM_KEYLAST = 264, - //WM_IME_STARTCOMPOSITION = 269, - //WM_IME_ENDCOMPOSITION = 270, - //WM_IME_COMPOSITION = 271, - //WM_IME_KEYLAST = 271, - //WM_INITDIALOG = 272, - //WM_COMMAND = 273, - //WM_SYSCOMMAND = 274, - //WM_TIMER = 275, - //WM_HSCROLL = 276, - //WM_VSCROLL = 277, - //WM_INITMENU = 278, - //WM_INITMENUPOPUP = 279, - //WM_MENUSELECT = 287, - //WM_MENUCHAR = 288, - //WM_ENTERIDLE = 289, - //WM_UNINITMENUPOPUP = 293, - //WM_CHANGEUISTATE = 295, - //WM_UPDATEUISTATE = 296, - //WM_QUERYUISTATE = 297, - //WM_CTLCOLORMSGBOX = 306, - //WM_CTLCOLOREDIT = 307, - //WM_CTLCOLORLISTBOX = 308, - //WM_CTLCOLORBTN = 309, - //WM_CTLCOLORDLG = 310, - //WM_CTLCOLORSCROLLBAR = 311, - //WM_CTLCOLORSTATIC = 312, - //WM_MOUSEMOVE = 0x200, - //WM_MOUSEFIRST = 0x200, - //WM_LBUTTONDOWN = 513, - //WM_LBUTTONUP = 514, - //WM_LBUTTONDBLCLK = 515, - //WM_RBUTTONDOWN = 516, - //WM_RBUTTONUP = 517, - //WM_RBUTTONDBLCLK = 518, - //WM_MBUTTONDOWN = 519, - //WM_MBUTTONUP = 520, - //WM_MBUTTONDBLCLK = 521, - //WM_MOUSEWHEEL = 522, - //WM_XBUTTONDOWN = 523, - //WM_XBUTTONUP = 524, - //WM_XBUTTONDBLCLK = 525, - //WM_MOUSEHWHEEL = 526, - //WM_MOUSELAST = 526, - //WM_PARENTNOTIFY = 528, - //WM_ENTERMENULOOP = 529, - //WM_EXITMENULOOP = 530, - //WM_NEXTMENU = 531, - //WM_SIZING = 532, - //WM_CAPTURECHANGED = 533, - //WM_MOVING = 534, - //WM_POWERBROADCAST = 536, - //WM_DEVICECHANGE = 537, - //WM_POINTERDEVICECHANGE = 568, - //WM_POINTERDEVICEINRANGE = 569, - //WM_POINTERDEVICEOUTOFRANGE = 570, - //WM_POINTERUPDATE = 581, - //WM_POINTERDOWN = 582, - //WM_POINTERUP = 583, - //WM_POINTERENTER = 585, - //WM_POINTERLEAVE = 586, - //WM_POINTERACTIVATE = 587, - //WM_POINTERCAPTURECHANGED = 588, - //WM_IME_SETCONTEXT = 641, - //WM_IME_NOTIFY = 642, - //WM_IME_CONTROL = 643, - //WM_IME_COMPOSITIONFULL = 644, - //WM_IME_SELECT = 645, - //WM_IME_CHAR = 646, - //WM_IME_REQUEST = 648, - //WM_IME_KEYDOWN = 656, - //WM_IME_KEYUP = 657, - //WM_MDICREATE = 544, - //WM_MDIDESTROY = 545, - //WM_MDIACTIVATE = 546, - //WM_MDIRESTORE = 547, - //WM_MDINEXT = 548, - //WM_MDIMAXIMIZE = 549, - //WM_MDITILE = 550, - //WM_MDICASCADE = 551, - //WM_MDIICONARRANGE = 552, - //WM_MDIGETACTIVE = 553, - //WM_MDISETMENU = 560, - //WM_ENTERSIZEMOVE = 561, - //WM_EXITSIZEMOVE = 562, - //WM_DROPFILES = 563, - //WM_MDIREFRESHMENU = 564, - //WM_MOUSEHOVER = 673, - //WM_NCMOUSELEAVE = 674, - //WM_MOUSELEAVE = 675, - //WM_WTSSESSION_CHANGE = 689, - //WM_TABLET_DEFBASE = 704, - //WM_TABLET_MAXOFFSET = 0x20, - //WM_TABLET_ADDED = 712, - //WM_TABLET_DELETED = 713, - //WM_TABLET_FLICK = 715, - //WM_TABLET_QUERYSYSTEMGESTURESTATUS = 716, - //WM_DPICHANGED = 736, - //WM_DPICHANGED_BEFOREPARENT = 738, - //WM_DPICHANGED_AFTERPARENT = 739, - //WM_CUT = 768, - //WM_COPY = 769, - //WM_PASTE = 770, - //WM_CLEAR = 771, - //WM_UNDO = 772, - //WM_RENDERFORMAT = 773, - //WM_RENDERALLFORMATS = 774, - //WM_DESTROYCLIPBOARD = 775, - //WM_DRAWCLIPBOARD = 776, - //WM_PAINTCLIPBOARD = 777, - //WM_VSCROLLCLIPBOARD = 778, - //WM_SIZECLIPBOARD = 779, - //WM_ASKCBFORMATNAME = 780, - //WM_CHANGECBCHAIN = 781, - //WM_HSCROLLCLIPBOARD = 782, - //WM_QUERYNEWPALETTE = 783, - //WM_PALETTEISCHANGING = 784, - //WM_PALETTECHANGED = 785, - WM_HOTKEY = 786, - //WM_PRINT = 791, - //WM_PRINTCLIENT = 792, - //WM_APPCOMMAND = 793, - WM_THEMECHANGED = 794, - WM_DWMCOMPOSITIONCHANGED = 798, - //WM_DWMNCRENDERINGCHANGED = 799, - WM_DWMCOLORIZATIONCOLORCHANGED = 800, - //WM_DWMWINDOWMAXIMIZEDCHANGE = 801, - //WM_HANDHELDFIRST = 856, - //WM_HANDHELDLAST = 863, - //WM_AFXFIRST = 864, - //WM_AFXLAST = 895, - //WM_PENWINFIRST = 896, - //WM_PENWINLAST = 911, - //WM_DWMSENDICONICTHUMBNAIL = 803, - //WM_DWMSENDICONICLIVEPREVIEWBITMAP = 806, - //WM_USER = 0x400, - //WM_APP = 0x8000 - } + //WM_NULL = 0, + WM_CREATE = 1, + //WM_DESTROY = 2, + //WM_MOVE = 3, + //WM_SIZE = 5, + //WM_ACTIVATE = 6, + //WM_SETFOCUS = 7, + //WM_KILLFOCUS = 8, + //WM_ENABLE = 10, + //WM_SETREDRAW = 11, + //WM_SETTEXT = 12, + WM_GETTEXT = 13, + //WM_GETTEXTLENGTH = 14, + //WM_PAINT = 0xF, + //WM_CLOSE = 0x10, + //WM_QUERYENDSESSION = 17, + //WM_QUIT = 18, + //WM_QUERYOPEN = 19, + //WM_ERASEBKGND = 20, + //WM_SYSCOLORCHANGE = 21, + //WM_ENDSESSION = 22, + //WM_SHOWWINDOW = 24, + //WM_CTLCOLOR = 25, + //WM_WININICHANGE = 26, + //WM_SETTINGCHANGE = 26, + //WM_DEVMODECHANGE = 27, + //WM_ACTIVATEAPP = 28, + //WM_FONTCHANGE = 29, + //WM_TIMECHANGE = 30, + //WM_CANCELMODE = 0x1F, + //WM_SETCURSOR = 0x20, + //WM_MOUSEACTIVATE = 33, + //WM_CHILDACTIVATE = 34, + //WM_QUEUESYNC = 35, + //WM_GETMINMAXINFO = 36, + //WM_PAINTICON = 38, + //WM_ICONERASEBKGND = 39, + //WM_NEXTDLGCTL = 40, + //WM_SPOOLERSTATUS = 42, + //WM_DRAWITEM = 43, + //WM_MEASUREITEM = 44, + //WM_DELETEITEM = 45, + //WM_VKEYTOITEM = 46, + //WM_CHARTOITEM = 47, + //WM_SETFONT = 48, + //WM_GETFONT = 49, + //WM_SETHOTKEY = 50, + //WM_GETHOTKEY = 51, + //WM_QUERYDRAGICON = 55, + //WM_COMPAREITEM = 57, + //WM_GETOBJECT = 61, + //WM_COMPACTING = 65, + //WM_COMMNOTIFY = 68, + //WM_WINDOWPOSCHANGING = 70, + //WM_WINDOWPOSCHANGED = 71, + //WM_POWER = 72, + WM_COPYDATA = 74, + //WM_CANCELJOURNAL = 75, + //WM_NOTIFY = 78, + //WM_INPUTLANGCHANGEREQUEST = 80, + //WM_INPUTLANGCHANGE = 81, + //WM_TCARD = 82, + //WM_HELP = 83, + //WM_USERCHANGED = 84, + //WM_NOTIFYFORMAT = 85, + //WM_CONTEXTMENU = 123, + //WM_STYLECHANGING = 124, + //WM_STYLECHANGED = 125, + //WM_DISPLAYCHANGE = 126, + //WM_GETICON = 0x7F, + //WM_SETICON = 0x80, + //WM_NCCREATE = 129, + WM_NCDESTROY = 130, + //WM_NCCALCSIZE = 131, + //WM_NCHITTEST = 132, + //WM_NCPAINT = 133, + WM_NCACTIVATE = 134, + //WM_GETDLGCODE = 135, + //WM_SYNCPAINT = 136, + //WM_MOUSEQUERY = 155, + //WM_NCMOUSEMOVE = 160, + //WM_NCLBUTTONDOWN = 161, + //WM_NCLBUTTONUP = 162, + //WM_NCLBUTTONDBLCLK = 163, + //WM_NCRBUTTONDOWN = 164, + //WM_NCRBUTTONUP = 165, + //WM_NCRBUTTONDBLCLK = 166, + //WM_NCMBUTTONDOWN = 167, + //WM_NCMBUTTONUP = 168, + //WM_NCMBUTTONDBLCLK = 169, + //WM_NCXBUTTONDOWN = 171, + //WM_NCXBUTTONUP = 172, + //WM_NCXBUTTONDBLCLK = 173, + //WM_INPUT = 0xFF, + //WM_KEYFIRST = 0x100, + //WM_KEYDOWN = 0x100, + //WM_KEYUP = 257, + //WM_CHAR = 258, + //WM_DEADCHAR = 259, + //WM_SYSKEYDOWN = 260, + //WM_SYSKEYUP = 261, + //WM_SYSCHAR = 262, + //WM_SYSDEADCHAR = 263, + //WM_KEYLAST = 264, + //WM_IME_STARTCOMPOSITION = 269, + //WM_IME_ENDCOMPOSITION = 270, + //WM_IME_COMPOSITION = 271, + //WM_IME_KEYLAST = 271, + //WM_INITDIALOG = 272, + //WM_COMMAND = 273, + //WM_SYSCOMMAND = 274, + //WM_TIMER = 275, + //WM_HSCROLL = 276, + //WM_VSCROLL = 277, + //WM_INITMENU = 278, + //WM_INITMENUPOPUP = 279, + //WM_MENUSELECT = 287, + //WM_MENUCHAR = 288, + //WM_ENTERIDLE = 289, + //WM_UNINITMENUPOPUP = 293, + //WM_CHANGEUISTATE = 295, + //WM_UPDATEUISTATE = 296, + //WM_QUERYUISTATE = 297, + //WM_CTLCOLORMSGBOX = 306, + //WM_CTLCOLOREDIT = 307, + //WM_CTLCOLORLISTBOX = 308, + //WM_CTLCOLORBTN = 309, + //WM_CTLCOLORDLG = 310, + //WM_CTLCOLORSCROLLBAR = 311, + //WM_CTLCOLORSTATIC = 312, + //WM_MOUSEMOVE = 0x200, + //WM_MOUSEFIRST = 0x200, + //WM_LBUTTONDOWN = 513, + //WM_LBUTTONUP = 514, + //WM_LBUTTONDBLCLK = 515, + //WM_RBUTTONDOWN = 516, + //WM_RBUTTONUP = 517, + //WM_RBUTTONDBLCLK = 518, + //WM_MBUTTONDOWN = 519, + //WM_MBUTTONUP = 520, + //WM_MBUTTONDBLCLK = 521, + //WM_MOUSEWHEEL = 522, + //WM_XBUTTONDOWN = 523, + //WM_XBUTTONUP = 524, + //WM_XBUTTONDBLCLK = 525, + //WM_MOUSEHWHEEL = 526, + //WM_MOUSELAST = 526, + //WM_PARENTNOTIFY = 528, + //WM_ENTERMENULOOP = 529, + //WM_EXITMENULOOP = 530, + //WM_NEXTMENU = 531, + //WM_SIZING = 532, + //WM_CAPTURECHANGED = 533, + //WM_MOVING = 534, + //WM_POWERBROADCAST = 536, + //WM_DEVICECHANGE = 537, + //WM_POINTERDEVICECHANGE = 568, + //WM_POINTERDEVICEINRANGE = 569, + //WM_POINTERDEVICEOUTOFRANGE = 570, + //WM_POINTERUPDATE = 581, + //WM_POINTERDOWN = 582, + //WM_POINTERUP = 583, + //WM_POINTERENTER = 585, + //WM_POINTERLEAVE = 586, + //WM_POINTERACTIVATE = 587, + //WM_POINTERCAPTURECHANGED = 588, + //WM_IME_SETCONTEXT = 641, + //WM_IME_NOTIFY = 642, + //WM_IME_CONTROL = 643, + //WM_IME_COMPOSITIONFULL = 644, + //WM_IME_SELECT = 645, + //WM_IME_CHAR = 646, + //WM_IME_REQUEST = 648, + //WM_IME_KEYDOWN = 656, + //WM_IME_KEYUP = 657, + //WM_MDICREATE = 544, + //WM_MDIDESTROY = 545, + //WM_MDIACTIVATE = 546, + //WM_MDIRESTORE = 547, + //WM_MDINEXT = 548, + //WM_MDIMAXIMIZE = 549, + //WM_MDITILE = 550, + //WM_MDICASCADE = 551, + //WM_MDIICONARRANGE = 552, + //WM_MDIGETACTIVE = 553, + //WM_MDISETMENU = 560, + //WM_ENTERSIZEMOVE = 561, + //WM_EXITSIZEMOVE = 562, + //WM_DROPFILES = 563, + //WM_MDIREFRESHMENU = 564, + //WM_MOUSEHOVER = 673, + //WM_NCMOUSELEAVE = 674, + //WM_MOUSELEAVE = 675, + //WM_WTSSESSION_CHANGE = 689, + //WM_TABLET_DEFBASE = 704, + //WM_TABLET_MAXOFFSET = 0x20, + //WM_TABLET_ADDED = 712, + //WM_TABLET_DELETED = 713, + //WM_TABLET_FLICK = 715, + //WM_TABLET_QUERYSYSTEMGESTURESTATUS = 716, + //WM_DPICHANGED = 736, + //WM_DPICHANGED_BEFOREPARENT = 738, + //WM_DPICHANGED_AFTERPARENT = 739, + //WM_CUT = 768, + //WM_COPY = 769, + //WM_PASTE = 770, + //WM_CLEAR = 771, + //WM_UNDO = 772, + //WM_RENDERFORMAT = 773, + //WM_RENDERALLFORMATS = 774, + //WM_DESTROYCLIPBOARD = 775, + //WM_DRAWCLIPBOARD = 776, + //WM_PAINTCLIPBOARD = 777, + //WM_VSCROLLCLIPBOARD = 778, + //WM_SIZECLIPBOARD = 779, + //WM_ASKCBFORMATNAME = 780, + //WM_CHANGECBCHAIN = 781, + //WM_HSCROLLCLIPBOARD = 782, + //WM_QUERYNEWPALETTE = 783, + //WM_PALETTEISCHANGING = 784, + //WM_PALETTECHANGED = 785, + WM_HOTKEY = 786, + //WM_PRINT = 791, + //WM_PRINTCLIENT = 792, + //WM_APPCOMMAND = 793, + WM_THEMECHANGED = 794, + WM_DWMCOMPOSITIONCHANGED = 798, + //WM_DWMNCRENDERINGCHANGED = 799, + WM_DWMCOLORIZATIONCOLORCHANGED = 800, + //WM_DWMWINDOWMAXIMIZEDCHANGE = 801, + //WM_HANDHELDFIRST = 856, + //WM_HANDHELDLAST = 863, + //WM_AFXFIRST = 864, + //WM_AFXLAST = 895, + //WM_PENWINFIRST = 896, + //WM_PENWINLAST = 911, + //WM_DWMSENDICONICTHUMBNAIL = 803, + //WM_DWMSENDICONICLIVEPREVIEWBITMAP = 806, + //WM_USER = 0x400, + //WM_APP = 0x8000 } diff --git a/src/Infrastructure/Windows/Win32WindowWrapper.cs b/src/Infrastructure/Windows/Win32WindowWrapper.cs index 8240670f..4fa04d7a 100644 --- a/src/Infrastructure/Windows/Win32WindowWrapper.cs +++ b/src/Infrastructure/Windows/Win32WindowWrapper.cs @@ -1,17 +1,16 @@ using System; using System.Windows.Forms; -namespace Sqlbi.Bravo.Infrastructure.Windows +namespace Sqlbi.Bravo.Infrastructure.Windows; + +internal class Win32WindowWrapper : IWin32Window { - internal class Win32WindowWrapper : IWin32Window + private Win32WindowWrapper(IntPtr handle) { - private Win32WindowWrapper(IntPtr handle) - { - Handle = handle; - } + Handle = handle; + } - public IntPtr Handle { get; private set; } + public IntPtr Handle { get; private set; } - public static Win32WindowWrapper CreateFrom(IntPtr handle) => new(handle); - } + public static Win32WindowWrapper CreateFrom(IntPtr handle) => new(handle); } diff --git a/src/Infrastructure/Windows/WindowDialogs.cs b/src/Infrastructure/Windows/WindowDialogs.cs index 901abdaf..a8249f76 100644 --- a/src/Infrastructure/Windows/WindowDialogs.cs +++ b/src/Infrastructure/Windows/WindowDialogs.cs @@ -1,185 +1,184 @@ -namespace Sqlbi.Bravo.Infrastructure.Windows +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Windows.Interop; + +namespace Sqlbi.Bravo.Infrastructure.Windows; + +/// +/// .NET wrapper around the Win32 open file dialog +/// +internal class OpenFileDialog { - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Windows.Interop; - using System; - using System.Drawing; - using System.Runtime.InteropServices; - using System.Windows.Forms; - - /// - /// .NET wrapper around the Win32 open file dialog - /// - internal class OpenFileDialog - { - public string? DefaultExt { get; set; } = null; - - public string? File { get; set; } = null; - - public string? Filter { get; set; } = null; + public string? DefaultExt { get; set; } = null; - public string? InitialDirectory { get; set; } = null; + public string? File { get; set; } = null; - public string? Title { get; set; } = null; + public string? Filter { get; set; } = null; - public DialogResult ShowDialog(IntPtr hWnd) - { - var ofn = new Comdlg32.OPENFILENAME(); - ofn.lStructSize = Marshal.SizeOf(ofn); - ofn.hwndOwner = hWnd; - ofn.hInstance = IntPtr.Zero; - ofn.lpstrTitle = Title.NullIfWhiteSpace(); - ofn.lpstrDefExt = DefaultExt.NullIfWhiteSpace(); - ofn.lpstrFilter = Filter.ToFileDialogFilterString(); - ofn.lpstrInitialDir = InitialDirectory.NullIfWhiteSpace(); - ofn.lpstrFile = new string(new char[Win32Constant.MAX_PATH]); - ofn.nMaxFile = ofn.lpstrFile.Length; - ofn.lpstrFileTitle = new string(new char[Win32Constant.MAX_PATH]); - ofn.nMaxFileTitle = ofn.lpstrFileTitle.Length; - - if (!Comdlg32.GetOpenFileName(ofn)) - return DialogResult.Cancel; + public string? InitialDirectory { get; set; } = null; - File = ofn.lpstrFile; - return DialogResult.OK; - } - } + public string? Title { get; set; } = null; - /// - /// .NET wrapper around the Win32 save file dialog - /// - internal class SaveFileDialog + public DialogResult ShowDialog(IntPtr hWnd) { - public string? DefaultExt { get; set; } = null; + var ofn = new Comdlg32.OPENFILENAME(); + ofn.lStructSize = Marshal.SizeOf(ofn); + ofn.hwndOwner = hWnd; + ofn.hInstance = IntPtr.Zero; + ofn.lpstrTitle = Title.NullIfWhiteSpace(); + ofn.lpstrDefExt = DefaultExt.NullIfWhiteSpace(); + ofn.lpstrFilter = Filter.ToFileDialogFilterString(); + ofn.lpstrInitialDir = InitialDirectory.NullIfWhiteSpace(); + ofn.lpstrFile = new string(new char[Win32Constant.MAX_PATH]); + ofn.nMaxFile = ofn.lpstrFile.Length; + ofn.lpstrFileTitle = new string(new char[Win32Constant.MAX_PATH]); + ofn.nMaxFileTitle = ofn.lpstrFileTitle.Length; + + if (!Comdlg32.GetOpenFileName(ofn)) + return DialogResult.Cancel; + + File = ofn.lpstrFile; + return DialogResult.OK; + } +} - public string? FileName { get; set; } = null; +/// +/// .NET wrapper around the Win32 save file dialog +/// +internal class SaveFileDialog +{ + public string? DefaultExt { get; set; } = null; - public string? Filter { get; set; } = null; + public string? FileName { get; set; } = null; - public string? InitialDirectory { get; set; } = null; + public string? Filter { get; set; } = null; - public string? Title { get; set; } = null; + public string? InitialDirectory { get; set; } = null; - public DialogResult ShowDialog(IntPtr hWnd) - { - var ofn = new Comdlg32.OPENFILENAME(); - ofn.lStructSize = Marshal.SizeOf(ofn); - ofn.hwndOwner = hWnd; - ofn.hInstance = IntPtr.Zero; - ofn.lpstrTitle = Title.NullIfWhiteSpace(); - ofn.lpstrDefExt = DefaultExt.NullIfWhiteSpace(); - ofn.lpstrFilter = Filter.ToFileDialogFilterString(); - ofn.lpstrInitialDir = InitialDirectory.NullIfWhiteSpace(); - ofn.lpstrFile = new string(new char[Win32Constant.MAX_PATH]); - ofn.nMaxFile = ofn.lpstrFile.Length; - ofn.lpstrFileTitle = new string(new char[Win32Constant.MAX_PATH]); - ofn.nMaxFileTitle = ofn.lpstrFileTitle.Length; - - if (!Comdlg32.GetSaveFileName(ofn)) - return DialogResult.Cancel; + public string? Title { get; set; } = null; - FileName = ofn.lpstrFile; - return DialogResult.OK; - } + public DialogResult ShowDialog(IntPtr hWnd) + { + var ofn = new Comdlg32.OPENFILENAME(); + ofn.lStructSize = Marshal.SizeOf(ofn); + ofn.hwndOwner = hWnd; + ofn.hInstance = IntPtr.Zero; + ofn.lpstrTitle = Title.NullIfWhiteSpace(); + ofn.lpstrDefExt = DefaultExt.NullIfWhiteSpace(); + ofn.lpstrFilter = Filter.ToFileDialogFilterString(); + ofn.lpstrInitialDir = InitialDirectory.NullIfWhiteSpace(); + ofn.lpstrFile = new string(new char[Win32Constant.MAX_PATH]); + ofn.nMaxFile = ofn.lpstrFile.Length; + ofn.lpstrFileTitle = new string(new char[Win32Constant.MAX_PATH]); + ofn.nMaxFileTitle = ofn.lpstrFileTitle.Length; + + if (!Comdlg32.GetSaveFileName(ofn)) + return DialogResult.Cancel; + + FileName = ofn.lpstrFile; + return DialogResult.OK; } +} + +/// +/// .NET wrapper around the Win32 color picker dialog +/// +internal class ColorPickerDialog +{ + private readonly int[] _customColors = new int[16] + { + 0x00FFFFFF, 0x00C0C0C0, 0x00808080, 0x00000000, + 0x00FF0000, 0x00800000, 0x00FFFF00, 0x00808000, + 0x0000FF00, 0x00008000, 0x0000FFFF, 0x00008080, + 0x000000FF, 0x00000080, 0x00FF00FF, 0x00800080, + }; + + public Color Color { get; set; } - /// - /// .NET wrapper around the Win32 color picker dialog - /// - internal class ColorPickerDialog + public DialogResult ShowDialog(IntPtr hWnd) { - private readonly int[] _customColors = new int[16] + var lpCustColors = Marshal.AllocCoTaskMem(16 * sizeof(int)); + try { - 0x00FFFFFF, 0x00C0C0C0, 0x00808080, 0x00000000, - 0x00FF0000, 0x00800000, 0x00FFFF00, 0x00808000, - 0x0000FF00, 0x00008000, 0x0000FFFF, 0x00008080, - 0x000000FF, 0x00000080, 0x00FF00FF, 0x00800080, - }; + Marshal.Copy(_customColors, 0, lpCustColors, 16); + + var cc = new Comdlg32.CHOOSECOLOR(); + cc.lStructSize = Marshal.SizeOf(cc); + cc.hwndOwner = hWnd; + cc.hInstance = IntPtr.Zero; + cc.lpCustColors = lpCustColors; + cc.rgbResult = ColorTranslator.ToWin32(Color); + cc.Flags = Comdlg32.CHOOSECOLORFLAGS.CC_RGBINIT; - public Color Color { get; set; } + if (!Comdlg32.ChooseColor(cc)) + return DialogResult.Cancel; + + if (cc.rgbResult != ColorTranslator.ToWin32(Color)) + Color = ColorTranslator.FromOle(cc.rgbResult); - public DialogResult ShowDialog(IntPtr hWnd) + Marshal.Copy(lpCustColors, _customColors, 0, 16); + return DialogResult.OK; + } + finally { - var lpCustColors = Marshal.AllocCoTaskMem(16 * sizeof(int)); - try - { - Marshal.Copy(_customColors, 0, lpCustColors, 16); - - var cc = new Comdlg32.CHOOSECOLOR(); - cc.lStructSize = Marshal.SizeOf(cc); - cc.hwndOwner = hWnd; - cc.hInstance = IntPtr.Zero; - cc.lpCustColors = lpCustColors; - cc.rgbResult = ColorTranslator.ToWin32(Color); - cc.Flags = Comdlg32.CHOOSECOLORFLAGS.CC_RGBINIT; - - if (!Comdlg32.ChooseColor(cc)) - return DialogResult.Cancel; - - if (cc.rgbResult != ColorTranslator.ToWin32(Color)) - Color = ColorTranslator.FromOle(cc.rgbResult); - - Marshal.Copy(lpCustColors, _customColors, 0, 16); - return DialogResult.OK; - } - finally - { - Marshal.FreeCoTaskMem(lpCustColors); - } + Marshal.FreeCoTaskMem(lpCustColors); } } +} - internal class MessageDialog +internal class MessageDialog +{ + public static void Show(string heading, string text) { - public static void Show(string heading, string text) + var appIcon = Icon.ExtractAssociatedIcon(AppEnvironment.ProcessPath); + var icon = new TaskDialogIcon(appIcon!); + + var page = new TaskDialogPage() { - var appIcon = Icon.ExtractAssociatedIcon(AppEnvironment.ProcessPath); - var icon = new TaskDialogIcon(appIcon!); + Caption = AppEnvironment.ApplicationMainWindowTitle, + Heading = heading, + Text = text, + Icon = icon, + AllowCancel = true + }; - var page = new TaskDialogPage() - { - Caption = AppEnvironment.ApplicationMainWindowTitle, - Heading = heading, - Text = text, - Icon = icon, - AllowCancel = true - }; + var hwndOwner = ProcessHelper.GetCurrentProcessMainWindowHandle(); + _ = TaskDialog.ShowDialog(hwndOwner, page, TaskDialogStartupLocation.CenterScreen); + } - var hwndOwner = ProcessHelper.GetCurrentProcessMainWindowHandle(); - _ = TaskDialog.ShowDialog(hwndOwner, page, TaskDialogStartupLocation.CenterScreen); - } + public static TaskDialogButton ShowDialog(string heading, string? text, string? footnoteText, bool allowCancel, params TaskDialogButton[] buttons) + { + var appIcon = Icon.ExtractAssociatedIcon(AppEnvironment.ProcessPath); + var icon = new TaskDialogIcon(appIcon!); - public static TaskDialogButton ShowDialog(string heading, string? text, string? footnoteText, bool allowCancel, params TaskDialogButton[] buttons) + var page = new TaskDialogPage() { - var appIcon = Icon.ExtractAssociatedIcon(AppEnvironment.ProcessPath); - var icon = new TaskDialogIcon(appIcon!); + Caption = AppEnvironment.ApplicationMainWindowTitle, + Heading = heading, + Text = text, + Icon = icon, + AllowCancel = allowCancel, // || buttons.Any((button) => button == TaskDialogButton.Cancel), + AllowMinimize = false + }; - var page = new TaskDialogPage() + if (footnoteText is not null) + { + page.Footnote = new TaskDialogFootnote() { - Caption = AppEnvironment.ApplicationMainWindowTitle, - Heading = heading, - Text = text, - Icon = icon, - AllowCancel = allowCancel, // || buttons.Any((button) => button == TaskDialogButton.Cancel), - AllowMinimize = false + Text = footnoteText, }; + } - if (footnoteText is not null) - { - page.Footnote = new TaskDialogFootnote() - { - Text = footnoteText, - }; - } - - foreach (var button in buttons) - page.Buttons.Add(button); + foreach (var button in buttons) + page.Buttons.Add(button); - var hwndOwner = ProcessHelper.GetCurrentProcessMainWindowHandle(); - var clickedButton = TaskDialog.ShowDialog(hwndOwner, page, TaskDialogStartupLocation.CenterScreen); + var hwndOwner = ProcessHelper.GetCurrentProcessMainWindowHandle(); + var clickedButton = TaskDialog.ShowDialog(hwndOwner, page, TaskDialogStartupLocation.CenterScreen); - return clickedButton; - } + return clickedButton; } } diff --git a/src/Infrastructure/Windows/WindowSubclass.cs b/src/Infrastructure/Windows/WindowSubclass.cs index a6ba3cd4..fde18016 100644 --- a/src/Infrastructure/Windows/WindowSubclass.cs +++ b/src/Infrastructure/Windows/WindowSubclass.cs @@ -1,39 +1,38 @@ -namespace Sqlbi.Bravo.Infrastructure.Windows +using System; +using Sqlbi.Bravo.Infrastructure.Windows.Interop; + +namespace Sqlbi.Bravo.Infrastructure.Windows; + +/// +/// Installs a window subclass callback to hook messages sent to the specified window +/// +internal abstract class WindowSubclass { - using Sqlbi.Bravo.Infrastructure.Windows.Interop; - using System; + private readonly Comctl32.SUBCLASSPROC _subclassProc; + private readonly IntPtr _hWnd; - /// - /// Installs a window subclass callback to hook messages sent to the specified window - /// - internal abstract class WindowSubclass + public WindowSubclass(IntPtr hWnd) { - private readonly Comctl32.SUBCLASSPROC _subclassProc; - private readonly IntPtr _hWnd; + _hWnd = hWnd; - public WindowSubclass(IntPtr hWnd) - { - _hWnd = hWnd; + _subclassProc = SubclassProc; + _ = Comctl32.SetWindowSubclass(hWnd, _subclassProc, IntPtr.Zero, IntPtr.Zero); + } - _subclassProc = SubclassProc; - _ = Comctl32.SetWindowSubclass(hWnd, _subclassProc, IntPtr.Zero, IntPtr.Zero); - } + private IntPtr SubclassProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam, IntPtr uIdSubclass, IntPtr dwRefData) + { + return WndProc(hWnd, uMsg, wParam, lParam, uIdSubclass, dwRefData); + } - private IntPtr SubclassProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam, IntPtr uIdSubclass, IntPtr dwRefData) + protected virtual IntPtr WndProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam, IntPtr uIdSubclass, IntPtr dwRefData) + { + if (uMsg == (uint)WindowMessage.WM_NCDESTROY) { - return WndProc(hWnd, uMsg, wParam, lParam, uIdSubclass, dwRefData); + // The subclass must be removed before the window being subclassed is destroyed + // This is a permanent subclass so can call RemoveWindowSubclass inside the subclass procedure itself + _ = Comctl32.RemoveWindowSubclass(_hWnd, _subclassProc, IntPtr.Zero); } - protected virtual IntPtr WndProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam, IntPtr uIdSubclass, IntPtr dwRefData) - { - if (uMsg == (uint)WindowMessage.WM_NCDESTROY) - { - // The subclass must be removed before the window being subclassed is destroyed - // This is a permanent subclass so can call RemoveWindowSubclass inside the subclass procedure itself - _ = Comctl32.RemoveWindowSubclass(_hWnd, _subclassProc, IntPtr.Zero); - } - - return Comctl32.DefSubclassProc(hWnd, uMsg, wParam, lParam); - } + return Comctl32.DefSubclassProc(hWnd, uMsg, wParam, lParam); } } diff --git a/src/Models/AnalyzeModel/TabularColumn.cs b/src/Models/AnalyzeModel/TabularColumn.cs index 75a5dac6..d4959f8f 100644 --- a/src/Models/AnalyzeModel/TabularColumn.cs +++ b/src/Models/AnalyzeModel/TabularColumn.cs @@ -1,74 +1,73 @@ -namespace Sqlbi.Bravo.Models.AnalyzeModel +using System.Diagnostics; +using System.Text.Json.Serialization; +using Dax.ViewModel; +using Sqlbi.Bravo.Infrastructure.Extensions; +using TOM = Microsoft.AnalysisServices.Tabular; + +namespace Sqlbi.Bravo.Models.AnalyzeModel; + +[DebuggerDisplay("'{TableName}'[{Name}]")] +public class TabularColumn { - using Dax.ViewModel; - using Sqlbi.Bravo.Infrastructure.Extensions; - using System.Diagnostics; - using System.Text.Json.Serialization; - using TOM = Microsoft.AnalysisServices.Tabular; + [JsonPropertyName("name")] + public string? FullName => $"'{TableName}'[{Name}]"; - [DebuggerDisplay("'{TableName}'[{Name}]")] - public class TabularColumn - { - [JsonPropertyName("name")] - public string? FullName => $"'{ TableName }'[{ Name }]"; + [JsonPropertyName("columnName")] + public string? Name { get; set; } - [JsonPropertyName("columnName")] - public string? Name { get; set; } + [JsonPropertyName("tableName")] + public string? TableName { get; set; } - [JsonPropertyName("tableName")] - public string? TableName { get; set; } + [JsonPropertyName("columnCardinality")] + public long Cardinality { get; set; } - [JsonPropertyName("columnCardinality")] - public long Cardinality { get; set; } + [JsonPropertyName("size")] + public long Size { get; set; } - [JsonPropertyName("size")] - public long Size { get; set; } + [JsonPropertyName("weight")] + public double Weight { get; set; } - [JsonPropertyName("weight")] - public double Weight { get; set; } + [JsonPropertyName("isReferenced")] + public bool IsReferenced { get; set; } - [JsonPropertyName("isReferenced")] - public bool IsReferenced { get; set; } + [JsonPropertyName("dataType")] + public string? DataType { get; set; } - [JsonPropertyName("dataType")] - public string? DataType { get; set; } + [JsonPropertyName("isHidden")] + public bool IsHidden { get; set; } - [JsonPropertyName("isHidden")] - public bool IsHidden { get; set; } + [JsonPropertyName("isQueryable")] + public bool? IsQueryable { get; set; } - [JsonPropertyName("isQueryable")] - public bool? IsQueryable { get; set; } + internal static TabularColumn CreateFrom(VpaColumn vpaColumn, long databaseSize) + { + // Prevent division by zero or invalid floating-point results when the database size is zero. + // NaN or Infinity values are invalid in JSON and cause serialization errors during API responses. + // This may occur when VPA extracts zero values for column sizes, which has been observed + // in models with Direct Lake partitions where the DMVs DISCOVER_STORAGE_TABLES, + // DISCOVER_STORAGE_TABLE_COLUMNS, and DISCOVER_STORAGE_TABLE_COLUMN_SEGMENTS may return null/zero. + // See https://github.com/sql-bi/VertiPaq-Analyzer/pull/196 + // See https://github.com/sql-bi/VertiPaq-Analyzer/pull/209 + // This check is retained for backward compatibility and to handle possible regressions, + // as the issue was previously fixed in the PBI service but appears to have reoccurred, + // resulting in the error reported in https://github.com/sql-bi/Bravo/issues/903. + double weight = 0; + if (databaseSize > 0) + weight = (double)vpaColumn.TotalSize / databaseSize; - internal static TabularColumn CreateFrom(VpaColumn vpaColumn, long databaseSize) + var column = new TabularColumn { - // Prevent division by zero or invalid floating-point results when the database size is zero. - // NaN or Infinity values are invalid in JSON and cause serialization errors during API responses. - // This may occur when VPA extracts zero values for column sizes, which has been observed - // in models with Direct Lake partitions where the DMVs DISCOVER_STORAGE_TABLES, - // DISCOVER_STORAGE_TABLE_COLUMNS, and DISCOVER_STORAGE_TABLE_COLUMN_SEGMENTS may return null/zero. - // See https://github.com/sql-bi/VertiPaq-Analyzer/pull/196 - // See https://github.com/sql-bi/VertiPaq-Analyzer/pull/209 - // This check is retained for backward compatibility and to handle possible regressions, - // as the issue was previously fixed in the PBI service but appears to have reoccurred, - // resulting in the error reported in https://github.com/sql-bi/Bravo/issues/903. - double weight = 0; - if (databaseSize > 0) - weight = (double)vpaColumn.TotalSize / databaseSize; - - var column = new TabularColumn - { - Name = vpaColumn.ColumnName, - TableName = vpaColumn.Table.TableName, - Cardinality = vpaColumn.ColumnCardinality, - Size = vpaColumn.TotalSize, - Weight = weight, - IsReferenced = vpaColumn.IsReferenced, - DataType = vpaColumn.DataType, - IsHidden = vpaColumn.IsHidden, - IsQueryable = vpaColumn.State.TryParseTo()?.IsQueryable(), - }; + Name = vpaColumn.ColumnName, + TableName = vpaColumn.Table.TableName, + Cardinality = vpaColumn.ColumnCardinality, + Size = vpaColumn.TotalSize, + Weight = weight, + IsReferenced = vpaColumn.IsReferenced, + DataType = vpaColumn.DataType, + IsHidden = vpaColumn.IsHidden, + IsQueryable = vpaColumn.State.TryParseTo()?.IsQueryable(), + }; - return column; - } + return column; } } diff --git a/src/Models/AnalyzeModel/TabularDatabase.cs b/src/Models/AnalyzeModel/TabularDatabase.cs index caddd00e..801283b3 100644 --- a/src/Models/AnalyzeModel/TabularDatabase.cs +++ b/src/Models/AnalyzeModel/TabularDatabase.cs @@ -1,334 +1,333 @@ -namespace Sqlbi.Bravo.Models.AnalyzeModel +using System; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using System.Text.Json.Serialization; +using System.Threading; +using Dax.ViewModel; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Services; +using Sqlbi.Bravo.Models.FormatDax; +using SSAS = Microsoft.AnalysisServices; + +namespace Sqlbi.Bravo.Models.AnalyzeModel; + +public class TabularDatabase { - using Dax.ViewModel; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Services; - using Sqlbi.Bravo.Models.FormatDax; - using System; - using System.Collections.Generic; - using System.Data; - using System.IO; - using System.Linq; - using System.Text.Json.Serialization; - using System.Threading; - using SSAS = Microsoft.AnalysisServices; - - public class TabularDatabase - { - [JsonPropertyName("features")] - public TabularDatabaseFeature Features { get; set; } = TabularDatabaseFeature.All; - - [JsonPropertyName("featureUnsupportedReasons")] - public TabularDatabaseFeatureUnsupportedReason FeatureUnsupportedReasons { get; set; } = TabularDatabaseFeatureUnsupportedReason.None; + [JsonPropertyName("features")] + public TabularDatabaseFeature Features { get; set; } = TabularDatabaseFeature.All; - [JsonPropertyName("model")] - public TabularDatabaseInfo? Info { get; set; } + [JsonPropertyName("featureUnsupportedReasons")] + public TabularDatabaseFeatureUnsupportedReason FeatureUnsupportedReasons { get; set; } = TabularDatabaseFeatureUnsupportedReason.None; - [JsonPropertyName("measures")] - public IEnumerable? Measures { get; set; } + [JsonPropertyName("model")] + public TabularDatabaseInfo? Info { get; set; } - internal static TabularDatabase CreateFrom(Stream vpaxStream, Stream? obfuscationDictionaryStream = null) - { - var daxModel = VpaxHelper.GetDaxModel(vpaxStream); + [JsonPropertyName("measures")] + public IEnumerable? Measures { get; set; } - if (obfuscationDictionaryStream is not null) - { - VpaxObfuscatorHelper.Deobfuscate(daxModel, obfuscationDictionaryStream); - } + internal static TabularDatabase CreateFrom(Stream vpaxStream, Stream? obfuscationDictionaryStream = null) + { + var daxModel = VpaxHelper.GetDaxModel(vpaxStream); - var database = CreateFrom(daxModel); - { - database.Features &= ~TabularDatabaseFeature.AnalyzeModelSynchronize; - database.Features &= ~TabularDatabaseFeature.AnalyzeModelExportVpax; - database.Features &= ~TabularDatabaseFeature.FormatDaxSynchronize; - database.Features &= ~TabularDatabaseFeature.FormatDaxUpdateModel; - database.Features &= ~TabularDatabaseFeature.ManageDatesAll; - database.Features &= ~TabularDatabaseFeature.ExportDataAll; - - database.FeatureUnsupportedReasons |= TabularDatabaseFeatureUnsupportedReason.MetadataOnly; - database.FeatureUnsupportedReasons |= TabularDatabaseFeatureUnsupportedReason.ReadOnly; - - if (daxModel.ObfuscatorDictionaryId is not null && obfuscationDictionaryStream is null) - { - // Model is obfuscated and no dictionary provided, enable deobfuscation features - database.Features |= TabularDatabaseFeature.AnalyzeModelDeobfuscateVpax; - database.Features |= TabularDatabaseFeature.FormatDaxDeobfuscateVpax; - } - } - return database; + if (obfuscationDictionaryStream is not null) + { + VpaxObfuscatorHelper.Deobfuscate(daxModel, obfuscationDictionaryStream); } - internal static TabularDatabase CreateFrom(TabularConnectionWrapper connection, CancellationToken cancellationToken) + var database = CreateFrom(daxModel); { - var daxModel = VpaxHelper.GetDaxModel(connection, statisticsEnabled: false, cancellationToken); - var database = CreateFrom(daxModel, connection); + database.Features &= ~TabularDatabaseFeature.AnalyzeModelSynchronize; + database.Features &= ~TabularDatabaseFeature.AnalyzeModelExportVpax; + database.Features &= ~TabularDatabaseFeature.FormatDaxSynchronize; + database.Features &= ~TabularDatabaseFeature.FormatDaxUpdateModel; + database.Features &= ~TabularDatabaseFeature.ManageDatesAll; + database.Features &= ~TabularDatabaseFeature.ExportDataAll; + + database.FeatureUnsupportedReasons |= TabularDatabaseFeatureUnsupportedReason.MetadataOnly; + database.FeatureUnsupportedReasons |= TabularDatabaseFeatureUnsupportedReason.ReadOnly; - if (connection.Database.ReadWriteMode == SSAS.ReadWriteMode.ReadOnly) + if (daxModel.ObfuscatorDictionaryId is not null && obfuscationDictionaryStream is null) { - database.Features &= ~TabularDatabaseFeature.AllUpdateModel; - database.FeatureUnsupportedReasons |= TabularDatabaseFeatureUnsupportedReason.ReadOnly; + // Model is obfuscated and no dictionary provided, enable deobfuscation features + database.Features |= TabularDatabaseFeature.AnalyzeModelDeobfuscateVpax; + database.Features |= TabularDatabaseFeature.FormatDaxDeobfuscateVpax; } - - return database; } + return database; + } - internal static TabularDatabase CreateFrom(Dax.Metadata.Model daxModel, TabularConnectionWrapper? connection = default) - { - var vpaModel = new VpaModel(daxModel); + internal static TabularDatabase CreateFrom(TabularConnectionWrapper connection, CancellationToken cancellationToken) + { + var daxModel = VpaxHelper.GetDaxModel(connection, statisticsEnabled: false, cancellationToken); + var database = CreateFrom(daxModel, connection); - var includedDaxTables = daxModel.Tables.Where(IsIncluded).ToArray(); - var includedDaxTableNames = includedDaxTables.Select((t) => t.TableName.Name).ToHashSet(); + if (connection.Database.ReadWriteMode == SSAS.ReadWriteMode.ReadOnly) + { + database.Features &= ~TabularDatabaseFeature.AllUpdateModel; + database.FeatureUnsupportedReasons |= TabularDatabaseFeatureUnsupportedReason.ReadOnly; + } - var includedTables = vpaModel.Tables.Where((t) => includedDaxTableNames.Contains(t.TableName)).ToArray(); - var includedColumns = includedTables.SelectMany((t) => t.Columns.Where((c) => !c.IsRowNumber)).ToArray(); - var includedMeasures = daxModel.Tables.SelectMany((t) => t.Measures).ToArray(); // Measures here are not filtered as they are always considered 'included' + return database; + } - var databaseETag = TabularModelHelper.GetDatabaseETag(vpaModel.Model.ModelName.Name, vpaModel.Model.Version, vpaModel.Model.LastUpdate); - var databaseSize = includedColumns.Sum((c) => c.TotalSize); - var tables = includedTables.Select((t) => TabularTable.CreateFrom(t, connection?.Model)).ToArray(); - var columns = includedColumns.Select((c) => TabularColumn.CreateFrom(c, databaseSize)).ToArray(); - var measures = includedMeasures.Select((m) => TabularMeasure.CreateFrom(m, databaseETag, connection?.Model)).ToArray(); - var autoLineBreakStyle = measures.GetAutoLineBreakStyle(); + internal static TabularDatabase CreateFrom(Dax.Metadata.Model daxModel, TabularConnectionWrapper? connection = default) + { + var vpaModel = new VpaModel(daxModel); - var database = new TabularDatabase - { - Info = new TabularDatabaseInfo - { - ETag = databaseETag, - Name = daxModel.ModelName.Name, - Culture = connection?.Model.Culture, - CompatibilityMode = daxModel.CompatibilityMode.TryParseTo(), - CompatibilityLevel = daxModel.CompatibilityLevel, - DatabaseSize = databaseSize, - AutoLineBreakStyle = autoLineBreakStyle, - ServerName = daxModel.ServerName?.Name ?? connection?.Server.Name, - ServerVersion = connection?.Server.Version, - ServerEdition = connection?.Server.Edition, - ServerMode = connection?.Server.ServerMode, - ServerLocation = connection?.Server.ServerLocation, - TablesMaxRowsCount = includedTables.Length == 0 ? 0L : includedTables.Max((t) => t.RowsCount), - TablesCount = includedTables.Length, - Tables = tables, - ColumnsUnreferencedCount = includedColumns.Count((c) => !c.IsReferenced), - ColumnsCount = includedColumns.Length, - Columns = columns, - }, - Measures = measures - }; - - if (daxModel.Tables.Any(IsAutoDateTimeTable)) - { - database.Features &= ~TabularDatabaseFeature.ManageDatesAll; - database.FeatureUnsupportedReasons |= TabularDatabaseFeatureUnsupportedReason.ManageDatesAutoDateTimeEnabled; - } + var includedDaxTables = daxModel.Tables.Where(IsIncluded).ToArray(); + var includedDaxTableNames = includedDaxTables.Select((t) => t.TableName.Name).ToHashSet(); - if (daxModel.Tables.Count == 0) - { - database.Features &= ~TabularDatabaseFeature.ManageDatesAll; - database.FeatureUnsupportedReasons |= TabularDatabaseFeatureUnsupportedReason.ManageDatesEmptyTableCollection; - } + var includedTables = vpaModel.Tables.Where((t) => includedDaxTableNames.Contains(t.TableName)).ToArray(); + var includedColumns = includedTables.SelectMany((t) => t.Columns.Where((c) => !c.IsRowNumber)).ToArray(); + var includedMeasures = daxModel.Tables.SelectMany((t) => t.Measures).ToArray(); // Measures here are not filtered as they are always considered 'included' - return database; + var databaseETag = TabularModelHelper.GetDatabaseETag(vpaModel.Model.ModelName.Name, vpaModel.Model.Version, vpaModel.Model.LastUpdate); + var databaseSize = includedColumns.Sum((c) => c.TotalSize); + var tables = includedTables.Select((t) => TabularTable.CreateFrom(t, connection?.Model)).ToArray(); + var columns = includedColumns.Select((c) => TabularColumn.CreateFrom(c, databaseSize)).ToArray(); + var measures = includedMeasures.Select((m) => TabularMeasure.CreateFrom(m, databaseETag, connection?.Model)).ToArray(); + var autoLineBreakStyle = measures.GetAutoLineBreakStyle(); - static bool IsAutoDateTimeTable(Dax.Metadata.Table daxTable) + var database = new TabularDatabase + { + Info = new TabularDatabaseInfo { - if (daxTable.IsLocalDateTable || daxTable.IsTemplateDateTable) - return true; - - return false; - } + ETag = databaseETag, + Name = daxModel.ModelName.Name, + Culture = connection?.Model.Culture, + CompatibilityMode = daxModel.CompatibilityMode.TryParseTo(), + CompatibilityLevel = daxModel.CompatibilityLevel, + DatabaseSize = databaseSize, + AutoLineBreakStyle = autoLineBreakStyle, + ServerName = daxModel.ServerName?.Name ?? connection?.Server.Name, + ServerVersion = connection?.Server.Version, + ServerEdition = connection?.Server.Edition, + ServerMode = connection?.Server.ServerMode, + ServerLocation = connection?.Server.ServerLocation, + TablesMaxRowsCount = includedTables.Length == 0 ? 0L : includedTables.Max((t) => t.RowsCount), + TablesCount = includedTables.Length, + Tables = tables, + ColumnsUnreferencedCount = includedColumns.Count((c) => !c.IsReferenced), + ColumnsCount = includedColumns.Length, + Columns = columns, + }, + Measures = measures + }; + + if (daxModel.Tables.Any(IsAutoDateTimeTable)) + { + database.Features &= ~TabularDatabaseFeature.ManageDatesAll; + database.FeatureUnsupportedReasons |= TabularDatabaseFeatureUnsupportedReason.ManageDatesAutoDateTimeEnabled; + } - static bool IsIncluded(Dax.Metadata.Table daxTable) - { - if (daxTable.IsPrivate) - return false; + if (daxModel.Tables.Count == 0) + { + database.Features &= ~TabularDatabaseFeature.ManageDatesAll; + database.FeatureUnsupportedReasons |= TabularDatabaseFeatureUnsupportedReason.ManageDatesEmptyTableCollection; + } - if (IsAutoDateTimeTable(daxTable)) - return false; + return database; + static bool IsAutoDateTimeTable(Dax.Metadata.Table daxTable) + { + if (daxTable.IsLocalDateTable || daxTable.IsTemplateDateTable) return true; - } + + return false; } - internal static TabularDatabase CreateFromDmvSchema(AdomdConnectionWrapper connection) + static bool IsIncluded(Dax.Metadata.Table daxTable) { - using var tablesWithColumnsCommand = connection.CreateDmvTablesWithColumnsCommand(); - using var tablesWithColumnsReader = tablesWithColumnsCommand.ExecuteReader(CommandBehavior.SingleResult); - var tablesWithColumns = tablesWithColumnsReader.Select((reader) => ((string)reader["DIMENSION_UNIQUE_NAME"]).GetDaxName()!).ToArray(); - - using var tablesCommand = connection.CreateDmvTablesCommand(); - using var tablesReader = tablesCommand.ExecuteReader(CommandBehavior.SingleResult); - var tables = tablesReader.Select((reader) => TabularTable.CreateFromDmvTables(reader, tablesWithColumns)).Where(IsIncluded).ToArray(); + if (daxTable.IsPrivate) + return false; - var database = new TabularDatabase - { - Info = new TabularDatabaseInfo - { - ETag = null, - Name = connection.Connection.Database, - CompatibilityMode = null, - CompatibilityLevel = null, - DatabaseSize = null, - AutoLineBreakStyle = null, - ServerName = null, - ServerVersion = null, - ServerEdition = null, - ServerMode = null, - ServerLocation = null, - TablesMaxRowsCount = tables.Length == 0L ? 0L : tables.Max((t) => t.RowsCount), - TablesCount = tables.Length, - Tables = tables, - ColumnsUnreferencedCount = 0, - ColumnsCount = 0, - Columns = Array.Empty(), - }, - Measures = Array.Empty(), - }; - - return database; - - static bool IsIncluded(TabularTable table) - { - if (table.Name.IsAutoDateTimePrivateTableName()) - return false; + if (IsAutoDateTimeTable(daxTable)) + return false; - return true; - } + return true; } } - [Flags] - public enum TabularDatabaseFeature + internal static TabularDatabase CreateFromDmvSchema(AdomdConnectionWrapper connection) { - // TODO: rename 'All' to 'Default' - - None = 0, - - AnalyzeModelPage = 1 << 100, - AnalyzeModelSynchronize = 1 << 101, - AnalyzeModelExportVpax = 1 << 102, - AnalyzeModelDeobfuscateVpax = 1 << 103, - AnalyzeModelAll = AnalyzeModelPage | AnalyzeModelSynchronize | AnalyzeModelExportVpax, - - FormatDaxPage = 1 << 200, - FormatDaxSynchronize = 1 << 201, - FormatDaxUpdateModel = 1 << 202, - FormatDaxDeobfuscateVpax = 1 << 203, - FormatDaxAll = FormatDaxPage | FormatDaxSynchronize | FormatDaxUpdateModel, - - ManageDatesPage = 1 << 300, - ManageDatesSynchronize = 1 << 301, - ManageDatesUpdateModel = 1 << 302, - ManageDatesAll = ManageDatesPage | ManageDatesSynchronize | ManageDatesUpdateModel, - - ExportDataPage = 1 << 400, - ExportDataSynchronize = 1 << 401, - ExportDataAll = ExportDataPage | ExportDataSynchronize, - - AllSynchronize = AnalyzeModelSynchronize | FormatDaxSynchronize | ManageDatesSynchronize | ExportDataSynchronize, - AllUpdateModel = FormatDaxUpdateModel | ManageDatesUpdateModel, - All = AnalyzeModelAll | FormatDaxAll | ManageDatesAll | ExportDataAll, - } + using var tablesWithColumnsCommand = connection.CreateDmvTablesWithColumnsCommand(); + using var tablesWithColumnsReader = tablesWithColumnsCommand.ExecuteReader(CommandBehavior.SingleResult); + var tablesWithColumns = tablesWithColumnsReader.Select((reader) => ((string)reader["DIMENSION_UNIQUE_NAME"]).GetDaxName()!).ToArray(); - [Flags] - public enum TabularDatabaseFeatureUnsupportedReason - { - None = 0, - - /// - /// The state of the connected database instance is read-only - /// - ReadOnly = 1 << 1, - - /// - /// The was generated from a VPAX file that is a representation of the Tabular model and includes only its metadata - /// - MetadataOnly = 1 << 2, - - /// - /// The XMLA endpoint is not supported for the workspace capacity SKU - /// - /// - /// The XMLA endpoint is available for Power BI Premium Capacity workspaces (i.e. workspaces assigned to a Px, Ax or EMx SKU), Power BI Embedded workspaces, or Power BI Premium-Per-User (PPU) workspaces - /// - XmlaEndpointNotSupported = 1 << 3, - - // AnalyzeModel range << 100, - // FormatDax range << 200, - - /// - /// Models with auto date/time option enabled are not supported, user must disable this option on the model before using ManageDate templates - /// - ManageDatesAutoDateTimeEnabled = 1 << 300, - - /// - /// Feature supported only for models in Power BI Desktop mode - /// - ManageDatesPBIDesktopModelOnly = 1 << 301, - - /// - /// Feature supported only by databases that have at least one table - /// - ManageDatesEmptyTableCollection = 1 << 302, - - // ExportData range << 400, + using var tablesCommand = connection.CreateDmvTablesCommand(); + using var tablesReader = tablesCommand.ExecuteReader(CommandBehavior.SingleResult); + var tables = tablesReader.Select((reader) => TabularTable.CreateFromDmvTables(reader, tablesWithColumns)).Where(IsIncluded).ToArray(); + + var database = new TabularDatabase + { + Info = new TabularDatabaseInfo + { + ETag = null, + Name = connection.Connection.Database, + CompatibilityMode = null, + CompatibilityLevel = null, + DatabaseSize = null, + AutoLineBreakStyle = null, + ServerName = null, + ServerVersion = null, + ServerEdition = null, + ServerMode = null, + ServerLocation = null, + TablesMaxRowsCount = tables.Length == 0L ? 0L : tables.Max((t) => t.RowsCount), + TablesCount = tables.Length, + Tables = tables, + ColumnsUnreferencedCount = 0, + ColumnsCount = 0, + Columns = Array.Empty(), + }, + Measures = Array.Empty(), + }; + + return database; + + static bool IsIncluded(TabularTable table) + { + if (table.Name.IsAutoDateTimePrivateTableName()) + return false; + + return true; + } } +} - public class TabularDatabaseInfo - { - [JsonPropertyName("etag")] - public string? ETag { get; set; } +[Flags] +public enum TabularDatabaseFeature +{ + // TODO: rename 'All' to 'Default' + + None = 0, + + AnalyzeModelPage = 1 << 100, + AnalyzeModelSynchronize = 1 << 101, + AnalyzeModelExportVpax = 1 << 102, + AnalyzeModelDeobfuscateVpax = 1 << 103, + AnalyzeModelAll = AnalyzeModelPage | AnalyzeModelSynchronize | AnalyzeModelExportVpax, + + FormatDaxPage = 1 << 200, + FormatDaxSynchronize = 1 << 201, + FormatDaxUpdateModel = 1 << 202, + FormatDaxDeobfuscateVpax = 1 << 203, + FormatDaxAll = FormatDaxPage | FormatDaxSynchronize | FormatDaxUpdateModel, + + ManageDatesPage = 1 << 300, + ManageDatesSynchronize = 1 << 301, + ManageDatesUpdateModel = 1 << 302, + ManageDatesAll = ManageDatesPage | ManageDatesSynchronize | ManageDatesUpdateModel, + + ExportDataPage = 1 << 400, + ExportDataSynchronize = 1 << 401, + ExportDataAll = ExportDataPage | ExportDataSynchronize, + + AllSynchronize = AnalyzeModelSynchronize | FormatDaxSynchronize | ManageDatesSynchronize | ExportDataSynchronize, + AllUpdateModel = FormatDaxUpdateModel | ManageDatesUpdateModel, + All = AnalyzeModelAll | FormatDaxAll | ManageDatesAll | ExportDataAll, +} - [JsonPropertyName("name")] - public string? Name { get; set; } +[Flags] +public enum TabularDatabaseFeatureUnsupportedReason +{ + None = 0, + + /// + /// The state of the connected database instance is read-only + /// + ReadOnly = 1 << 1, + + /// + /// The was generated from a VPAX file that is a representation of the Tabular model and includes only its metadata + /// + MetadataOnly = 1 << 2, + + /// + /// The XMLA endpoint is not supported for the workspace capacity SKU + /// + /// + /// The XMLA endpoint is available for Power BI Premium Capacity workspaces (i.e. workspaces assigned to a Px, Ax or EMx SKU), Power BI Embedded workspaces, or Power BI Premium-Per-User (PPU) workspaces + /// + XmlaEndpointNotSupported = 1 << 3, + + // AnalyzeModel range << 100, + // FormatDax range << 200, + + /// + /// Models with auto date/time option enabled are not supported, user must disable this option on the model before using ManageDate templates + /// + ManageDatesAutoDateTimeEnabled = 1 << 300, + + /// + /// Feature supported only for models in Power BI Desktop mode + /// + ManageDatesPBIDesktopModelOnly = 1 << 301, + + /// + /// Feature supported only by databases that have at least one table + /// + ManageDatesEmptyTableCollection = 1 << 302, + + // ExportData range << 400, +} - [JsonPropertyName("culture")] - public string? Culture { get; set; } +public class TabularDatabaseInfo +{ + [JsonPropertyName("etag")] + public string? ETag { get; set; } - [JsonPropertyName("compatibilityMode")] - public SSAS.CompatibilityMode? CompatibilityMode { get; set; } + [JsonPropertyName("name")] + public string? Name { get; set; } - [JsonPropertyName("compatibilityLevel")] - public int? CompatibilityLevel { get; set; } + [JsonPropertyName("culture")] + public string? Culture { get; set; } - [JsonPropertyName("serverName")] - public string? ServerName { get; set; } + [JsonPropertyName("compatibilityMode")] + public SSAS.CompatibilityMode? CompatibilityMode { get; set; } - [JsonPropertyName("serverVersion")] - public string? ServerVersion { get; set; } + [JsonPropertyName("compatibilityLevel")] + public int? CompatibilityLevel { get; set; } - [JsonPropertyName("serverEdition")] - public SSAS.ServerEdition? ServerEdition { get; set; } + [JsonPropertyName("serverName")] + public string? ServerName { get; set; } - [JsonPropertyName("serverMode")] - public SSAS.ServerMode? ServerMode { get; set; } + [JsonPropertyName("serverVersion")] + public string? ServerVersion { get; set; } - [JsonPropertyName("serverLocation")] - public SSAS.ServerLocation? ServerLocation { get; set; } + [JsonPropertyName("serverEdition")] + public SSAS.ServerEdition? ServerEdition { get; set; } - [JsonPropertyName("tablesCount")] - public int? TablesCount { get; set; } + [JsonPropertyName("serverMode")] + public SSAS.ServerMode? ServerMode { get; set; } - [JsonPropertyName("columnsCount")] - public int? ColumnsCount { get; set; } + [JsonPropertyName("serverLocation")] + public SSAS.ServerLocation? ServerLocation { get; set; } - [JsonPropertyName("maxRows")] - public long? TablesMaxRowsCount { get; set; } + [JsonPropertyName("tablesCount")] + public int? TablesCount { get; set; } - [JsonPropertyName("size")] - public long? DatabaseSize { get; set; } + [JsonPropertyName("columnsCount")] + public int? ColumnsCount { get; set; } - [JsonPropertyName("unreferencedCount")] - public int? ColumnsUnreferencedCount { get; set; } + [JsonPropertyName("maxRows")] + public long? TablesMaxRowsCount { get; set; } - [JsonPropertyName("autoLineBreakStyle")] - public DaxLineBreakStyle? AutoLineBreakStyle { get; set; } + [JsonPropertyName("size")] + public long? DatabaseSize { get; set; } - [JsonPropertyName("columns")] - public IEnumerable? Columns { get; set; } + [JsonPropertyName("unreferencedCount")] + public int? ColumnsUnreferencedCount { get; set; } - [JsonPropertyName("tables")] - public IEnumerable? Tables { get; set; } - } + [JsonPropertyName("autoLineBreakStyle")] + public DaxLineBreakStyle? AutoLineBreakStyle { get; set; } + + [JsonPropertyName("columns")] + public IEnumerable? Columns { get; set; } + + [JsonPropertyName("tables")] + public IEnumerable? Tables { get; set; } } diff --git a/src/Models/AnalyzeModel/TabularMeasure.cs b/src/Models/AnalyzeModel/TabularMeasure.cs index 35b9f45a..87bb556c 100644 --- a/src/Models/AnalyzeModel/TabularMeasure.cs +++ b/src/Models/AnalyzeModel/TabularMeasure.cs @@ -1,77 +1,76 @@ -namespace Sqlbi.Bravo.Models.AnalyzeModel -{ - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Services.DaxTemplate; - using Sqlbi.Bravo.Models.FormatDax; - using System.Diagnostics; - using System.Linq; - using System.Text.Json.Serialization; - using TOM = Microsoft.AnalysisServices.Tabular; +using System.Diagnostics; +using System.Linq; +using System.Text.Json.Serialization; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Services.DaxTemplate; +using Sqlbi.Bravo.Models.FormatDax; +using TOM = Microsoft.AnalysisServices.Tabular; - [DebuggerDisplay("'{TableName}'[{Name}]")] - public class TabularMeasure - { - [JsonPropertyName("etag")] - public string? ETag { get; set; } +namespace Sqlbi.Bravo.Models.AnalyzeModel; - [JsonPropertyName("name")] - public string? Name { get; set; } +[DebuggerDisplay("'{TableName}'[{Name}]")] +public class TabularMeasure +{ + [JsonPropertyName("etag")] + public string? ETag { get; set; } - [JsonPropertyName("tableName")] - public string? TableName { get; set; } + [JsonPropertyName("name")] + public string? Name { get; set; } - [JsonPropertyName("expression")] - public string? Expression { get; set; } + [JsonPropertyName("tableName")] + public string? TableName { get; set; } - [JsonPropertyName("displayFolder")] - public string? DisplayFolder { get; set; } + [JsonPropertyName("expression")] + public string? Expression { get; set; } - [JsonPropertyName("lineBreakStyle")] - public DaxLineBreakStyle? LineBreakStyle { get; set; } + [JsonPropertyName("displayFolder")] + public string? DisplayFolder { get; set; } - [JsonPropertyName("isHidden")] - public bool? IsHidden { get; set; } + [JsonPropertyName("lineBreakStyle")] + public DaxLineBreakStyle? LineBreakStyle { get; set; } - [JsonPropertyName("isManageDatesTimeIntelligence")] - public bool? IsManageDatesTimeIntelligence { get; set; } + [JsonPropertyName("isHidden")] + public bool? IsHidden { get; set; } - internal static TabularMeasure CreateFrom(Dax.Metadata.Measure daxMeasure, string databaseETag, TOM.Model? tomModel = default) - { - var (expression, lineBreakStyle) = daxMeasure.MeasureExpression?.Expression.NormalizeDax() ?? (null, DaxLineBreakStyle.None); + [JsonPropertyName("isManageDatesTimeIntelligence")] + public bool? IsManageDatesTimeIntelligence { get; set; } - var measure = new TabularMeasure - { - ETag = databaseETag, - Name = daxMeasure.MeasureName.Name, - TableName = daxMeasure.Table.TableName.Name, - Expression = expression ?? string.Empty, - DisplayFolder = daxMeasure.DisplayFolder?.Note, - LineBreakStyle = lineBreakStyle, - IsHidden = null, - IsManageDatesTimeIntelligence = null - }; + internal static TabularMeasure CreateFrom(Dax.Metadata.Measure daxMeasure, string databaseETag, TOM.Model? tomModel = default) + { + var (expression, lineBreakStyle) = daxMeasure.MeasureExpression?.Expression.NormalizeDax() ?? (null, DaxLineBreakStyle.None); - var tomMeasure = tomModel?.Tables?.FindMeasure(measure.TableName, measure.Name); - if (tomMeasure is not null) - { - measure.IsHidden = tomMeasure.IsHidden; - measure.IsManageDatesTimeIntelligence = tomMeasure.Annotations.Contains(DaxTemplateManager.SqlbiTemplateAnnotation); - } + var measure = new TabularMeasure + { + ETag = databaseETag, + Name = daxMeasure.MeasureName.Name, + TableName = daxMeasure.Table.TableName.Name, + Expression = expression ?? string.Empty, + DisplayFolder = daxMeasure.DisplayFolder?.Note, + LineBreakStyle = lineBreakStyle, + IsHidden = null, + IsManageDatesTimeIntelligence = null + }; - return measure; + var tomMeasure = tomModel?.Tables?.FindMeasure(measure.TableName, measure.Name); + if (tomMeasure is not null) + { + measure.IsHidden = tomMeasure.IsHidden; + measure.IsManageDatesTimeIntelligence = tomMeasure.Annotations.Contains(DaxTemplateManager.SqlbiTemplateAnnotation); } + + return measure; } +} - internal static class TabularMeasureExtensions +internal static class TabularMeasureExtensions +{ + public static DaxLineBreakStyle? GetAutoLineBreakStyle(this TabularMeasure[] measures) { - public static DaxLineBreakStyle? GetAutoLineBreakStyle(this TabularMeasure[] measures) - { - var preferredStyleQuery = measures.GroupBy((measure) => measure.LineBreakStyle) - .Select((group) => new { LineBreakStyle = group.Key, Count = group.Count() }) - .OrderByDescending((item) => item.Count) - .FirstOrDefault(); + var preferredStyleQuery = measures.GroupBy((measure) => measure.LineBreakStyle) + .Select((group) => new { LineBreakStyle = group.Key, Count = group.Count() }) + .OrderByDescending((item) => item.Count) + .FirstOrDefault(); - return preferredStyleQuery?.LineBreakStyle; - } + return preferredStyleQuery?.LineBreakStyle; } } diff --git a/src/Models/AnalyzeModel/TabularTable.cs b/src/Models/AnalyzeModel/TabularTable.cs index d8366582..6b750ae1 100644 --- a/src/Models/AnalyzeModel/TabularTable.cs +++ b/src/Models/AnalyzeModel/TabularTable.cs @@ -1,141 +1,140 @@ -namespace Sqlbi.Bravo.Models.AnalyzeModel +using System; +using System.Data; +using System.Diagnostics; +using System.Linq; +using System.Text.Json.Serialization; +using Dax.ViewModel; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Services.DaxTemplate; +using TOM = Microsoft.AnalysisServices.Tabular; + +namespace Sqlbi.Bravo.Models.AnalyzeModel; + +[DebuggerDisplay("{Name}")] +public class TabularTable { - using Dax.ViewModel; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Services.DaxTemplate; - using System; - using System.Data; - using System.Diagnostics; - using System.Linq; - using System.Text.Json.Serialization; - using TOM = Microsoft.AnalysisServices.Tabular; - - [DebuggerDisplay("{Name}")] - public class TabularTable - { - [JsonPropertyName("features")] - public TabularTableFeature Features { get; set; } = TabularTableFeature.All; + [JsonPropertyName("features")] + public TabularTableFeature Features { get; set; } = TabularTableFeature.All; - [JsonPropertyName("featureUnsupportedReasons")] - public TabularTableFeatureUnsupportedReason FeatureUnsupportedReasons { get; set; } = TabularTableFeatureUnsupportedReason.None; + [JsonPropertyName("featureUnsupportedReasons")] + public TabularTableFeatureUnsupportedReason FeatureUnsupportedReasons { get; set; } = TabularTableFeatureUnsupportedReason.None; - [JsonPropertyName("name")] - public string? Name { get; set; } + [JsonPropertyName("name")] + public string? Name { get; set; } - [JsonPropertyName("rowsCount")] - public long? RowsCount { get; set; } + [JsonPropertyName("rowsCount")] + public long? RowsCount { get; set; } - [JsonPropertyName("size")] - public long? Size { get; set; } + [JsonPropertyName("size")] + public long? Size { get; set; } - [JsonPropertyName("isDateTable")] - public bool? IsDateTable { get; set; } + [JsonPropertyName("isDateTable")] + public bool? IsDateTable { get; set; } - [JsonPropertyName("isHidden")] - public bool? IsHidden { get; set; } + [JsonPropertyName("isHidden")] + public bool? IsHidden { get; set; } - [JsonPropertyName("isQueryable")] - public bool? IsQueryable { get; set; } + [JsonPropertyName("isQueryable")] + public bool? IsQueryable { get; set; } - [JsonPropertyName("isManageDates")] - public bool? IsManageDates { get; set; } + [JsonPropertyName("isManageDates")] + public bool? IsManageDates { get; set; } - [JsonPropertyName("isDirectQuery")] - public bool? IsDirectQuery { get; set; } + [JsonPropertyName("isDirectQuery")] + public bool? IsDirectQuery { get; set; } - internal static TabularTable CreateFromDmvTables(IDataReader reader, string[] tablesWithColumns) + internal static TabularTable CreateFromDmvTables(IDataReader reader, string[] tablesWithColumns) + { + var table = new TabularTable { - var table = new TabularTable - { - Name = (string)reader["DIMENSION_UNIQUE_NAME"], - RowsCount = (uint)reader["DIMENSION_CARDINALITY"], - // Just using the 'DIMENSION_TYPE' property returns an incomplete result compared to the one obtained via VertipaqAnalyzer, so the result may be different in some cases - IsDateTable = ((short)reader["DIMENSION_TYPE"]) == 1, /* MD_DIMTYPE_TIME */ - }; - - table.Name = table.Name.GetDaxName(); - - if (!tablesWithColumns.Contains(table.Name)) - { - table.Features &= ~TabularTableFeature.ExportData; - table.FeatureUnsupportedReasons |= TabularTableFeatureUnsupportedReason.ExportDataNoColumns; - } - - return table; - } + Name = (string)reader["DIMENSION_UNIQUE_NAME"], + RowsCount = (uint)reader["DIMENSION_CARDINALITY"], + // Just using the 'DIMENSION_TYPE' property returns an incomplete result compared to the one obtained via VertipaqAnalyzer, so the result may be different in some cases + IsDateTable = ((short)reader["DIMENSION_TYPE"]) == 1, /* MD_DIMTYPE_TIME */ + }; - internal static TabularTable CreateFrom(VpaTable vpaTable, TOM.Model? tomModel = default) + table.Name = table.Name.GetDaxName(); + + if (!tablesWithColumns.Contains(table.Name)) { - var table = new TabularTable - { - Name = vpaTable.TableName, - RowsCount = vpaTable.RowsCount, - Size = vpaTable.TableSize, - IsDateTable = vpaTable.IsDateTable, - IsHidden = null, - IsQueryable = null, - IsManageDates = null, - IsDirectQuery = vpaTable.HasDirectQueryPartitions, - }; - - var tomTable = tomModel?.Tables.Find(table.Name); - if (tomTable is not null) - { - table.IsHidden = tomTable.IsHidden; - table.IsQueryable = tomTable.IsQueryable(); - table.IsManageDates = tomTable.Annotations.Contains(DaxTemplateManager.SqlbiTemplateAnnotation); - } - - if (table.IsQueryable == false) - { - table.Features &= ~TabularTableFeature.ExportData; - table.FeatureUnsupportedReasons |= TabularTableFeatureUnsupportedReason.ExportDataNotQueryable; - } - - if (vpaTable.ColumnsNumber == 0L || (vpaTable.ColumnsNumber == 1L && vpaTable.Columns.Single().IsRowNumber)) - { - table.Features &= ~TabularTableFeature.ExportData; - table.FeatureUnsupportedReasons |= TabularTableFeatureUnsupportedReason.ExportDataNoColumns; - } - - return table; + table.Features &= ~TabularTableFeature.ExportData; + table.FeatureUnsupportedReasons |= TabularTableFeatureUnsupportedReason.ExportDataNoColumns; } + + return table; } - [Flags] - public enum TabularTableFeature + internal static TabularTable CreateFrom(VpaTable vpaTable, TOM.Model? tomModel = default) { - None = 0, + var table = new TabularTable + { + Name = vpaTable.TableName, + RowsCount = vpaTable.RowsCount, + Size = vpaTable.TableSize, + IsDateTable = vpaTable.IsDateTable, + IsHidden = null, + IsQueryable = null, + IsManageDates = null, + IsDirectQuery = vpaTable.HasDirectQueryPartitions, + }; + + var tomTable = tomModel?.Tables.Find(table.Name); + if (tomTable is not null) + { + table.IsHidden = tomTable.IsHidden; + table.IsQueryable = tomTable.IsQueryable(); + table.IsManageDates = tomTable.Annotations.Contains(DaxTemplateManager.SqlbiTemplateAnnotation); + } - // AnalyzeModel range << 100, - // FormatDax range << 200, - // ManageDates range << 300, + if (table.IsQueryable == false) + { + table.Features &= ~TabularTableFeature.ExportData; + table.FeatureUnsupportedReasons |= TabularTableFeatureUnsupportedReason.ExportDataNotQueryable; + } - ExportData = 1 << 400, + if (vpaTable.ColumnsNumber == 0L || (vpaTable.ColumnsNumber == 1L && vpaTable.Columns.Single().IsRowNumber)) + { + table.Features &= ~TabularTableFeature.ExportData; + table.FeatureUnsupportedReasons |= TabularTableFeatureUnsupportedReason.ExportDataNoColumns; + } - All = ExportData, + return table; } +} - [Flags] - public enum TabularTableFeatureUnsupportedReason - { - None = 0, - - // AnalyzeModel range << 100, - // FormatDax range << 200, - // ManageDates range << 300, - - /// - /// The table has no columns so it cannot be used as an export data source - /// - /// - /// https://github.com/sql-bi/Bravo/issues/128 "Query (%, %) Table '%' cannot be used in computations because it does not have any columns." - /// - ExportDataNoColumns = 1 << 400, - - /// - /// The table has columns that are not queryable (i.e. TOM.ObjectState IN CalculationNeeded, SemanticError, EvaluationError, SyntaxError, ...) - /// - ExportDataNotQueryable = 1 << 401, - } +[Flags] +public enum TabularTableFeature +{ + None = 0, + + // AnalyzeModel range << 100, + // FormatDax range << 200, + // ManageDates range << 300, + + ExportData = 1 << 400, + + All = ExportData, +} + +[Flags] +public enum TabularTableFeatureUnsupportedReason +{ + None = 0, + + // AnalyzeModel range << 100, + // FormatDax range << 200, + // ManageDates range << 300, + + /// + /// The table has no columns so it cannot be used as an export data source + /// + /// + /// https://github.com/sql-bi/Bravo/issues/128 "Query (%, %) Table '%' cannot be used in computations because it does not have any columns." + /// + ExportDataNoColumns = 1 << 400, + + /// + /// The table has columns that are not queryable (i.e. TOM.ObjectState IN CalculationNeeded, SemanticError, EvaluationError, SyntaxError, ...) + /// + ExportDataNotQueryable = 1 << 401, } diff --git a/src/Models/Authentication/AccountDto.cs b/src/Models/Authentication/AccountDto.cs index 6a19a788..71526f40 100644 --- a/src/Models/Authentication/AccountDto.cs +++ b/src/Models/Authentication/AccountDto.cs @@ -1,20 +1,18 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; +using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Authentication; -namespace Sqlbi.Bravo.Models.Authentication -{ - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Authentication; +namespace Sqlbi.Bravo.Models.Authentication; - public sealed record AccountDto( - [Required] [property: JsonPropertyName("id")] string Identifier, - [Required] string Email, - [Required] string Username); +public sealed record AccountDto( + [Required] [property: JsonPropertyName("id")] string Identifier, + [Required] string Email, + [Required] string Username); - internal static class AccountDtoMappingExtensions - { - internal static AccountDto ToDto(this AuthenticationResult authenticationResult) => new( - authenticationResult.Identifier, - authenticationResult.Email, - authenticationResult.Name); - } +internal static class AccountDtoMappingExtensions +{ + internal static AccountDto ToDto(this AuthenticationResult authenticationResult) => new( + authenticationResult.Identifier, + authenticationResult.Email, + authenticationResult.Name); } diff --git a/src/Models/Authentication/CloudEnvironmentDto.cs b/src/Models/Authentication/CloudEnvironmentDto.cs index 7428c5db..57bca35a 100644 --- a/src/Models/Authentication/CloudEnvironmentDto.cs +++ b/src/Models/Authentication/CloudEnvironmentDto.cs @@ -1,38 +1,37 @@ -using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; +using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; -namespace Sqlbi.Bravo.Models.Authentication -{ - public sealed record CloudEnvironmentDto( - [Required] string Name, - [Required] string Description, - [Required] string AuthorityUri, - [Required] string ClientId, - [Required] string RedirectUri, - [Required] string ResourceId, - [Required] string BackendUri, - [Required(AllowEmptyStrings = true)] string ClusterUri); +namespace Sqlbi.Bravo.Models.Authentication; + +public sealed record CloudEnvironmentDto( + [Required] string Name, + [Required] string Description, + [Required] string AuthorityUri, + [Required] string ClientId, + [Required] string RedirectUri, + [Required] string ResourceId, + [Required] string BackendUri, + [Required(AllowEmptyStrings = true)] string ClusterUri); - internal static class CloudEnvironmentDtoMappingExtensions - { - internal static CloudEnvironmentDto ToDto(this CloudEnvironment model) => new( - model.Name, - model.Description, - model.AuthorityUri, - model.ClientId, - model.RedirectUri, - model.ResourceId, - model.BackendUri, - model.ClusterUri); +internal static class CloudEnvironmentDtoMappingExtensions +{ + internal static CloudEnvironmentDto ToDto(this CloudEnvironment model) => new( + model.Name, + model.Description, + model.AuthorityUri, + model.ClientId, + model.RedirectUri, + model.ResourceId, + model.BackendUri, + model.ClusterUri); - internal static CloudEnvironment ToModel(this CloudEnvironmentDto dto) => new( - dto.Name, - dto.Description, - dto.AuthorityUri, - dto.ClientId, - dto.RedirectUri, - dto.ResourceId, - dto.BackendUri, - dto.ClusterUri); - } + internal static CloudEnvironment ToModel(this CloudEnvironmentDto dto) => new( + dto.Name, + dto.Description, + dto.AuthorityUri, + dto.ClientId, + dto.RedirectUri, + dto.ResourceId, + dto.BackendUri, + dto.ClusterUri); } diff --git a/src/Models/Authentication/GetEnvironmentsRequest.cs b/src/Models/Authentication/GetEnvironmentsRequest.cs index a280ee98..f6f4a882 100644 --- a/src/Models/Authentication/GetEnvironmentsRequest.cs +++ b/src/Models/Authentication/GetEnvironmentsRequest.cs @@ -1,7 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Sqlbi.Bravo.Models.Authentication -{ - public sealed record GetEnvironmentsRequest( - [Required] string Email); -} +namespace Sqlbi.Bravo.Models.Authentication; + +public sealed record GetEnvironmentsRequest( + [Required] string Email); diff --git a/src/Models/Authentication/GetEnvironmentsResponse.cs b/src/Models/Authentication/GetEnvironmentsResponse.cs index d5f5f4a2..2efcbdc8 100644 --- a/src/Models/Authentication/GetEnvironmentsResponse.cs +++ b/src/Models/Authentication/GetEnvironmentsResponse.cs @@ -1,9 +1,10 @@ -using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; +using System.Collections.Generic; +using System.Linq; +using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; -namespace Sqlbi.Bravo.Models.Authentication +namespace Sqlbi.Bravo.Models.Authentication; + +public sealed class GetEnvironmentsResponse(IEnumerable environments) { - public sealed class GetEnvironmentsResponse(IEnumerable environments) - { - public IReadOnlyList Environments { get; } = [.. environments.Select(e => e.ToDto())]; - } + public IReadOnlyList Environments { get; } = [.. environments.Select(e => e.ToDto())]; } diff --git a/src/Models/Authentication/SignInRequest.cs b/src/Models/Authentication/SignInRequest.cs index 43367042..61898d91 100644 --- a/src/Models/Authentication/SignInRequest.cs +++ b/src/Models/Authentication/SignInRequest.cs @@ -1,8 +1,7 @@ using System.ComponentModel.DataAnnotations; -namespace Sqlbi.Bravo.Models.Authentication -{ - public sealed record SignInRequest( - [Required] string Email, - [Required] CloudEnvironmentDto Environment); -} +namespace Sqlbi.Bravo.Models.Authentication; + +public sealed record SignInRequest( + [Required] string Email, + [Required] CloudEnvironmentDto Environment); diff --git a/src/Models/Authentication/SignInResponse.cs b/src/Models/Authentication/SignInResponse.cs index 1f710a22..7f5f9f59 100644 --- a/src/Models/Authentication/SignInResponse.cs +++ b/src/Models/Authentication/SignInResponse.cs @@ -1,9 +1,8 @@ -namespace Sqlbi.Bravo.Models.Authentication -{ - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Authentication; +using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Authentication; + +namespace Sqlbi.Bravo.Models.Authentication; - public sealed class SignInResponse(AuthenticationResult authenticationResult) - { - public AccountDto Account { get; } = authenticationResult.ToDto(); - } +public sealed class SignInResponse(AuthenticationResult authenticationResult) +{ + public AccountDto Account { get; } = authenticationResult.ToDto(); } diff --git a/src/Models/BravoOptions.cs b/src/Models/BravoOptions.cs index a6a604ea..74b5f503 100644 --- a/src/Models/BravoOptions.cs +++ b/src/Models/BravoOptions.cs @@ -1,96 +1,95 @@ -namespace Sqlbi.Bravo.Models +using System.Text.Json; +using System.Text.Json.Serialization; +using Sqlbi.Bravo.Infrastructure.Configuration; +using Sqlbi.Bravo.Infrastructure.Configuration.Settings; + +namespace Sqlbi.Bravo.Models; + +public class BravoOptions : IUserSettings { - using Sqlbi.Bravo.Infrastructure.Configuration; - using Sqlbi.Bravo.Infrastructure.Configuration.Settings; - using System.Text.Json; - using System.Text.Json.Serialization; + public BravoOptions() + { + } - public class BravoOptions: IUserSettings + private BravoOptions(IUserSettings userSettings) { - public BravoOptions() - { - } + TelemetryEnabled = userSettings.TelemetryEnabled; + DiagnosticLevel = userSettings.DiagnosticLevel; + UpdateChannel = userSettings.UpdateChannel; + UpdateCheckEnabled = userSettings.UpdateCheckEnabled; + Theme = userSettings.Theme; + Proxy = userSettings.Proxy; + UseSystemBrowserForAuthentication = userSettings.UseSystemBrowserForAuthentication; + CustomTemplatesEnabled = userSettings.CustomTemplatesEnabled; + CustomOptions = userSettings.CustomOptions; + } - private BravoOptions(IUserSettings userSettings) - { - TelemetryEnabled = userSettings.TelemetryEnabled; - DiagnosticLevel = userSettings.DiagnosticLevel; - UpdateChannel = userSettings.UpdateChannel; - UpdateCheckEnabled = userSettings.UpdateCheckEnabled; - Theme = userSettings.Theme; - Proxy = userSettings.Proxy; - UseSystemBrowserForAuthentication = userSettings.UseSystemBrowserForAuthentication; - CustomTemplatesEnabled = userSettings.CustomTemplatesEnabled; - CustomOptions = userSettings.CustomOptions; - } + [JsonPropertyName("telemetryEnabled")] + public bool TelemetryEnabled { get; set; } = UserSettings.DefaultTelemetryEnabled; - [JsonPropertyName("telemetryEnabled")] - public bool TelemetryEnabled { get; set; } = UserSettings.DefaultTelemetryEnabled; + [JsonPropertyName("diagnosticLevel")] + public DiagnosticLevelType DiagnosticLevel { get; set; } = UserSettings.DefaultDiagnosticLevel; - [JsonPropertyName("diagnosticLevel")] - public DiagnosticLevelType DiagnosticLevel { get; set; } = UserSettings.DefaultDiagnosticLevel; + [JsonPropertyName("updateChannel")] + public UpdateChannelType UpdateChannel { get; set; } = UserSettings.DefaultUpdateChannel; - [JsonPropertyName("updateChannel")] - public UpdateChannelType UpdateChannel { get; set; } = UserSettings.DefaultUpdateChannel; + [JsonPropertyName("updateCheckEnabled")] + public bool UpdateCheckEnabled { get; set; } = UserSettings.DefaultUpdateCheckEnabled; - [JsonPropertyName("updateCheckEnabled")] - public bool UpdateCheckEnabled { get; set; } = UserSettings.DefaultUpdateCheckEnabled; + [JsonPropertyName("theme")] + public ThemeType Theme { get; set; } = UserSettings.DefaultTheme; - [JsonPropertyName("theme")] - public ThemeType Theme { get; set; } = UserSettings.DefaultTheme; + [JsonPropertyName("proxy")] + public ProxySettings? Proxy { get; set; } - [JsonPropertyName("proxy")] - public ProxySettings? Proxy { get; set; } + [JsonPropertyName("useSystemBrowserForAuthentication")] + public bool UseSystemBrowserForAuthentication { get; set; } = UserSettings.DefaultUseSystemBrowserForAuthentication; - [JsonPropertyName("useSystemBrowserForAuthentication")] - public bool UseSystemBrowserForAuthentication { get; set; } = UserSettings.DefaultUseSystemBrowserForAuthentication; + [JsonPropertyName("customTemplatesEnabled")] + public bool CustomTemplatesEnabled { get; set; } = UserSettings.DefaultCustomTemplatesEnabled; - [JsonPropertyName("customTemplatesEnabled")] - public bool CustomTemplatesEnabled { get; set; } = UserSettings.DefaultCustomTemplatesEnabled; + [JsonPropertyName("customOptions")] + public JsonElement? CustomOptions { get; set; } - [JsonPropertyName("customOptions")] - public JsonElement? CustomOptions { get; set; } + public static BravoOptions CreateFromUserPreferences() + { + var options = new BravoOptions(UserPreferences.Current); + return options; + } - public static BravoOptions CreateFromUserPreferences() + public void SaveToUserPreferences() + { + Validate(); + + var settings = UserPreferences.Current; { - var options = new BravoOptions(UserPreferences.Current); - return options; + settings.TelemetryEnabled = TelemetryEnabled; + settings.DiagnosticLevel = DiagnosticLevel; + settings.UpdateChannel = UpdateChannel; + settings.UpdateCheckEnabled = UpdateCheckEnabled; + settings.Theme = Theme; + settings.Proxy = Proxy; + settings.UseSystemBrowserForAuthentication = UseSystemBrowserForAuthentication; + settings.CustomTemplatesEnabled = CustomTemplatesEnabled; + settings.CustomOptions = CustomOptions; } + UserPreferences.Save(); + } - public void SaveToUserPreferences() + private bool Validate(bool throwOnError = true) + { + try { - Validate(); - - var settings = UserPreferences.Current; - { - settings.TelemetryEnabled = TelemetryEnabled; - settings.DiagnosticLevel = DiagnosticLevel; - settings.UpdateChannel = UpdateChannel; - settings.UpdateCheckEnabled = UpdateCheckEnabled; - settings.Theme = Theme; - settings.Proxy = Proxy; - settings.UseSystemBrowserForAuthentication = UseSystemBrowserForAuthentication; - settings.CustomTemplatesEnabled = CustomTemplatesEnabled; - settings.CustomOptions = CustomOptions; - } - UserPreferences.Save(); - } + Proxy?.Validate(throwOnError); - private bool Validate(bool throwOnError = true) + return true; + } + catch { - try - { - Proxy?.Validate(throwOnError); - - return true; - } - catch - { - if (throwOnError) - throw; - - return false; - } + if (throwOnError) + throw; + + return false; } } } diff --git a/src/Models/BravoUpdate.cs b/src/Models/BravoUpdate.cs index 1ff59ffe..17db1e94 100644 --- a/src/Models/BravoUpdate.cs +++ b/src/Models/BravoUpdate.cs @@ -1,36 +1,35 @@ -namespace Sqlbi.Bravo.Models -{ - using Sqlbi.Bravo.Infrastructure.Configuration.Settings; - using System.Text.Json.Serialization; +using System.Text.Json.Serialization; +using Sqlbi.Bravo.Infrastructure.Configuration.Settings; - public interface IUpdateInfo - { - UpdateChannelType? UpdateChannel { get; set; } +namespace Sqlbi.Bravo.Models; - bool IsNewerVersion { get; set; } +public interface IUpdateInfo +{ + UpdateChannelType? UpdateChannel { get; set; } - string? Version { get; set; } + bool IsNewerVersion { get; set; } - string? DownloadUrl { get; set; } + string? Version { get; set; } - string? ChangelogUrl { get; set; } - } + string? DownloadUrl { get; set; } - public class BravoUpdate : IUpdateInfo - { - [JsonPropertyName("updateChannel")] - public UpdateChannelType? UpdateChannel { get; set; } + string? ChangelogUrl { get; set; } +} + +public class BravoUpdate : IUpdateInfo +{ + [JsonPropertyName("updateChannel")] + public UpdateChannelType? UpdateChannel { get; set; } - [JsonPropertyName("isNewerVersion")] - public bool IsNewerVersion { get; set; } = false; + [JsonPropertyName("isNewerVersion")] + public bool IsNewerVersion { get; set; } = false; - [JsonPropertyName("version")] - public string? Version { get; set; } + [JsonPropertyName("version")] + public string? Version { get; set; } - [JsonPropertyName("downloadUrl")] - public string? DownloadUrl { get; set; } + [JsonPropertyName("downloadUrl")] + public string? DownloadUrl { get; set; } - [JsonPropertyName("changelogUrl")] - public string? ChangelogUrl { get; set; } - } + [JsonPropertyName("changelogUrl")] + public string? ChangelogUrl { get; set; } } diff --git a/src/Models/DiagnosticMessage.cs b/src/Models/DiagnosticMessage.cs index fe30ce5f..5e5fe9e8 100644 --- a/src/Models/DiagnosticMessage.cs +++ b/src/Models/DiagnosticMessage.cs @@ -1,53 +1,52 @@ -namespace Sqlbi.Bravo.Models -{ - using System; - using System.Text.Json.Serialization; +using System; +using System.Text.Json.Serialization; - public class DiagnosticMessage - { - [JsonPropertyName("type")] - public DiagnosticMessageType Type { get; set; } = DiagnosticMessageType.Text; +namespace Sqlbi.Bravo.Models; - [JsonPropertyName("severity")] - public DiagnosticMessageSeverity Severity { get; set; } = DiagnosticMessageSeverity.None; +public class DiagnosticMessage +{ + [JsonPropertyName("type")] + public DiagnosticMessageType Type { get; set; } = DiagnosticMessageType.Text; - [JsonPropertyName("name")] - public string? Name { get; set; } + [JsonPropertyName("severity")] + public DiagnosticMessageSeverity Severity { get; set; } = DiagnosticMessageSeverity.None; - [JsonPropertyName("content")] - public string? Content { get; set; } + [JsonPropertyName("name")] + public string? Name { get; set; } - [JsonPropertyName("timestamp")] - public DateTime Timestamp { get; set; } = DateTime.UtcNow; + [JsonPropertyName("content")] + public string? Content { get; set; } - [JsonIgnore] - public DateTime? ReadTimestamp { get; set; } + [JsonPropertyName("timestamp")] + public DateTime Timestamp { get; set; } = DateTime.UtcNow; - internal static DiagnosticMessage Create(DiagnosticMessageType type, DiagnosticMessageSeverity severity, string name, string content) - { - var message = new DiagnosticMessage - { - Type = type, - Severity = severity, - Name = $"[HOST] { name }", - Content = content, - ReadTimestamp = null - }; - - return message; - } - } + [JsonIgnore] + public DateTime? ReadTimestamp { get; set; } - public enum DiagnosticMessageType + internal static DiagnosticMessage Create(DiagnosticMessageType type, DiagnosticMessageSeverity severity, string name, string content) { - Text = 0, - Json = 1, + var message = new DiagnosticMessage + { + Type = type, + Severity = severity, + Name = $"[HOST] {name}", + Content = content, + ReadTimestamp = null + }; + + return message; } +} - public enum DiagnosticMessageSeverity - { - None = 0, - Warning = 1, - Error = 2, - } +public enum DiagnosticMessageType +{ + Text = 0, + Json = 1, +} + +public enum DiagnosticMessageSeverity +{ + None = 0, + Warning = 1, + Error = 2, } diff --git a/src/Models/ExportData/ExportDataEntity.cs b/src/Models/ExportData/ExportDataEntity.cs index f65de962..7e72b702 100644 --- a/src/Models/ExportData/ExportDataEntity.cs +++ b/src/Models/ExportData/ExportDataEntity.cs @@ -1,69 +1,68 @@ -namespace Sqlbi.Bravo.Models.ExportData -{ - using System.Collections.Generic; - using System.Text.Json.Serialization; +using System.Collections.Generic; +using System.Text.Json.Serialization; - public abstract class ExportDataEntity - { - [JsonPropertyName("status")] - public ExportDataStatus Status { get; set; } +namespace Sqlbi.Bravo.Models.ExportData; - public void SetRunning() => Status = ExportDataStatus.Running; +public abstract class ExportDataEntity +{ + [JsonPropertyName("status")] + public ExportDataStatus Status { get; set; } - public void SetCompleted() => Status = ExportDataStatus.Completed; + public void SetRunning() => Status = ExportDataStatus.Running; - } + public void SetCompleted() => Status = ExportDataStatus.Completed; - public class ExportDataJob : ExportDataEntity - { - [JsonPropertyName("path")] - public string? Path { get; set; } +} + +public class ExportDataJob : ExportDataEntity +{ + [JsonPropertyName("path")] + public string? Path { get; set; } - [JsonPropertyName("tables")] - public HashSet Tables { get; set; } = new(); + [JsonPropertyName("tables")] + public HashSet Tables { get; set; } = new(); - public void SetCanceled() => Status = ExportDataStatus.Canceled; + public void SetCanceled() => Status = ExportDataStatus.Canceled; - public void SetFailed() => Status = ExportDataStatus.Failed; + public void SetFailed() => Status = ExportDataStatus.Failed; - public static ExportDataJob CreateFrom(ExportDataSettings settings) + public static ExportDataJob CreateFrom(ExportDataSettings settings) + { + var job = new ExportDataJob { - var job = new ExportDataJob - { - Path = settings.ExportPath - }; + Path = settings.ExportPath + }; - return job; - } + return job; } +} - public class ExportDataTable : ExportDataEntity - { - [JsonPropertyName("name")] - public string? Name { get; set; } +public class ExportDataTable : ExportDataEntity +{ + [JsonPropertyName("name")] + public string? Name { get; set; } - [JsonPropertyName("rows")] - public int Rows { get; set; } = 0; + [JsonPropertyName("rows")] + public int Rows { get; set; } = 0; - [JsonPropertyName("columns")] - public int Columns { get; set; } = 0; + [JsonPropertyName("columns")] + public int Columns { get; set; } = 0; - public void SetTruncated() => Status = ExportDataStatus.Truncated; - } + public void SetTruncated() => Status = ExportDataStatus.Truncated; +} - internal static class ExportDataJobExtensions +internal static class ExportDataJobExtensions +{ + public static ExportDataTable AddNew(this ExportDataJob job, string name) { - public static ExportDataTable AddNew(this ExportDataJob job, string name) + var table = new ExportDataTable { - var table = new ExportDataTable - { - Name = name, - }; + Name = name, + }; - job.Tables.Add(table); - table.SetRunning(); + job.Tables.Add(table); + table.SetRunning(); - return table; - } + return table; } } diff --git a/src/Models/ExportData/ExportDataSettings.cs b/src/Models/ExportData/ExportDataSettings.cs index 4f199d2f..9fcdfdab 100644 --- a/src/Models/ExportData/ExportDataSettings.cs +++ b/src/Models/ExportData/ExportDataSettings.cs @@ -1,63 +1,62 @@ -namespace Sqlbi.Bravo.Models.ExportData +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Globalization; +using System.IO; +using System.Text.Json.Serialization; +using Sqlbi.Bravo.Infrastructure.Models; +using Sqlbi.Bravo.Models.AnalyzeModel; + +namespace Sqlbi.Bravo.Models.ExportData; + +public abstract class ExportDataSettings +{ + /// + /// Tables to export + /// + [Required] + [JsonPropertyName("tables")] + public ICollection Tables { get; set; } = Array.Empty(); + + /// + /// Full local path where the files will be created + /// + [JsonIgnore] + public string ExportPath { get; set; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments, Environment.SpecialFolderOption.DoNotVerify), $"BravoExportData-{DateTime.Now:yyyyMMddHHmmss}"); +} + +public class ExportDelimitedTextSettings : ExportDataSettings +{ + /// + /// Specifies whether UTF-16 should be used as the character encoding for the file, otherwise UTF-8 is used as default + /// + [JsonPropertyName("unicodeEncoding")] + public bool UnicodeEncoding { get; set; } = false; + + /// + /// Specifies the delimiter used to separate fields. If not provided is used as default + /// + [JsonPropertyName("delimiter")] + public string? Delimiter { get; set; } + + /// + /// Specifies if all string fields should be quoted. Default is false + /// + [JsonPropertyName("quoteStringFields")] + public bool QuoteStringFields { get; set; } = false; + + /// + /// Specifies whether to export the data to a subfolder with the same name as the source + /// + [JsonPropertyName("createSubfolder")] + public bool CreateSubfolder { get; set; } = false; +} + +public class ExportExcelSettings : ExportDataSettings { - using Sqlbi.Bravo.Infrastructure.Models; - using Sqlbi.Bravo.Models.AnalyzeModel; - using System; - using System.Collections.Generic; - using System.ComponentModel.DataAnnotations; - using System.Globalization; - using System.IO; - using System.Text.Json.Serialization; - - public abstract class ExportDataSettings - { - /// - /// Tables to export - /// - [Required] - [JsonPropertyName("tables")] - public ICollection Tables { get; set; } = Array.Empty(); - - /// - /// Full local path where the files will be created - /// - [JsonIgnore] - public string ExportPath { get; set; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments, Environment.SpecialFolderOption.DoNotVerify), $"BravoExportData-{DateTime.Now:yyyyMMddHHmmss}"); - } - - public class ExportDelimitedTextSettings : ExportDataSettings - { - /// - /// Specifies whether UTF-16 should be used as the character encoding for the file, otherwise UTF-8 is used as default - /// - [JsonPropertyName("unicodeEncoding")] - public bool UnicodeEncoding { get; set; } = false; - - /// - /// Specifies the delimiter used to separate fields. If not provided is used as default - /// - [JsonPropertyName("delimiter")] - public string? Delimiter { get; set; } - - /// - /// Specifies if all string fields should be quoted. Default is false - /// - [JsonPropertyName("quoteStringFields")] - public bool QuoteStringFields { get; set; } = false; - - /// - /// Specifies whether to export the data to a subfolder with the same name as the source - /// - [JsonPropertyName("createSubfolder")] - public bool CreateSubfolder { get; set; } = false; - } - - public class ExportExcelSettings : ExportDataSettings - { - /// - /// Specifies whether an export summary worksheet should be created - /// - [JsonPropertyName("createExportSummary")] - public bool CreateExportSummary { get; set; } = true; - } + /// + /// Specifies whether an export summary worksheet should be created + /// + [JsonPropertyName("createExportSummary")] + public bool CreateExportSummary { get; set; } = true; } diff --git a/src/Models/ExportData/ExportDataStatus.cs b/src/Models/ExportData/ExportDataStatus.cs index 2fe4e450..c7245da7 100644 --- a/src/Models/ExportData/ExportDataStatus.cs +++ b/src/Models/ExportData/ExportDataStatus.cs @@ -1,33 +1,32 @@ -namespace Sqlbi.Bravo.Models.ExportData +namespace Sqlbi.Bravo.Models.ExportData; + +public enum ExportDataStatus { - public enum ExportDataStatus - { - Unknown = 0, + Unknown = 0, - /// - /// Data export is running. Applies to and - /// - Running = 1, + /// + /// Data export is running. Applies to and + /// + Running = 1, - /// - /// Data export is completed. Applies to and - /// - Completed = 2, + /// + /// Data export is completed. Applies to and + /// + Completed = 2, - /// - /// Data export was canceled. Only applies to - /// - Canceled = 3, + /// + /// Data export was canceled. Only applies to + /// + Canceled = 3, - /// - /// Data export was failed due to an error. Only applies to - /// - Failed = 4, + /// + /// Data export was failed due to an error. Only applies to + /// + Failed = 4, - /// - /// Data export was interrupted due to reaching the limit allowed by the data destination. Only applies - /// - /// Excel cannot exceed the limit of 1,048,576 rows - Truncated = 5, - } + /// + /// Data export was interrupted due to reaching the limit allowed by the data destination. Only applies + /// + /// Excel cannot exceed the limit of 1,048,576 rows + Truncated = 5, } diff --git a/src/Models/ExportData/ExportDelimitedTextRequest.cs b/src/Models/ExportData/ExportDelimitedTextRequest.cs index c674c4c3..d8ecef65 100644 --- a/src/Models/ExportData/ExportDelimitedTextRequest.cs +++ b/src/Models/ExportData/ExportDelimitedTextRequest.cs @@ -1,26 +1,25 @@ -namespace Sqlbi.Bravo.Models.ExportData -{ - using System.ComponentModel.DataAnnotations; - using System.Text.Json.Serialization; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; - public class ExportDelimitedTextRequest - { - [Required] - [JsonPropertyName("settings")] - public ExportDelimitedTextSettings? Settings { get; set; } - } +namespace Sqlbi.Bravo.Models.ExportData; - public class ExportDelimitedTextFromPBIReportRequest : ExportDelimitedTextRequest - { - [Required] - [JsonPropertyName("report")] - public PBIDesktopReport? Report { get; set; } - } +public class ExportDelimitedTextRequest +{ + [Required] + [JsonPropertyName("settings")] + public ExportDelimitedTextSettings? Settings { get; set; } +} - public class ExportDelimitedTextFromPBICloudDatasetRequest : ExportDelimitedTextRequest - { - [Required] - [JsonPropertyName("dataset")] - public PBICloudDataset? Dataset { get; set; } - } +public class ExportDelimitedTextFromPBIReportRequest : ExportDelimitedTextRequest +{ + [Required] + [JsonPropertyName("report")] + public PBIDesktopReport? Report { get; set; } +} + +public class ExportDelimitedTextFromPBICloudDatasetRequest : ExportDelimitedTextRequest +{ + [Required] + [JsonPropertyName("dataset")] + public PBICloudDataset? Dataset { get; set; } } diff --git a/src/Models/ExportData/ExportExcelRequest.cs b/src/Models/ExportData/ExportExcelRequest.cs index 9359b9ac..df7acd37 100644 --- a/src/Models/ExportData/ExportExcelRequest.cs +++ b/src/Models/ExportData/ExportExcelRequest.cs @@ -1,26 +1,25 @@ -namespace Sqlbi.Bravo.Models.ExportData -{ - using System.ComponentModel.DataAnnotations; - using System.Text.Json.Serialization; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; - public abstract class ExportExcelRequest - { - [Required] - [JsonPropertyName("settings")] - public ExportExcelSettings? Settings { get; set; } - } +namespace Sqlbi.Bravo.Models.ExportData; - public class ExportExcelFromPBIReportRequest : ExportExcelRequest - { - [Required] - [JsonPropertyName("report")] - public PBIDesktopReport? Report { get; set; } - } +public abstract class ExportExcelRequest +{ + [Required] + [JsonPropertyName("settings")] + public ExportExcelSettings? Settings { get; set; } +} - public class ExportExcelFromPBICloudDatasetRequest : ExportExcelRequest - { - [Required] - [JsonPropertyName("dataset")] - public PBICloudDataset? Dataset { get; set; } - } +public class ExportExcelFromPBIReportRequest : ExportExcelRequest +{ + [Required] + [JsonPropertyName("report")] + public PBIDesktopReport? Report { get; set; } +} + +public class ExportExcelFromPBICloudDatasetRequest : ExportExcelRequest +{ + [Required] + [JsonPropertyName("dataset")] + public PBICloudDataset? Dataset { get; set; } } diff --git a/src/Models/FormatDax/DatabaseUpdateResult.cs b/src/Models/FormatDax/DatabaseUpdateResult.cs index fc5b2e98..d727893f 100644 --- a/src/Models/FormatDax/DatabaseUpdateResult.cs +++ b/src/Models/FormatDax/DatabaseUpdateResult.cs @@ -1,14 +1,13 @@ -namespace Sqlbi.Bravo.Models.FormatDax -{ - using System.Text.Json.Serialization; +using System.Text.Json.Serialization; + +namespace Sqlbi.Bravo.Models.FormatDax; - // TODO: rename to 'UpdateResponse' - public class DatabaseUpdateResult - { - /// - /// The unique identifier of the current version of the tabular model computed after the update - /// - [JsonPropertyName("etag")] - public string? DatabaseETag { get; set; } - } +// TODO: rename to 'UpdateResponse' +public class DatabaseUpdateResult +{ + /// + /// The unique identifier of the current version of the tabular model computed after the update + /// + [JsonPropertyName("etag")] + public string? DatabaseETag { get; set; } } diff --git a/src/Models/FormatDax/FormatDaxRequest.cs b/src/Models/FormatDax/FormatDaxRequest.cs index 6569fb55..0ce297a0 100644 --- a/src/Models/FormatDax/FormatDaxRequest.cs +++ b/src/Models/FormatDax/FormatDaxRequest.cs @@ -1,93 +1,92 @@ -namespace Sqlbi.Bravo.Models.FormatDax +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using Dax.Formatter.Models; +using Sqlbi.Bravo.Infrastructure; +using Sqlbi.Bravo.Models.AnalyzeModel; +using TOM = Microsoft.AnalysisServices; + +namespace Sqlbi.Bravo.Models.FormatDax; + +public class FormatDaxRequest +{ + [Required] + [JsonPropertyName("options")] + public FormatDaxOptions? Options { get; set; } + + [Required] + [JsonPropertyName("measures")] + public IEnumerable? Measures { get; set; } +} + +public class FormatDaxOptions +{ + /// + /// Auto-calculated based on the existing measures in the model. See + /// + [Required] + [JsonPropertyName("autoLineBreakStyle")] + public DaxLineBreakStyle? AutoLineBreakStyle { get; set; } + + /// + /// Preferred from user settings + /// + [JsonPropertyName("lineBreakStyle")] + public DaxLineBreakStyle LineBreakStyle { get; set; } = AppEnvironment.FormatDaxLineBreakDefault; + + [JsonPropertyName("lineStyle")] + public DaxFormatterLineStyle? LineStyle { get; set; } + + [JsonPropertyName("spacingStyle")] + public DaxFormatterSpacingStyle? SpacingStyle { get; set; } + + [JsonPropertyName("listSeparator")] + public char? ListSeparator { get; set; } + + [JsonPropertyName("decimalSeparator")] + public char? DecimalSeparator { get; set; } + + [JsonPropertyName("serverName")] + public string? ServerName { get; set; } + + [JsonPropertyName("serverVersion")] + public string? ServerVersion { get; set; } + + [JsonPropertyName("serverEdition")] + public TOM.ServerEdition? ServerEdition { get; set; } + + [JsonPropertyName("serverMode")] + public TOM.ServerMode? ServerMode { get; set; } + + [JsonPropertyName("serverLocation")] + public TOM.ServerLocation? ServerLocation { get; set; } + + [JsonPropertyName("databaseName")] + public string? DatabaseName { get; set; } + + [JsonPropertyName("compatibilityLevel")] + public int? CompatibilityLevel { get; set; } +} + +public enum DaxLineBreakStyle { - using Dax.Formatter.Models; - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Models.AnalyzeModel; - using System.Collections.Generic; - using System.ComponentModel.DataAnnotations; - using System.Text.Json.Serialization; - using TOM = Microsoft.AnalysisServices; - - public class FormatDaxRequest - { - [Required] - [JsonPropertyName("options")] - public FormatDaxOptions? Options { get; set; } - - [Required] - [JsonPropertyName("measures")] - public IEnumerable? Measures { get; set; } - } - - public class FormatDaxOptions - { - /// - /// Auto-calculated based on the existing measures in the model. See - /// - [Required] - [JsonPropertyName("autoLineBreakStyle")] - public DaxLineBreakStyle? AutoLineBreakStyle { get; set; } - - /// - /// Preferred from user settings - /// - [JsonPropertyName("lineBreakStyle")] - public DaxLineBreakStyle LineBreakStyle { get; set; } = AppEnvironment.FormatDaxLineBreakDefault; - - [JsonPropertyName("lineStyle")] - public DaxFormatterLineStyle? LineStyle { get; set; } - - [JsonPropertyName("spacingStyle")] - public DaxFormatterSpacingStyle? SpacingStyle { get; set; } - - [JsonPropertyName("listSeparator")] - public char? ListSeparator { get; set; } - - [JsonPropertyName("decimalSeparator")] - public char? DecimalSeparator { get; set; } - - [JsonPropertyName("serverName")] - public string? ServerName { get; set; } - - [JsonPropertyName("serverVersion")] - public string? ServerVersion { get; set; } - - [JsonPropertyName("serverEdition")] - public TOM.ServerEdition? ServerEdition { get; set; } - - [JsonPropertyName("serverMode")] - public TOM.ServerMode? ServerMode { get; set; } - - [JsonPropertyName("serverLocation")] - public TOM.ServerLocation? ServerLocation { get; set; } - - [JsonPropertyName("databaseName")] - public string? DatabaseName { get; set; } - - [JsonPropertyName("compatibilityLevel")] - public int? CompatibilityLevel { get; set; } - } - - public enum DaxLineBreakStyle - { - /// - /// No line break character at beginning of DAX expression - /// - None = 0, - - /// - /// The first character of the DAX expression is a line break - /// - InitialLineBreak = 1, - - ///// - ///// Like only for multi-line DAX expressions - ///// - //InitiaLineBreakOnMultilineOnly = TBD, - - /// - /// Automatically pick or based on the existing measures in the model, using the prevalent technique in existing measures - /// - Auto = 2 - } + /// + /// No line break character at beginning of DAX expression + /// + None = 0, + + /// + /// The first character of the DAX expression is a line break + /// + InitialLineBreak = 1, + + ///// + ///// Like only for multi-line DAX expressions + ///// + //InitiaLineBreakOnMultilineOnly = TBD, + + /// + /// Automatically pick or based on the existing measures in the model, using the prevalent technique in existing measures + /// + Auto = 2 } diff --git a/src/Models/FormatDax/FormatDaxResponse.cs b/src/Models/FormatDax/FormatDaxResponse.cs index abf4fce8..67a106ec 100644 --- a/src/Models/FormatDax/FormatDaxResponse.cs +++ b/src/Models/FormatDax/FormatDaxResponse.cs @@ -1,57 +1,56 @@ -namespace Sqlbi.Bravo.Models.FormatDax -{ - using Dax.Formatter.Models; - using Sqlbi.Bravo.Infrastructure; - using System.Collections.Generic; - using System.Text.Json.Serialization; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using Dax.Formatter.Models; +using Sqlbi.Bravo.Infrastructure; - public class FormatDaxResponse : List - { - // TODO: do not inherit from Generic.List, add instead a 'Measures' property - //[JsonPropertyName("measures")] - //public IEnumerable? Measures { get; set; } - } +namespace Sqlbi.Bravo.Models.FormatDax; - public class FormattedMeasure - { - [JsonPropertyName("etag")] - public string? ETag { get; set; } +public class FormatDaxResponse : List +{ + // TODO: do not inherit from Generic.List, add instead a 'Measures' property + //[JsonPropertyName("measures")] + //public IEnumerable? Measures { get; set; } +} - [JsonPropertyName("name")] - public string? Name { get; set; } +public class FormattedMeasure +{ + [JsonPropertyName("etag")] + public string? ETag { get; set; } - [JsonPropertyName("tableName")] - public string? TableName { get; set; } + [JsonPropertyName("name")] + public string? Name { get; set; } - [JsonPropertyName("expression")] - public string? Expression { get; set; } + [JsonPropertyName("tableName")] + public string? TableName { get; set; } - [JsonPropertyName("lineBreakStyle")] - public DaxLineBreakStyle LineBreakStyle { get; set; } = AppEnvironment.FormatDaxLineBreakDefault; + [JsonPropertyName("expression")] + public string? Expression { get; set; } - [JsonPropertyName("errors")] - public IEnumerable? Errors { get; set; } - } + [JsonPropertyName("lineBreakStyle")] + public DaxLineBreakStyle LineBreakStyle { get; set; } = AppEnvironment.FormatDaxLineBreakDefault; - public class FormatterError - { - [JsonPropertyName("line")] - public int? Line { get; set; } + [JsonPropertyName("errors")] + public IEnumerable? Errors { get; set; } +} - [JsonPropertyName("column")] - public int? Column { get; set; } +public class FormatterError +{ + [JsonPropertyName("line")] + public int? Line { get; set; } + + [JsonPropertyName("column")] + public int? Column { get; set; } - [JsonPropertyName("message")] - public string? Message { get; set; } + [JsonPropertyName("message")] + public string? Message { get; set; } - public static FormatterError CreateFrom(DaxFormatterError error) + public static FormatterError CreateFrom(DaxFormatterError error) + { + return new FormatterError { - return new FormatterError - { - Line = error.Line, - Column = error.Column, - Message = error.Message - }; - } + Line = error.Line, + Column = error.Column, + Message = error.Message + }; } } diff --git a/src/Models/FormatDax/UpdatePBICloudDatasetRequest.cs b/src/Models/FormatDax/UpdatePBICloudDatasetRequest.cs index 01ee2b74..36ec1ddc 100644 --- a/src/Models/FormatDax/UpdatePBICloudDatasetRequest.cs +++ b/src/Models/FormatDax/UpdatePBICloudDatasetRequest.cs @@ -1,17 +1,16 @@ -namespace Sqlbi.Bravo.Models.FormatDax -{ - using System.Collections.Generic; - using System.ComponentModel.DataAnnotations; - using System.Text.Json.Serialization; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Sqlbi.Bravo.Models.FormatDax; - public class UpdatePBICloudDatasetRequest - { - [Required] - [JsonPropertyName("dataset")] - public PBICloudDataset? Dataset { get; set; } +public class UpdatePBICloudDatasetRequest +{ + [Required] + [JsonPropertyName("dataset")] + public PBICloudDataset? Dataset { get; set; } - [Required] - [JsonPropertyName("measures")] - public IEnumerable? Measures { get; set; } - } + [Required] + [JsonPropertyName("measures")] + public IEnumerable? Measures { get; set; } } diff --git a/src/Models/FormatDax/UpdatePBIDesktopReportRequest.cs b/src/Models/FormatDax/UpdatePBIDesktopReportRequest.cs index ac6d1e7a..76d12321 100644 --- a/src/Models/FormatDax/UpdatePBIDesktopReportRequest.cs +++ b/src/Models/FormatDax/UpdatePBIDesktopReportRequest.cs @@ -1,17 +1,16 @@ -namespace Sqlbi.Bravo.Models.FormatDax -{ - using System.Collections.Generic; - using System.ComponentModel.DataAnnotations; - using System.Text.Json.Serialization; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Sqlbi.Bravo.Models.FormatDax; - public class UpdatePBIDesktopReportRequest - { - [Required] - [JsonPropertyName("report")] - public PBIDesktopReport? Report { get; set; } +public class UpdatePBIDesktopReportRequest +{ + [Required] + [JsonPropertyName("report")] + public PBIDesktopReport? Report { get; set; } - [Required] - [JsonPropertyName("measures")] - public IEnumerable? Measures { get; set; } - } + [Required] + [JsonPropertyName("measures")] + public IEnumerable? Measures { get; set; } } diff --git a/src/Models/ManageDates/ApplyConfigurationRequest.cs b/src/Models/ManageDates/ApplyConfigurationRequest.cs index e1071c97..234af8a1 100644 --- a/src/Models/ManageDates/ApplyConfigurationRequest.cs +++ b/src/Models/ManageDates/ApplyConfigurationRequest.cs @@ -1,20 +1,19 @@ -namespace Sqlbi.Bravo.Models.ManageDates -{ - using System.ComponentModel.DataAnnotations; - using System.Text.Json.Serialization; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; - public class ApplyConfigurationRequest - { - [Required] - [JsonPropertyName("report")] - public PBIDesktopReport? Report { get; set; } +namespace Sqlbi.Bravo.Models.ManageDates; - [Required] - [JsonPropertyName("configuration")] - public DateConfiguration? Configuration { get; set; } - } +public class ApplyConfigurationRequest +{ + [Required] + [JsonPropertyName("report")] + public PBIDesktopReport? Report { get; set; } - public class ValidateConfigurationRequest : ApplyConfigurationRequest - { - } + [Required] + [JsonPropertyName("configuration")] + public DateConfiguration? Configuration { get; set; } +} + +public class ValidateConfigurationRequest : ApplyConfigurationRequest +{ } diff --git a/src/Models/ManageDates/CustomPackage.cs b/src/Models/ManageDates/CustomPackage.cs index f55711f3..be92731b 100644 --- a/src/Models/ManageDates/CustomPackage.cs +++ b/src/Models/ManageDates/CustomPackage.cs @@ -1,47 +1,45 @@ -namespace Sqlbi.Bravo.Models.ManageDates +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; + +namespace Sqlbi.Bravo.Models.ManageDates; + +public class CustomPackage +{ + [Required] + [JsonPropertyName("type")] + public CustomPackageType? Type { get; set; } + + [JsonPropertyName("path")] + public string? Path { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("description")] + public string? Description { get; set; } + + [JsonPropertyName("workspacePath")] + public string? WorkspacePath { get; set; } + + [JsonPropertyName("workspaceName")] + public string? WorkspaceName { get; set; } + + [JsonPropertyName("hasWorkspace")] + public bool HasWorkspace { get; set; } + + [JsonPropertyName("hasPackage")] + public bool HasPackage { get; set; } +} + +public enum CustomPackageType { - using System.ComponentModel.DataAnnotations; - using System.Diagnostics; - using System.Text.Json.Serialization; - - public class CustomPackage - { - [Required] - [JsonPropertyName("type")] - public CustomPackageType? Type { get; set; } - - [JsonPropertyName("path")] - public string? Path { get; set; } - - [JsonPropertyName("name")] - public string? Name { get; set; } - - [JsonPropertyName("description")] - public string? Description { get; set; } - - [JsonPropertyName("workspacePath")] - public string? WorkspacePath { get; set; } - - [JsonPropertyName("workspaceName")] - public string? WorkspaceName { get; set; } - - [JsonPropertyName("hasWorkspace")] - public bool HasWorkspace { get; set; } - - [JsonPropertyName("hasPackage")] - public bool HasPackage { get; set; } - } - - public enum CustomPackageType - { - /// - /// Custom template package from the user's local repository - /// - User = 0, - - /// - /// Custom template package from the organization's shared repository - /// - Organization = 1, - } + /// + /// Custom template package from the user's local repository + /// + User = 0, + + /// + /// Custom template package from the organization's shared repository + /// + Organization = 1, } diff --git a/src/Models/ManageDates/DateConfiguration.cs b/src/Models/ManageDates/DateConfiguration.cs index a8d18cb3..b4f2935f 100644 --- a/src/Models/ManageDates/DateConfiguration.cs +++ b/src/Models/ManageDates/DateConfiguration.cs @@ -1,577 +1,576 @@ -namespace Sqlbi.Bravo.Models.ManageDates +using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Text.Json.Serialization; +using Dax.Template.Enums; +using Dax.Template.Exceptions; +using Dax.Template.Interfaces; +using Dax.Template.Tables; +using Sqlbi.Bravo.Infrastructure; +using Sqlbi.Bravo.Infrastructure.Configuration; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Services.DaxTemplate; +using TOM = Microsoft.AnalysisServices.Tabular; + +namespace Sqlbi.Bravo.Models.ManageDates; + +[DebuggerDisplay("{Name} {IsCurrent}")] +public class DateConfiguration { - using Dax.Template.Enums; - using Dax.Template.Exceptions; - using Dax.Template.Interfaces; - using Dax.Template.Tables; - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.Configuration; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Services.DaxTemplate; - using System; - using System.ComponentModel.DataAnnotations; - using System.Diagnostics; - using System.IO; - using System.Linq; - using System.Text.Json; - using System.Text.Json.Serialization; - using TOM = Microsoft.AnalysisServices.Tabular; - - [DebuggerDisplay("{Name} {IsCurrent}")] - public class DateConfiguration + internal const string ExtendedPropertyName = "SQLBI_BRAVO_ManageDatesConfiguration"; + + internal static readonly JsonSerializerOptions ExtendedPropertyJsonOptions = new() { - internal const string ExtendedPropertyName = "SQLBI_BRAVO_ManageDatesConfiguration"; + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; - internal static readonly JsonSerializerOptions ExtendedPropertyJsonOptions = new() - { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - - public DateConfiguration() - { - IsCurrent = false; - IsCustom = false; - } + public DateConfiguration() + { + IsCurrent = false; + IsCustom = false; + } - /// - /// For internal use only, not to be shown in Bravo UI - /// - [Required] - [JsonPropertyName("templateUri")] - public string? TemplateUri { get; set; } + /// + /// For internal use only, not to be shown in Bravo UI + /// + [Required] + [JsonPropertyName("templateUri")] + public string? TemplateUri { get; set; } - /// - /// Returns true if this is the one currently applied to the model - /// - [JsonPropertyName("isCurrent")] - public bool IsCurrent { get; private set; } = false; + /// + /// Returns true if this is the one currently applied to the model + /// + [JsonPropertyName("isCurrent")] + public bool IsCurrent { get; private set; } = false; - /// - /// Returns true if this belongs to a custom developed template and not a predefined Bravo template - /// - [JsonPropertyName("isCustom")] - public bool IsCustom { get; set; } = false; + /// + /// Returns true if this belongs to a custom developed template and not a predefined Bravo template + /// + [JsonPropertyName("isCustom")] + public bool IsCustom { get; set; } = false; - [JsonPropertyName("name")] - public string? Name { get; set; } + [JsonPropertyName("name")] + public string? Name { get; set; } - [JsonPropertyName("description")] - public string? Description { get; set; } + [JsonPropertyName("description")] + public string? Description { get; set; } - #region Dax.Template.ILocalization + #region Dax.Template.ILocalization - [JsonPropertyName("isoFormat")] - public string? IsoFormat { get; set; } + [JsonPropertyName("isoFormat")] + public string? IsoFormat { get; set; } - [JsonPropertyName("isoTranslation")] - public string? IsoTranslation { get; set; } + [JsonPropertyName("isoTranslation")] + public string? IsoTranslation { get; set; } - #endregion + #endregion - #region Dax.Template.IScanConfig + #region Dax.Template.IScanConfig - [JsonPropertyName("autoScan")] - public AutoScanEnum? AutoScan { get; set; } + [JsonPropertyName("autoScan")] + public AutoScanEnum? AutoScan { get; set; } - [JsonPropertyName("onlyTablesColumns")] - public string[]? OnlyTablesColumns { get; set; } + [JsonPropertyName("onlyTablesColumns")] + public string[]? OnlyTablesColumns { get; set; } - [JsonPropertyName("exceptTablesColumns")] - public string[]? ExceptTablesColumns { get; set; } + [JsonPropertyName("exceptTablesColumns")] + public string[]? ExceptTablesColumns { get; set; } - #endregion + #endregion - #region Dax.Template.IHolidaysConfig + #region Dax.Template.IHolidaysConfig - [JsonPropertyName("isoCountry")] - public string? IsoCountry { get; set; } + [JsonPropertyName("isoCountry")] + public string? IsoCountry { get; set; } - #endregion + #endregion - #region Dax.Template.IDateTemplateConfig + #region Dax.Template.IDateTemplateConfig - [JsonPropertyName("firstYear")] - public int? FirstYear { get; set; } + [JsonPropertyName("firstYear")] + public int? FirstYear { get; set; } - [JsonPropertyName("lastYear")] - public int? LastYear { get; set; } + [JsonPropertyName("lastYear")] + public int? LastYear { get; set; } - #endregion + #endregion - #region Dax.Template.IMeasureTemplateConfig + #region Dax.Template.IMeasureTemplateConfig - [JsonPropertyName("autoNaming")] - public AutoNamingEnum? AutoNaming { get; set; } + [JsonPropertyName("autoNaming")] + public AutoNamingEnum? AutoNaming { get; set; } - [JsonPropertyName("targetMeasures")] - public string[]? TargetMeasures { get; set; } + [JsonPropertyName("targetMeasures")] + public string[]? TargetMeasures { get; set; } - [JsonPropertyName("tableSingleInstanceMeasures")] - public string? TableSingleInstanceMeasures { get; set; } + [JsonPropertyName("tableSingleInstanceMeasures")] + public string? TableSingleInstanceMeasures { get; set; } - #endregion + #endregion - #region Dax.Template.ICustomTableConfig + #region Dax.Template.ICustomTableConfig - [JsonPropertyName("defaults")] - public DateDefaults? Defaults { get; set; } + [JsonPropertyName("defaults")] + public DateDefaults? Defaults { get; set; } - #endregion + #endregion - #region Date (Dax.Template.Tables.Dates.CustomDateTable) + #region Date (Dax.Template.Tables.Dates.CustomDateTable) - /// - /// Indicates whether the exists in the config.template.json - /// - [Required] - [JsonPropertyName("dateAvailable")] - public bool DateAvailable { get; set; } = false; + /// + /// Indicates whether the exists in the config.template.json + /// + [Required] + [JsonPropertyName("dateAvailable")] + public bool DateAvailable { get; set; } = false; - /// - /// Indicates whether the user has enabled this template for deploy - /// - [Required] - [JsonPropertyName("dateEnabled")] - public bool DateEnabled { get; set; } = false; + /// + /// Indicates whether the user has enabled this template for deploy + /// + [Required] + [JsonPropertyName("dateEnabled")] + public bool DateEnabled { get; set; } = false; - [Required] - [JsonPropertyName("dateTableName")] - public string? DateTableName { get; set; } + [Required] + [JsonPropertyName("dateTableName")] + public string? DateTableName { get; set; } - [Required] - [JsonPropertyName("dateTableValidation")] - public TableValidation DateTableValidation { get; set; } = TableValidation.Unknown; + [Required] + [JsonPropertyName("dateTableValidation")] + public TableValidation DateTableValidation { get; set; } = TableValidation.Unknown; - [Required] - [JsonPropertyName("dateReferenceTableName")] - public string? DateReferenceTableName { get; set; } + [Required] + [JsonPropertyName("dateReferenceTableName")] + public string? DateReferenceTableName { get; set; } - [Required] - [JsonPropertyName("dateReferenceTableValidation")] - public TableValidation DateReferenceTableValidation { get; set; } = TableValidation.Unknown; + [Required] + [JsonPropertyName("dateReferenceTableValidation")] + public TableValidation DateReferenceTableValidation { get; set; } = TableValidation.Unknown; - #endregion + #endregion - #region Holidays (Dax.Template.Tables.Dates.HolidaysTable + Dax.Template.Tables.Dates.HolidaysDefinitionTable) + #region Holidays (Dax.Template.Tables.Dates.HolidaysTable + Dax.Template.Tables.Dates.HolidaysDefinitionTable) - /// - /// Indicates whether the exists in the config.template.json - /// - [Required] - [JsonPropertyName("holidaysAvailable")] - public bool HolidaysAvailable { get; set; } = false; + /// + /// Indicates whether the exists in the config.template.json + /// + [Required] + [JsonPropertyName("holidaysAvailable")] + public bool HolidaysAvailable { get; set; } = false; - /// - /// Indicates whether the user has enabled the creation of the holidays table - /// - [Required] - [JsonPropertyName("holidaysEnabled")] - public bool HolidaysEnabled { get; set; } = false; + /// + /// Indicates whether the user has enabled the creation of the holidays table + /// + [Required] + [JsonPropertyName("holidaysEnabled")] + public bool HolidaysEnabled { get; set; } = false; - [JsonPropertyName("holidaysTableName")] - public string? HolidaysTableName { get; set; } + [JsonPropertyName("holidaysTableName")] + public string? HolidaysTableName { get; set; } - [Required] - [JsonPropertyName("holidaysTableValidation")] - public TableValidation HolidaysTableValidation { get; set; } = TableValidation.Unknown; + [Required] + [JsonPropertyName("holidaysTableValidation")] + public TableValidation HolidaysTableValidation { get; set; } = TableValidation.Unknown; - [JsonPropertyName("holidaysDefinitionTableName")] - public string? HolidaysDefinitionTableName { get; set; } + [JsonPropertyName("holidaysDefinitionTableName")] + public string? HolidaysDefinitionTableName { get; set; } - [Required] - [JsonPropertyName("holidaysDefinitionTableValidation")] - public TableValidation HolidaysDefinitionTableValidation { get; set; } = TableValidation.Unknown; + [Required] + [JsonPropertyName("holidaysDefinitionTableValidation")] + public TableValidation HolidaysDefinitionTableValidation { get; set; } = TableValidation.Unknown; - #endregion + #endregion - #region TimeIntelligence (Dax.Template.Measures.MeasuresTemplateDefinition.MeasureTemplate) + #region TimeIntelligence (Dax.Template.Measures.MeasuresTemplateDefinition.MeasureTemplate) - /// - /// Indicates whether the exists in the config.template.json - /// - [Required] - [JsonPropertyName("timeIntelligenceAvailable")] - public bool TimeIntelligenceAvailable { get; set; } = false; + /// + /// Indicates whether the exists in the config.template.json + /// + [Required] + [JsonPropertyName("timeIntelligenceAvailable")] + public bool TimeIntelligenceAvailable { get; set; } = false; - /// - /// Indicates whether the user has enabled this template for deploy - /// - [Required] - [JsonPropertyName("timeIntelligenceEnabled")] - public bool TimeIntelligenceEnabled { get; set; } = true; + /// + /// Indicates whether the user has enabled this template for deploy + /// + [Required] + [JsonPropertyName("timeIntelligenceEnabled")] + public bool TimeIntelligenceEnabled { get; set; } = true; - #endregion + #endregion - public void CopyTo(TemplateConfiguration templateConfiguration) + public void CopyTo(TemplateConfiguration templateConfiguration) + { + templateConfiguration.Name = Name ?? templateConfiguration.Name; + templateConfiguration.Description = Description ?? templateConfiguration.Description; + // + // Dax.Template.ILocalization + // + templateConfiguration.IsoFormat = IsoFormat ?? templateConfiguration.IsoFormat; + templateConfiguration.IsoTranslation = IsoTranslation ?? templateConfiguration.IsoTranslation; + // + // Dax.Template.IScanConfig + // + templateConfiguration.AutoScan = AutoScan ?? templateConfiguration.AutoScan; + if (OnlyTablesColumns?.Length > 0) templateConfiguration.OnlyTablesColumns = OnlyTablesColumns; + if (ExceptTablesColumns?.Length > 0) templateConfiguration.ExceptTablesColumns = ExceptTablesColumns; + // + // Dax.Template.IHolidaysConfig + // + templateConfiguration.IsoCountry = IsoCountry ?? templateConfiguration.IsoCountry; + // + // Dax.Template.IDateTemplateConfig + // + if (FirstYear.HasValue) { - templateConfiguration.Name = Name ?? templateConfiguration.Name; - templateConfiguration.Description = Description ?? templateConfiguration.Description; - // - // Dax.Template.ILocalization - // - templateConfiguration.IsoFormat = IsoFormat ?? templateConfiguration.IsoFormat; - templateConfiguration.IsoTranslation = IsoTranslation ?? templateConfiguration.IsoTranslation; - // - // Dax.Template.IScanConfig - // - templateConfiguration.AutoScan = AutoScan ?? templateConfiguration.AutoScan; - if (OnlyTablesColumns?.Length > 0) templateConfiguration.OnlyTablesColumns = OnlyTablesColumns; - if (ExceptTablesColumns?.Length > 0) templateConfiguration.ExceptTablesColumns = ExceptTablesColumns; - // - // Dax.Template.IHolidaysConfig - // - templateConfiguration.IsoCountry = IsoCountry ?? templateConfiguration.IsoCountry; - // - // Dax.Template.IDateTemplateConfig - // - if (FirstYear.HasValue) + templateConfiguration.FirstYear = FirstYear.Value; + templateConfiguration.FirstYearMin = FirstYear.Value; + templateConfiguration.FirstYearMax = FirstYear.Value; + } + if (LastYear.HasValue) + { + templateConfiguration.LastYear = LastYear.Value; + templateConfiguration.LastYearMin = LastYear.Value; + templateConfiguration.LastYearMax = LastYear.Value; + } + // + // Dax.Template.Dax.Template.IMeasureTemplateConfig + // + templateConfiguration.AutoNaming = AutoNaming ?? templateConfiguration.AutoNaming; + if (TargetMeasures?.Length > 0) + { + templateConfiguration.TargetMeasures = TargetMeasures + .Select((name) => new IMeasureTemplateConfig.TargetMeasure { Name = name }) + .ToArray(); + } + templateConfiguration.TableSingleInstanceMeasures = TableSingleInstanceMeasures ?? templateConfiguration.TableSingleInstanceMeasures; + // + // Dax.Template.ICustomTableConfig + // + Defaults?.CopyTo(templateConfiguration); + // + // ITemplates.TemplateEntry + // + var templateEntries = templateConfiguration.GetTemplateEntries(); + // + // ITemplates.TemplateEntry - Date (Dax.Template.Tables.Dates.CustomDateTable) + // + if (templateEntries.Date is not null) + { + if (templateEntries.Date.IsEnabled = DateEnabled) { - templateConfiguration.FirstYear = FirstYear.Value; - templateConfiguration.FirstYearMin = FirstYear.Value; - templateConfiguration.FirstYearMax = FirstYear.Value; + DateTableValidation.Assert(); + DateReferenceTableValidation.Assert(); + + templateEntries.Date.Table = DateTableName; + templateEntries.Date.ReferenceTable = DateReferenceTableName; } - if (LastYear.HasValue) + } + // + // ITemplates.TemplateEntry - Holidays (Dax.Template.Tables.Dates.HolidaysTable + Dax.Template.Tables.Dates.HolidaysDefinitionTable) + // + if (templateEntries.Holidays is not null) + { + BravoUnexpectedException.ThrowIfNull(templateEntries.HolidaysDefinition); + + if (templateEntries.Holidays.IsEnabled = templateEntries.HolidaysDefinition.IsEnabled = HolidaysEnabled) { - templateConfiguration.LastYear = LastYear.Value; - templateConfiguration.LastYearMin = LastYear.Value; - templateConfiguration.LastYearMax = LastYear.Value; + HolidaysTableValidation.Assert(); + HolidaysDefinitionTableValidation.Assert(); + BravoUnexpectedException.ThrowIfNull(templateConfiguration.HolidaysReference); + + templateEntries.Holidays.Table = templateConfiguration.HolidaysReference.TableName = HolidaysTableName; + templateEntries.HolidaysDefinition.Table = templateConfiguration.HolidaysDefinitionTable = HolidaysDefinitionTableName; } - // - // Dax.Template.Dax.Template.IMeasureTemplateConfig - // - templateConfiguration.AutoNaming = AutoNaming ?? templateConfiguration.AutoNaming; - if (TargetMeasures?.Length > 0) + } + // + // ITemplates.TemplateEntry - TimeIntelligence (Dax.Template.Measures.MeasuresTemplateDefinition.MeasureTemplate) + // + if (templateEntries.TimeIntelligence is not null) + { + if (templateEntries.TimeIntelligence.IsEnabled = TimeIntelligenceEnabled) { - templateConfiguration.TargetMeasures = TargetMeasures - .Select((name) => new IMeasureTemplateConfig.TargetMeasure { Name = name }) - .ToArray(); + // nothing to do } - templateConfiguration.TableSingleInstanceMeasures = TableSingleInstanceMeasures ?? templateConfiguration.TableSingleInstanceMeasures; + } + } + + public static DateConfiguration CreateFrom(Dax.Template.Package package) + { + var templateEntries = package.Configuration.GetTemplateEntries(); + + var configuration = new DateConfiguration + { + TemplateUri = package.Configuration.TemplateUri, + Name = package.Configuration.Name, + Description = package.Configuration.Description, // - // Dax.Template.ICustomTableConfig + // ILocalization // - Defaults?.CopyTo(templateConfiguration); + IsoFormat = package.Configuration.IsoFormat, + IsoTranslation = package.Configuration.IsoTranslation, // - // ITemplates.TemplateEntry + // IScanConfig // - var templateEntries = templateConfiguration.GetTemplateEntries(); + AutoScan = package.Configuration.AutoScan, + OnlyTablesColumns = package.Configuration.OnlyTablesColumns, + ExceptTablesColumns = package.Configuration.ExceptTablesColumns, // - // ITemplates.TemplateEntry - Date (Dax.Template.Tables.Dates.CustomDateTable) + // IHolidaysConfig // - if (templateEntries.Date is not null) - { - if (templateEntries.Date.IsEnabled = DateEnabled) - { - DateTableValidation.Assert(); - DateReferenceTableValidation.Assert(); - - templateEntries.Date.Table = DateTableName; - templateEntries.Date.ReferenceTable = DateReferenceTableName; - } - } + IsoCountry = package.Configuration.IsoCountry, // - // ITemplates.TemplateEntry - Holidays (Dax.Template.Tables.Dates.HolidaysTable + Dax.Template.Tables.Dates.HolidaysDefinitionTable) - // - if (templateEntries.Holidays is not null) - { - BravoUnexpectedException.ThrowIfNull(templateEntries.HolidaysDefinition); - - if (templateEntries.Holidays.IsEnabled = templateEntries.HolidaysDefinition.IsEnabled = HolidaysEnabled) - { - HolidaysTableValidation.Assert(); - HolidaysDefinitionTableValidation.Assert(); - BravoUnexpectedException.ThrowIfNull(templateConfiguration.HolidaysReference); - - templateEntries.Holidays.Table = templateConfiguration.HolidaysReference.TableName = HolidaysTableName; - templateEntries.HolidaysDefinition.Table = templateConfiguration.HolidaysDefinitionTable = HolidaysDefinitionTableName; - } - } + // IDateTemplateConfig // - // ITemplates.TemplateEntry - TimeIntelligence (Dax.Template.Measures.MeasuresTemplateDefinition.MeasureTemplate) + FirstYear = package.Configuration.FirstYear, + LastYear = package.Configuration.LastYear, // - if (templateEntries.TimeIntelligence is not null) - { - if (templateEntries.TimeIntelligence.IsEnabled = TimeIntelligenceEnabled) - { - // nothing to do - } - } - } - - public static DateConfiguration CreateFrom(Dax.Template.Package package) - { - var templateEntries = package.Configuration.GetTemplateEntries(); + // IMeasureTemplateConfig + // + AutoNaming = package.Configuration.AutoNaming, + TargetMeasures = package.Configuration.TargetMeasures?.Where((measure) => measure.Name is not null).Select((measure) => measure.Name!).ToArray(), + TableSingleInstanceMeasures = package.Configuration.TableSingleInstanceMeasures, + // + // ICustomTableConfig + // + Defaults = DateDefaults.CreateFrom(package.Configuration), + // + // ITemplates.TemplateEntry - enable/disable + // + DateAvailable = templateEntries.Date is not null, + DateTableName = templateEntries.Date?.Table, + DateReferenceTableName = templateEntries.Date?.ReferenceTable, + //-- + HolidaysAvailable = templateEntries.Holidays is not null && templateEntries.HolidaysDefinition is not null, + HolidaysTableName = templateEntries.Holidays?.Table, + HolidaysDefinitionTableName = templateEntries.HolidaysDefinition?.Table, + //-- + TimeIntelligenceAvailable = templateEntries.TimeIntelligence is not null, + }; - var configuration = new DateConfiguration - { - TemplateUri = package.Configuration.TemplateUri, - Name = package.Configuration.Name, - Description = package.Configuration.Description, - // - // ILocalization - // - IsoFormat = package.Configuration.IsoFormat, - IsoTranslation = package.Configuration.IsoTranslation, - // - // IScanConfig - // - AutoScan = package.Configuration.AutoScan, - OnlyTablesColumns = package.Configuration.OnlyTablesColumns, - ExceptTablesColumns = package.Configuration.ExceptTablesColumns, - // - // IHolidaysConfig - // - IsoCountry = package.Configuration.IsoCountry, - // - // IDateTemplateConfig - // - FirstYear = package.Configuration.FirstYear, - LastYear = package.Configuration.LastYear, - // - // IMeasureTemplateConfig - // - AutoNaming = package.Configuration.AutoNaming, - TargetMeasures = package.Configuration.TargetMeasures?.Where((measure) => measure.Name is not null).Select((measure) => measure.Name!).ToArray(), - TableSingleInstanceMeasures = package.Configuration.TableSingleInstanceMeasures, - // - // ICustomTableConfig - // - Defaults = DateDefaults.CreateFrom(package.Configuration), - // - // ITemplates.TemplateEntry - enable/disable - // - DateAvailable = templateEntries.Date is not null, - DateTableName = templateEntries.Date?.Table, - DateReferenceTableName = templateEntries.Date?.ReferenceTable, - //-- - HolidaysAvailable = templateEntries.Holidays is not null && templateEntries.HolidaysDefinition is not null, - HolidaysTableName = templateEntries.Holidays?.Table, - HolidaysDefinitionTableName = templateEntries.HolidaysDefinition?.Table, - //-- - TimeIntelligenceAvailable = templateEntries.TimeIntelligence is not null, - }; + return configuration; + } - return configuration; - } + public static DateConfiguration? GetCurrentFrom(TOM.Model model) + { + var property = model.ExtendedProperties.Find(ExtendedPropertyName); - public static DateConfiguration? GetCurrentFrom(TOM.Model model) + if (property is not null && property is TOM.JsonExtendedProperty jsonProperty) { - var property = model.ExtendedProperties.Find(ExtendedPropertyName); - - if (property is not null && property is TOM.JsonExtendedProperty jsonProperty) + var configuration = JsonSerializer.Deserialize(jsonProperty.Value, ExtendedPropertyJsonOptions); + if (configuration is not null) { - var configuration = JsonSerializer.Deserialize(jsonProperty.Value, ExtendedPropertyJsonOptions); - if (configuration is not null) - { - var datesTemplateTableCount = 0; - var holidaysTemplateTableCount = 0; + var datesTemplateTableCount = 0; + var holidaysTemplateTableCount = 0; - #region Update TableName properties from TOM Model + #region Update TableName properties from TOM Model - // Update the configuration.[Date/Holidays]TableName properties from the connected TOM Model by searching based on annotations - // This ensures that the names in the configuration are correct even if the table has been renamed manually or with another tool - // We do not update the name property in case more than one table of each type is detected because the Dax.Template library will raise an exception + // Update the configuration.[Date/Holidays]TableName properties from the connected TOM Model by searching based on annotations + // This ensures that the names in the configuration are correct even if the table has been renamed manually or with another tool + // We do not update the name property in case more than one table of each type is detected because the Dax.Template library will raise an exception - if (configuration.DateAvailable && configuration.DateEnabled) + if (configuration.DateAvailable && configuration.DateEnabled) + { + // search all tables where annotations 'SQLBI_Template = Dates' + var datesTemplateTables = model.Tables.FindByAnnotation(DaxTemplateManager.SqlbiTemplateAnnotation, DaxTemplateManager.SqlbiTemplateAnnotationDatesValue).ToArray(); + if (datesTemplateTables.Length > 0) { - // search all tables where annotations 'SQLBI_Template = Dates' - var datesTemplateTables = model.Tables.FindByAnnotation(DaxTemplateManager.SqlbiTemplateAnnotation, DaxTemplateManager.SqlbiTemplateAnnotationDatesValue).ToArray(); - if (datesTemplateTables.Length > 0) - { - // filter where annotation 'SQLBI_TemplateTable = Date' - var datesTemplateDateTables = datesTemplateTables.FindByAnnotation(DaxTemplateManager.SqlbiTemplateTableAnnotation, DaxTemplateManager.SqlbiTemplateTableAnnotationDateValue).Take(2).ToArray(); - if (datesTemplateDateTables.Length == 1) - configuration.DateTableName = datesTemplateDateTables[0].Name; - - // filter where annotation 'SQLBI_TemplateTable = DateAutoTemplate' - var datesTemplateDateReferenceTables = datesTemplateTables.FindByAnnotation(DaxTemplateManager.SqlbiTemplateTableAnnotation, DaxTemplateManager.SqlbiTemplateTableAnnotationDateAutoTemplateValue).Take(2).ToArray(); - if (datesTemplateDateReferenceTables.Length == 1) - configuration.DateReferenceTableName = datesTemplateDateReferenceTables[0].Name; - } - datesTemplateTableCount = datesTemplateTables.Length; + // filter where annotation 'SQLBI_TemplateTable = Date' + var datesTemplateDateTables = datesTemplateTables.FindByAnnotation(DaxTemplateManager.SqlbiTemplateTableAnnotation, DaxTemplateManager.SqlbiTemplateTableAnnotationDateValue).Take(2).ToArray(); + if (datesTemplateDateTables.Length == 1) + configuration.DateTableName = datesTemplateDateTables[0].Name; + + // filter where annotation 'SQLBI_TemplateTable = DateAutoTemplate' + var datesTemplateDateReferenceTables = datesTemplateTables.FindByAnnotation(DaxTemplateManager.SqlbiTemplateTableAnnotation, DaxTemplateManager.SqlbiTemplateTableAnnotationDateAutoTemplateValue).Take(2).ToArray(); + if (datesTemplateDateReferenceTables.Length == 1) + configuration.DateReferenceTableName = datesTemplateDateReferenceTables[0].Name; } + datesTemplateTableCount = datesTemplateTables.Length; + } - if (configuration.HolidaysAvailable && configuration.HolidaysEnabled) + if (configuration.HolidaysAvailable && configuration.HolidaysEnabled) + { + // search all tables where annotation 'SQLBI_Template = Holidays' + var holidaysTemplateTables = model.Tables.FindByAnnotation(DaxTemplateManager.SqlbiTemplateAnnotation, DaxTemplateManager.SqlbiTemplateAnnotationHolidaysValue).ToArray(); + if (holidaysTemplateTables.Length > 0) { - // search all tables where annotation 'SQLBI_Template = Holidays' - var holidaysTemplateTables = model.Tables.FindByAnnotation(DaxTemplateManager.SqlbiTemplateAnnotation, DaxTemplateManager.SqlbiTemplateAnnotationHolidaysValue).ToArray(); - if (holidaysTemplateTables.Length > 0) - { - // filter where annotation 'SQLBI_TemplateTable = Holidays' - var holidaysTemplateHolidaysTables = holidaysTemplateTables.FindByAnnotation(DaxTemplateManager.SqlbiTemplateTableAnnotation, DaxTemplateManager.SqlbiTemplateTableAnnotationHolidaysValue).Take(2).ToArray(); - if (holidaysTemplateHolidaysTables.Length == 1) - configuration.HolidaysTableName = holidaysTemplateHolidaysTables[0].Name; - - // filter where annotation 'SQLBI_TemplateTable = HolidaysDefinition' - var holidaysTemplateHolidaysDefinitionTables = holidaysTemplateTables.FindByAnnotation(DaxTemplateManager.SqlbiTemplateTableAnnotation, DaxTemplateManager.SqlbiTemplateTableAnnotationHolidaysDefinitionValue).Take(2).ToArray(); - if (holidaysTemplateHolidaysDefinitionTables.Length == 1) - configuration.HolidaysDefinitionTableName = holidaysTemplateHolidaysDefinitionTables[0].Name; - } - holidaysTemplateTableCount = holidaysTemplateTables.Length; + // filter where annotation 'SQLBI_TemplateTable = Holidays' + var holidaysTemplateHolidaysTables = holidaysTemplateTables.FindByAnnotation(DaxTemplateManager.SqlbiTemplateTableAnnotation, DaxTemplateManager.SqlbiTemplateTableAnnotationHolidaysValue).Take(2).ToArray(); + if (holidaysTemplateHolidaysTables.Length == 1) + configuration.HolidaysTableName = holidaysTemplateHolidaysTables[0].Name; + + // filter where annotation 'SQLBI_TemplateTable = HolidaysDefinition' + var holidaysTemplateHolidaysDefinitionTables = holidaysTemplateTables.FindByAnnotation(DaxTemplateManager.SqlbiTemplateTableAnnotation, DaxTemplateManager.SqlbiTemplateTableAnnotationHolidaysDefinitionValue).Take(2).ToArray(); + if (holidaysTemplateHolidaysDefinitionTables.Length == 1) + configuration.HolidaysDefinitionTableName = holidaysTemplateHolidaysDefinitionTables[0].Name; } + holidaysTemplateTableCount = holidaysTemplateTables.Length; + } - #endregion - - if (datesTemplateTableCount == 0 && holidaysTemplateTableCount == 0) - { - // Ignore the current configuration if the user deleted all tables created via Dax.Template - return null; - } + #endregion - configuration.IsCurrent = true; - return configuration; + if (datesTemplateTableCount == 0 && holidaysTemplateTableCount == 0) + { + // Ignore the current configuration if the user deleted all tables created via Dax.Template + return null; } - } - return null; + configuration.IsCurrent = true; + return configuration; + } } + + return null; } +} - internal static class DateConfigurationExtensions +internal static class DateConfigurationExtensions +{ + public static Dax.Template.Package LoadPackage(this DateConfiguration configuration, bool configure = true) { - public static Dax.Template.Package LoadPackage(this DateConfiguration configuration, bool configure = true) + BravoUnexpectedException.ThrowIfNull(configuration.TemplateUri); + { - BravoUnexpectedException.ThrowIfNull(configuration.TemplateUri); + // >> HACK + // versions 0.9.0 to 0.9.3 - TemplateUri format is %LOCALAPPDATA%\[name].template.json + // versions 0.9.4 to 0.9.5 - TemplateUri format is [name].template.json + if (Uri.TryCreate(configuration.TemplateUri, UriKind.Absolute, out _) == false) { - // >> HACK - // versions 0.9.0 to 0.9.3 - TemplateUri format is %LOCALAPPDATA%\[name].template.json - // versions 0.9.4 to 0.9.5 - TemplateUri format is [name].template.json - - if (Uri.TryCreate(configuration.TemplateUri, UriKind.Absolute, out _) == false) - { - // If TemplateUri does not contain an absolute URI, we forcibly create one using a known valid local path - configuration.TemplateUri = Path.Combine(DaxTemplateManager.CachePath, configuration.TemplateUri); - } - // << HACK + // If TemplateUri does not contain an absolute URI, we forcibly create one using a known valid local path + configuration.TemplateUri = Path.Combine(DaxTemplateManager.CachePath, configuration.TemplateUri); } + // << HACK + } - var templateUri = new Uri(configuration.TemplateUri, UriKind.Absolute); - var templatePath = templateUri.LocalPath; + var templateUri = new Uri(configuration.TemplateUri, UriKind.Absolute); + var templatePath = templateUri.LocalPath; - if (configuration.IsCustom) + if (configuration.IsCustom) + { + if (!File.Exists(templatePath) && UserPreferences.Current.CustomOptions is not null) { - if (!File.Exists(templatePath) && UserPreferences.Current.CustomOptions is not null) - { - var templatesOptionFound = UserPreferences.Current.CustomOptions.Value.TryGetProperty("templates", out var templatesOption); + var templatesOptionFound = UserPreferences.Current.CustomOptions.Value.TryGetProperty("templates", out var templatesOption); - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(DateConfigurationExtensions)}.{nameof(LoadPackage)}.CustomOptions.Templates[{templatesOption.ValueKind}]", content: templatesOption.ToString()); + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Json, name: $"{nameof(DateConfigurationExtensions)}.{nameof(LoadPackage)}.CustomOptions.Templates[{templatesOption.ValueKind}]", content: templatesOption.ToString()); - if (templatesOptionFound) + if (templatesOptionFound) + { + var customPackages = templatesOption.Deserialize() ?? Array.Empty(); + var userPackages = customPackages.Where((p) => p.HasPackage && p.Type == CustomPackageType.User && p.Name == configuration.Name).ToList(); + if (userPackages.Count == 1) { - var customPackages = templatesOption.Deserialize() ?? Array.Empty(); - var userPackages = customPackages.Where((p) => p.HasPackage && p.Type == CustomPackageType.User && p.Name == configuration.Name).ToList(); - if (userPackages.Count == 1) - { - var path = userPackages[0].Path; - if (path is not null) - templatePath = path; - } + var path = userPackages[0].Path; + if (path is not null) + templatePath = path; } } } - else - { - // Ignore the TemplateUri path and force the current user's local cache path - templatePath = Path.Combine(DaxTemplateManager.CachePath, Path.GetFileName(templatePath)); - } - - Dax.Template.Package package; - try - { - package = Dax.Template.Package.LoadFromFile(templatePath); - } - catch (Exception ex) when (ex is FileNotFoundException || ex is DirectoryNotFoundException) - { - throw new TemplateException($"The '{configuration.Name}' template file could not be found.", ex); - } - - if (configure) - { - configuration.CopyTo(package.Configuration); - } - - return package; } - - public static void SerializeTo(this DateConfiguration configuration, TOM.Model model) + else { - var configurationString = JsonSerializer.Serialize(configuration, DateConfiguration.ExtendedPropertyJsonOptions); - var configurationProperty = model.ExtendedProperties.Find(DateConfiguration.ExtendedPropertyName); - - if (configurationProperty is null) - { - var jsonProperty = new TOM.JsonExtendedProperty - { - Name = DateConfiguration.ExtendedPropertyName, - Value = configurationString, - }; - - model.ExtendedProperties.Add(jsonProperty); - } - else - { - BravoUnexpectedException.Assert(configurationProperty is TOM.JsonExtendedProperty); + // Ignore the TemplateUri path and force the current user's local cache path + templatePath = Path.Combine(DaxTemplateManager.CachePath, Path.GetFileName(templatePath)); + } - var jsonProperty = (TOM.JsonExtendedProperty)configurationProperty; + Dax.Template.Package package; + try + { + package = Dax.Template.Package.LoadFromFile(templatePath); + } + catch (Exception ex) when (ex is FileNotFoundException || ex is DirectoryNotFoundException) + { + throw new TemplateException($"The '{configuration.Name}' template file could not be found.", ex); + } - if (jsonProperty.Value != configurationString) - jsonProperty.Value = configurationString; - } + if (configure) + { + configuration.CopyTo(package.Configuration); } + + return package; } - internal static class TemplateConfigurationExtensions + public static void SerializeTo(this DateConfiguration configuration, TOM.Model model) { - private const string DateTemplateClassName = "CustomDateTable"; - private const string HolidaysTemplateClassName = "HolidaysTable"; - private const string HolidaysDefinitionTemplateClassName = "HolidaysDefinitionTable"; - private const string TimeIntelligenceTemplateClassName = "MeasuresTemplate"; + var configurationString = JsonSerializer.Serialize(configuration, DateConfiguration.ExtendedPropertyJsonOptions); + var configurationProperty = model.ExtendedProperties.Find(DateConfiguration.ExtendedPropertyName); - public static (ITemplates.TemplateEntry? Date, ITemplates.TemplateEntry? Holidays, ITemplates.TemplateEntry? HolidaysDefinition, ITemplates.TemplateEntry? TimeIntelligence) GetTemplateEntries(this TemplateConfiguration configuration) + if (configurationProperty is null) { - var date = configuration.Templates?.SingleOrDefault((entry) => entry.Class?.Equals(DateTemplateClassName) ?? false); - var holidays = configuration.Templates?.SingleOrDefault((entry) => entry.Class?.Equals(HolidaysTemplateClassName) ?? false); - var holidaysDefinition = configuration.Templates?.SingleOrDefault((entry) => entry.Class?.Equals(HolidaysDefinitionTemplateClassName) ?? false); - var timeIntelligence = configuration.Templates?.SingleOrDefault((entry) => entry.Class?.Equals(TimeIntelligenceTemplateClassName) ?? false); + var jsonProperty = new TOM.JsonExtendedProperty + { + Name = DateConfiguration.ExtendedPropertyName, + Value = configurationString, + }; + + model.ExtendedProperties.Add(jsonProperty); + } + else + { + BravoUnexpectedException.Assert(configurationProperty is TOM.JsonExtendedProperty); - return (date, holidays, holidaysDefinition, timeIntelligence); + var jsonProperty = (TOM.JsonExtendedProperty)configurationProperty; + + if (jsonProperty.Value != configurationString) + jsonProperty.Value = configurationString; } } +} + +internal static class TemplateConfigurationExtensions +{ + private const string DateTemplateClassName = "CustomDateTable"; + private const string HolidaysTemplateClassName = "HolidaysTable"; + private const string HolidaysDefinitionTemplateClassName = "HolidaysDefinitionTable"; + private const string TimeIntelligenceTemplateClassName = "MeasuresTemplate"; - public enum TableValidation + public static (ITemplates.TemplateEntry? Date, ITemplates.TemplateEntry? Holidays, ITemplates.TemplateEntry? HolidaysDefinition, ITemplates.TemplateEntry? TimeIntelligence) GetTemplateEntries(this TemplateConfiguration configuration) { - Unknown = 0, - - /// - /// A table with the same name does not exist and will be created - /// - ValidNotExists = 1, - - /// - /// A table with the same name already exists but will be altered - /// - ValidAlterable = 2, - - /// - /// A table with the same name already exists and cannot be altered, a different name is required - /// - InvalidExists = 100, - - /// - /// The table name contains words or characters that cannot be used in the name of a table - /// - InvalidNamingRequirements = 101, + var date = configuration.Templates?.SingleOrDefault((entry) => entry.Class?.Equals(DateTemplateClassName) ?? false); + var holidays = configuration.Templates?.SingleOrDefault((entry) => entry.Class?.Equals(HolidaysTemplateClassName) ?? false); + var holidaysDefinition = configuration.Templates?.SingleOrDefault((entry) => entry.Class?.Equals(HolidaysDefinitionTemplateClassName) ?? false); + var timeIntelligence = configuration.Templates?.SingleOrDefault((entry) => entry.Class?.Equals(TimeIntelligenceTemplateClassName) ?? false); + + return (date, holidays, holidaysDefinition, timeIntelligence); } +} + +public enum TableValidation +{ + Unknown = 0, + + /// + /// A table with the same name does not exist and will be created + /// + ValidNotExists = 1, + + /// + /// A table with the same name already exists but will be altered + /// + ValidAlterable = 2, + + /// + /// A table with the same name already exists and cannot be altered, a different name is required + /// + InvalidExists = 100, + + /// + /// The table name contains words or characters that cannot be used in the name of a table + /// + InvalidNamingRequirements = 101, +} - internal static class TableValidationExtensions +internal static class TableValidationExtensions +{ + public static void Assert(this TableValidation value) { - public static void Assert(this TableValidation value) - { - BravoUnexpectedException.Assert(value != TableValidation.Unknown); - BravoUnexpectedException.Assert(value != TableValidation.InvalidExists); - BravoUnexpectedException.Assert(value != TableValidation.InvalidNamingRequirements); - } + BravoUnexpectedException.Assert(value != TableValidation.Unknown); + BravoUnexpectedException.Assert(value != TableValidation.InvalidExists); + BravoUnexpectedException.Assert(value != TableValidation.InvalidNamingRequirements); } } diff --git a/src/Models/ManageDates/DateDefaults.cs b/src/Models/ManageDates/DateDefaults.cs index f0228fa6..47766996 100644 --- a/src/Models/ManageDates/DateDefaults.cs +++ b/src/Models/ManageDates/DateDefaults.cs @@ -1,138 +1,137 @@ -namespace Sqlbi.Bravo.Models.ManageDates -{ - using Dax.Template.Tables; - using Sqlbi.Bravo.Infrastructure.Extensions; - using System; - using System.Globalization; - using System.Text.Json.Serialization; +using System; +using System.Globalization; +using System.Text.Json.Serialization; +using Dax.Template.Tables; +using Sqlbi.Bravo.Infrastructure.Extensions; - public class DateDefaults - { - [JsonPropertyName("firstFiscalMonth")] - public int? FirstFiscalMonth { get; set; } +namespace Sqlbi.Bravo.Models.ManageDates; - [JsonPropertyName("firstDayOfWeek")] - public DayOfWeek? FirstDayOfWeek { get; set; } +public class DateDefaults +{ + [JsonPropertyName("firstFiscalMonth")] + public int? FirstFiscalMonth { get; set; } - [JsonPropertyName("monthsInYear")] - public int? MonthsInYear { get; set; } + [JsonPropertyName("firstDayOfWeek")] + public DayOfWeek? FirstDayOfWeek { get; set; } - [JsonPropertyName("workingDayType")] - public string? WorkingDayType { get; set; } + [JsonPropertyName("monthsInYear")] + public int? MonthsInYear { get; set; } - [JsonPropertyName("nonWorkingDayType")] - public string? NonWorkingDayType { get; set; } + [JsonPropertyName("workingDayType")] + public string? WorkingDayType { get; set; } - [JsonPropertyName("typeStartFiscalYear")] - public TypeStartFiscalYear? TypeStartFiscalYear { get; set; } + [JsonPropertyName("nonWorkingDayType")] + public string? NonWorkingDayType { get; set; } - [JsonPropertyName("quarterWeekType")] - public QuarterWeekType? QuarterWeekType { get; set; } + [JsonPropertyName("typeStartFiscalYear")] + public TypeStartFiscalYear? TypeStartFiscalYear { get; set; } - [JsonPropertyName("weeklyType")] - public WeeklyType? WeeklyType { get; set; } + [JsonPropertyName("quarterWeekType")] + public QuarterWeekType? QuarterWeekType { get; set; } - public void CopyTo(TemplateConfiguration templateConfiguration) + [JsonPropertyName("weeklyType")] + public WeeklyType? WeeklyType { get; set; } + + public void CopyTo(TemplateConfiguration templateConfiguration) + { + Set(nameof(FirstFiscalMonth), FirstFiscalMonth, quoted: false); + Set(nameof(FirstDayOfWeek), (int?)FirstDayOfWeek, quoted: false); + Set(nameof(MonthsInYear), MonthsInYear, quoted: false); + Set(nameof(TypeStartFiscalYear), (int?)TypeStartFiscalYear, quoted: false); + Set(nameof(WorkingDayType), WorkingDayType, quoted: true); + Set(nameof(NonWorkingDayType), NonWorkingDayType, quoted: true); + Set(nameof(QuarterWeekType), (int?)QuarterWeekType, quoted: true); + Set(nameof(WeeklyType), WeeklyType, quoted: true); + + void Set(string name, T? value, bool quoted) { - Set(nameof(FirstFiscalMonth), FirstFiscalMonth, quoted: false); - Set(nameof(FirstDayOfWeek), (int?)FirstDayOfWeek, quoted: false); - Set(nameof(MonthsInYear), MonthsInYear, quoted: false); - Set(nameof(TypeStartFiscalYear), (int?)TypeStartFiscalYear, quoted: false); - Set(nameof(WorkingDayType), WorkingDayType, quoted: true); - Set(nameof(NonWorkingDayType), NonWorkingDayType, quoted: true); - Set(nameof(QuarterWeekType), (int?)QuarterWeekType, quoted: true); - Set(nameof(WeeklyType), WeeklyType, quoted: true); - - void Set(string name, T? value, bool quoted) - { - if (value is null) - return; + if (value is null) + return; - var parameterKey = $"__{ name }"; + var parameterKey = $"__{name}"; - if (templateConfiguration.DefaultVariables.ContainsKey(parameterKey)) - { - var parameterValue = quoted ? $"\"{ value }\"" : $"{ value }"; + if (templateConfiguration.DefaultVariables.ContainsKey(parameterKey)) + { + var parameterValue = quoted ? $"\"{value}\"" : $"{value}"; - templateConfiguration.DefaultVariables[parameterKey] = parameterValue; - } + templateConfiguration.DefaultVariables[parameterKey] = parameterValue; } } + } - public static DateDefaults CreateFrom(TemplateConfiguration templateConfiguration) - { - DateDefaults dateDefaults = new(); + public static DateDefaults CreateFrom(TemplateConfiguration templateConfiguration) + { + DateDefaults dateDefaults = new(); - dateDefaults.FirstFiscalMonth = GetInt(nameof(FirstFiscalMonth)); - dateDefaults.FirstDayOfWeek = GetString(nameof(FirstDayOfWeek)).TryParseTo(); - dateDefaults.MonthsInYear = GetInt(nameof(MonthsInYear)); - dateDefaults.WorkingDayType = GetString(nameof(WorkingDayType), unquote: true); - dateDefaults.NonWorkingDayType = GetString(nameof(NonWorkingDayType), unquote: true); - dateDefaults.TypeStartFiscalYear = GetString(nameof(TypeStartFiscalYear)).TryParseTo(); - dateDefaults.QuarterWeekType = GetString(nameof(QuarterWeekType), unquote: true).TryParseTo(); - dateDefaults.WeeklyType = GetString(nameof(WeeklyType), unquote: true).TryParseTo(); + dateDefaults.FirstFiscalMonth = GetInt(nameof(FirstFiscalMonth)); + dateDefaults.FirstDayOfWeek = GetString(nameof(FirstDayOfWeek)).TryParseTo(); + dateDefaults.MonthsInYear = GetInt(nameof(MonthsInYear)); + dateDefaults.WorkingDayType = GetString(nameof(WorkingDayType), unquote: true); + dateDefaults.NonWorkingDayType = GetString(nameof(NonWorkingDayType), unquote: true); + dateDefaults.TypeStartFiscalYear = GetString(nameof(TypeStartFiscalYear)).TryParseTo(); + dateDefaults.QuarterWeekType = GetString(nameof(QuarterWeekType), unquote: true).TryParseTo(); + dateDefaults.WeeklyType = GetString(nameof(WeeklyType), unquote: true).TryParseTo(); - // Override the template value only if the variable exists, otherwise keep the null value - if (dateDefaults.FirstFiscalMonth is not null) - dateDefaults.FirstFiscalMonth = 1; + // Override the template value only if the variable exists, otherwise keep the null value + if (dateDefaults.FirstFiscalMonth is not null) + dateDefaults.FirstFiscalMonth = 1; - // Override the template value only if the variable exists, otherwise keep the null value - if (dateDefaults.FirstDayOfWeek is not null) - dateDefaults.FirstDayOfWeek = DateTimeFormatInfo.CurrentInfo.FirstDayOfWeek; + // Override the template value only if the variable exists, otherwise keep the null value + if (dateDefaults.FirstDayOfWeek is not null) + dateDefaults.FirstDayOfWeek = DateTimeFormatInfo.CurrentInfo.FirstDayOfWeek; - return dateDefaults; + return dateDefaults; - int? GetInt(string? name) - { - var value = GetString(name); - if (value == null) - return null; + int? GetInt(string? name) + { + var value = GetString(name); + if (value == null) + return null; - if (int.TryParse(value, out var valueInt)) - return valueInt; + if (int.TryParse(value, out var valueInt)) + return valueInt; - return null; - } + return null; + } - string? GetString(string? name, bool unquote = false) - { - if (name.IsNullOrEmpty()) - return null; + string? GetString(string? name, bool unquote = false) + { + if (name.IsNullOrEmpty()) + return null; - var parameterKey = $"__{ name }"; + var parameterKey = $"__{name}"; - if (templateConfiguration.DefaultVariables.TryGetValue(parameterKey, out var parameterValue)) + if (templateConfiguration.DefaultVariables.TryGetValue(parameterKey, out var parameterValue)) + { + if (unquote) { - if (unquote) - { - if (parameterValue[0] == '"' && parameterValue[^1] == '"') - parameterValue = parameterValue[1..^1]; - } - - return parameterValue; + if (parameterValue[0] == '"' && parameterValue[^1] == '"') + parameterValue = parameterValue[1..^1]; } - return null; + return parameterValue; } + + return null; } } +} - public enum TypeStartFiscalYear - { - FirstDayOfFiscalYear = 0, - LastDayOfFiscalYear = 1, - } +public enum TypeStartFiscalYear +{ + FirstDayOfFiscalYear = 0, + LastDayOfFiscalYear = 1, +} - public enum WeeklyType - { - Last = 0, - Nearest = 1, - } +public enum WeeklyType +{ + Last = 0, + Nearest = 1, +} - public enum QuarterWeekType - { - Weekly445 = 445, - Weekly454 = 454, - Weekly544 = 544, - } +public enum QuarterWeekType +{ + Weekly445 = 445, + Weekly454 = 454, + Weekly544 = 544, } diff --git a/src/Models/ManageDates/PreviewChangesRequest.cs b/src/Models/ManageDates/PreviewChangesRequest.cs index 6837c32d..64a0d10a 100644 --- a/src/Models/ManageDates/PreviewChangesRequest.cs +++ b/src/Models/ManageDates/PreviewChangesRequest.cs @@ -1,33 +1,32 @@ -namespace Sqlbi.Bravo.Models.ManageDates -{ - using System.ComponentModel.DataAnnotations; - using System.Text.Json.Serialization; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; - public class PreviewChangesRequest - { - [Required] - [JsonPropertyName("report")] - public PBIDesktopReport? Report { get; set; } +namespace Sqlbi.Bravo.Models.ManageDates; - [Required] - [JsonPropertyName("settings")] - public PreviewChangesSettings? Settings { get; set; } - } +public class PreviewChangesRequest +{ + [Required] + [JsonPropertyName("report")] + public PBIDesktopReport? Report { get; set; } - public class PreviewChangesSettings - { - /// - /// to be applied - /// - [Required] - [JsonPropertyName("configuration")] - public DateConfiguration? Configuration { get; set; } + [Required] + [JsonPropertyName("settings")] + public PreviewChangesSettings? Settings { get; set; } +} + +public class PreviewChangesSettings +{ + /// + /// to be applied + /// + [Required] + [JsonPropertyName("configuration")] + public DateConfiguration? Configuration { get; set; } - /// - /// Number of records generated as a preview of requested changes - /// - [Required] - [JsonPropertyName("previewRows")] - public int PreviewRows { get; set; } = 0; - } + /// + /// Number of records generated as a preview of requested changes + /// + [Required] + [JsonPropertyName("previewRows")] + public int PreviewRows { get; set; } = 0; } diff --git a/src/Models/PBICloudDataset.cs b/src/Models/PBICloudDataset.cs index 7bfc3122..69b137f8 100644 --- a/src/Models/PBICloudDataset.cs +++ b/src/Models/PBICloudDataset.cs @@ -1,228 +1,227 @@ -namespace Sqlbi.Bravo.Models +using System; +using System.Diagnostics; +using System.Text.Json.Serialization; +using Sqlbi.Bravo.Infrastructure; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Models; +using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; +using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; + +namespace Sqlbi.Bravo.Models; + +[DebuggerDisplay("{WorkspaceName} - {DisplayName} - {ConnectionMode}")] +public class PBICloudDataset : IDataModel { - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Models; - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Contracts; - using System; - using System.Diagnostics; - using System.Text.Json.Serialization; - - [DebuggerDisplay("{WorkspaceName} - {DisplayName} - {ConnectionMode}")] - public class PBICloudDataset : IDataModel - { - [JsonPropertyName("workspaceId")] - public string? WorkspaceId { get; set; } + [JsonPropertyName("workspaceId")] + public string? WorkspaceId { get; set; } - [JsonPropertyName("workspaceName")] - public string? WorkspaceName { get; set; } + [JsonPropertyName("workspaceName")] + public string? WorkspaceName { get; set; } - [JsonPropertyName("workspaceObjectId")] - public string? WorkspaceObjectId { get; set; } + [JsonPropertyName("workspaceObjectId")] + public string? WorkspaceObjectId { get; set; } - [JsonPropertyName("id")] - public long? Id { get; set; } + [JsonPropertyName("id")] + public long? Id { get; set; } - [JsonPropertyName("serverName")] - public string? ServerName { get; set; } + [JsonPropertyName("serverName")] + public string? ServerName { get; set; } - [JsonPropertyName("databaseName")] - public string? DatabaseName { get; set; } + [JsonPropertyName("databaseName")] + public string? DatabaseName { get; set; } - [JsonPropertyName("externalServerName")] - public string? ExternalServerName { get; set; } + [JsonPropertyName("externalServerName")] + public string? ExternalServerName { get; set; } - [JsonPropertyName("externalDatabaseName")] - public string? ExternalDatabaseName { get; set; } + [JsonPropertyName("externalDatabaseName")] + public string? ExternalDatabaseName { get; set; } - [JsonPropertyName("identityProvider")] - public string? IdentityProvider { get; set; } + [JsonPropertyName("identityProvider")] + public string? IdentityProvider { get; set; } - [JsonPropertyName("name")] - public string? DisplayName { get; set; } + [JsonPropertyName("name")] + public string? DisplayName { get; set; } - [JsonPropertyName("description")] - public string? Description { get; set; } + [JsonPropertyName("description")] + public string? Description { get; set; } - [JsonPropertyName("owner")] - public string? Owner { get; set; } + [JsonPropertyName("owner")] + public string? Owner { get; set; } - [JsonPropertyName("refreshed")] - public DateTime? Refreshed { get; set; } + [JsonPropertyName("refreshed")] + public DateTime? Refreshed { get; set; } - [JsonPropertyName("onPremModelConnectionString")] - public string? OnPremModelConnectionString { get; set; } + [JsonPropertyName("onPremModelConnectionString")] + public string? OnPremModelConnectionString { get; set; } - [JsonPropertyName("endorsement")] - public PBICloudDatasetEndorsement? Endorsement { get; set; } + [JsonPropertyName("endorsement")] + public PBICloudDatasetEndorsement? Endorsement { get; set; } - [JsonPropertyName("workspaceType")] - public PBICloudDatasetWorkspaceType? WorkspaceType { get; set; } + [JsonPropertyName("workspaceType")] + public PBICloudDatasetWorkspaceType? WorkspaceType { get; set; } - [JsonPropertyName("capacitySkuType")] - public PBICloudDatasetCapacitySkuType? CapacitySkuType { get; set; } + [JsonPropertyName("capacitySkuType")] + public PBICloudDatasetCapacitySkuType? CapacitySkuType { get; set; } - [JsonPropertyName("isPushDataEnabled")] - public bool? IsPushDataEnabled { get; set; } + [JsonPropertyName("isPushDataEnabled")] + public bool? IsPushDataEnabled { get; set; } - [JsonPropertyName("isExcelWorkbook")] - public bool? IsExcelWorkbook { get; set; } + [JsonPropertyName("isExcelWorkbook")] + public bool? IsExcelWorkbook { get; set; } - [JsonPropertyName("isOnPremModel")] - public bool? IsOnPremModel { get; set; } + [JsonPropertyName("isOnPremModel")] + public bool? IsOnPremModel { get; set; } - [JsonPropertyName("isPremiumCapacity")] - public bool? IsPremiumCapacity { get; set; } + [JsonPropertyName("isPremiumCapacity")] + public bool? IsPremiumCapacity { get; set; } - [JsonPropertyName("isXmlaEndPointSupported")] - public bool IsXmlaEndPointSupported + [JsonPropertyName("isXmlaEndPointSupported")] + public bool IsXmlaEndPointSupported + { + get { - get - { - if (IsPremiumCapacity is null || IsPremiumCapacity == false) - return false; - - // Exclude unsupported datasets - a.k.a. datasets not accessible by the XMLA endpoint - // see https://docs.microsoft.com/en-us/power-bi/admin/service-premium-connect-tools#unsupported-datasets - - // Datasets in 'My Workspace' are unsupported - if (WorkspaceType == PBICloudDatasetWorkspaceType.PersonalGroup) - return false; + if (IsPremiumCapacity is null || IsPremiumCapacity == false) + return false; - // Datasets based on a live connection to an Azure Analysis Services or SQL Server Analysis Services model are unsupported - if (IsOnPremModel ?? false) - return false; + // Exclude unsupported datasets - a.k.a. datasets not accessible by the XMLA endpoint + // see https://docs.microsoft.com/en-us/power-bi/admin/service-premium-connect-tools#unsupported-datasets - // TODO: Exclude datasets based on a live connection to a Power BI dataset in another workspace - // if ( ... ) - // return false; - - // Datasets with Push data by using the REST API are unsupported - if (IsPushDataEnabled ?? false) - return false; - - // Excel workbook datasets are unsupported - if (IsExcelWorkbook ?? false) - return false; - - return true; - } - } + // Datasets in 'My Workspace' are unsupported + if (WorkspaceType == PBICloudDatasetWorkspaceType.PersonalGroup) + return false; - [JsonPropertyName("connectionMode")] - public PBICloudDatasetConnectionMode ConnectionMode { get; set; } = PBICloudDatasetConnectionMode.Unknown; + // Datasets based on a live connection to an Azure Analysis Services or SQL Server Analysis Services model are unsupported + if (IsOnPremModel ?? false) + return false; - public override bool Equals(object? obj) - { - return Equals(obj as PBICloudDataset); - } + // TODO: Exclude datasets based on a live connection to a Power BI dataset in another workspace + // if ( ... ) + // return false; - public bool Equals(PBICloudDataset? other) - { - return other != null && - WorkspaceId == other.WorkspaceId && - Id == other.Id; - } + // Datasets with Push data by using the REST API are unsupported + if (IsPushDataEnabled ?? false) + return false; - public override int GetHashCode() - { - HashCode hash = new(); - hash.Add(WorkspaceId); - hash.Add(Id); - return hash.ToHashCode(); - } + // Excel workbook datasets are unsupported + if (IsExcelWorkbook ?? false) + return false; - internal static PBICloudDataset CreateFrom(CloudEnvironment environment, CloudWorkspace cloudWorkspace, CloudSharedModel cloudSharedModel) - { - BravoUnexpectedException.ThrowIfNull(cloudWorkspace); - BravoUnexpectedException.ThrowIfNull(cloudSharedModel); - BravoUnexpectedException.ThrowIfNull(cloudSharedModel.Model); - - var cloudModel = cloudSharedModel.Model; - - var dataset = new PBICloudDataset - { - WorkspaceId = cloudWorkspace.Id, - WorkspaceName = cloudWorkspace.Name.NullIfEmpty() ?? cloudSharedModel.WorkspaceName, - WorkspaceObjectId = cloudWorkspace.ObjectId, - Id = cloudModel.Id, - ServerName = CommonHelper.ChangeUriScheme(environment.BackendUri, CloudApiClient.PBIDatasetProtocolScheme, ignorePort: true), - DatabaseName = cloudModel.DBName, - ExternalServerName = null, - ExternalDatabaseName = null, - IdentityProvider = environment.GetIdentityProvider(), - DisplayName = cloudModel.DisplayName, - Description = cloudModel.Description, - Owner = $"{ cloudModel.CreatorUser?.GivenName } { cloudModel.CreatorUser?.FamilyName }", - Refreshed = cloudModel.LastRefreshTime, - OnPremModelConnectionString = cloudModel.OnPremModelConnectionString, - Endorsement = cloudSharedModel.GalleryItem?.Stage.TryParseTo(), - WorkspaceType = cloudSharedModel.WorkspaceType.TryParseTo(), - CapacitySkuType = cloudWorkspace.CapacitySkuType.TryParseTo(), - IsPremiumCapacity = cloudWorkspace.IsPremiumCapacity, - IsPushDataEnabled = cloudModel.IsPushDataEnabled, - IsExcelWorkbook = cloudModel.IsExcelWorkbook, - IsOnPremModel = cloudModel.IsOnPremModel, - ConnectionMode = PBICloudDatasetConnectionMode.Supported, - }; - - if (dataset.IsXmlaEndPointSupported) - { - dataset.ExternalServerName = CommonHelper.ChangeUriScheme(environment.ClusterUri, CloudApiClient.PBIPremiumXmlaEndpointProtocolScheme, ignorePort: true); - dataset.ExternalDatabaseName = cloudModel.DisplayName; - } - else if (dataset.IsOnPremModel == true) - { - var properties = ConnectionStringHelper.GetConnectionStringProperties(dataset.OnPremModelConnectionString); - dataset.ExternalServerName = dataset.ServerName = properties.ServerName; - dataset.ExternalDatabaseName = dataset.DatabaseName = properties.DatabaseName; - } - else - { - dataset.ExternalServerName = dataset.ServerName; - dataset.ExternalDatabaseName = $"{ cloudModel.VSName }-{ cloudModel.DBName }"; - } - - return dataset; + return true; } } - /// - /// Re-mapping - /// - public enum PBICloudDatasetEndorsement + [JsonPropertyName("connectionMode")] + public PBICloudDatasetConnectionMode ConnectionMode { get; set; } = PBICloudDatasetConnectionMode.Unknown; + + public override bool Equals(object? obj) { - None = 0, - Promoted = 1, - Certified = 2, + return Equals(obj as PBICloudDataset); } - /// - /// Re-mapping - /// - public enum PBICloudDatasetWorkspaceType + public bool Equals(PBICloudDataset? other) { - Personal = 0, - Workspace = 1, - Group = 2, - PersonalGroup = 3 + return other != null && + WorkspaceId == other.WorkspaceId && + Id == other.Id; } - /// - /// Re-mapping - /// - public enum PBICloudDatasetCapacitySkuType + public override int GetHashCode() { - Unknown = 0, - Premium, - Shared, + HashCode hash = new(); + hash.Add(WorkspaceId); + hash.Add(Id); + return hash.ToHashCode(); } - public enum PBICloudDatasetConnectionMode + internal static PBICloudDataset CreateFrom(CloudEnvironment environment, CloudWorkspace cloudWorkspace, CloudSharedModel cloudSharedModel) { - Unknown = 0, - Supported = 1, + BravoUnexpectedException.ThrowIfNull(cloudWorkspace); + BravoUnexpectedException.ThrowIfNull(cloudSharedModel); + BravoUnexpectedException.ThrowIfNull(cloudSharedModel.Model); + + var cloudModel = cloudSharedModel.Model; + + var dataset = new PBICloudDataset + { + WorkspaceId = cloudWorkspace.Id, + WorkspaceName = cloudWorkspace.Name.NullIfEmpty() ?? cloudSharedModel.WorkspaceName, + WorkspaceObjectId = cloudWorkspace.ObjectId, + Id = cloudModel.Id, + ServerName = CommonHelper.ChangeUriScheme(environment.BackendUri, CloudApiClient.PBIDatasetProtocolScheme, ignorePort: true), + DatabaseName = cloudModel.DBName, + ExternalServerName = null, + ExternalDatabaseName = null, + IdentityProvider = environment.GetIdentityProvider(), + DisplayName = cloudModel.DisplayName, + Description = cloudModel.Description, + Owner = $"{cloudModel.CreatorUser?.GivenName} {cloudModel.CreatorUser?.FamilyName}", + Refreshed = cloudModel.LastRefreshTime, + OnPremModelConnectionString = cloudModel.OnPremModelConnectionString, + Endorsement = cloudSharedModel.GalleryItem?.Stage.TryParseTo(), + WorkspaceType = cloudSharedModel.WorkspaceType.TryParseTo(), + CapacitySkuType = cloudWorkspace.CapacitySkuType.TryParseTo(), + IsPremiumCapacity = cloudWorkspace.IsPremiumCapacity, + IsPushDataEnabled = cloudModel.IsPushDataEnabled, + IsExcelWorkbook = cloudModel.IsExcelWorkbook, + IsOnPremModel = cloudModel.IsOnPremModel, + ConnectionMode = PBICloudDatasetConnectionMode.Supported, + }; + + if (dataset.IsXmlaEndPointSupported) + { + dataset.ExternalServerName = CommonHelper.ChangeUriScheme(environment.ClusterUri, CloudApiClient.PBIPremiumXmlaEndpointProtocolScheme, ignorePort: true); + dataset.ExternalDatabaseName = cloudModel.DisplayName; + } + else if (dataset.IsOnPremModel == true) + { + var properties = ConnectionStringHelper.GetConnectionStringProperties(dataset.OnPremModelConnectionString); + dataset.ExternalServerName = dataset.ServerName = properties.ServerName; + dataset.ExternalDatabaseName = dataset.DatabaseName = properties.DatabaseName; + } + else + { + dataset.ExternalServerName = dataset.ServerName; + dataset.ExternalDatabaseName = $"{cloudModel.VSName}-{cloudModel.DBName}"; + } + + return dataset; } -} \ No newline at end of file +} + +/// +/// Re-mapping +/// +public enum PBICloudDatasetEndorsement +{ + None = 0, + Promoted = 1, + Certified = 2, +} + +/// +/// Re-mapping +/// +public enum PBICloudDatasetWorkspaceType +{ + Personal = 0, + Workspace = 1, + Group = 2, + PersonalGroup = 3 +} + +/// +/// Re-mapping +/// +public enum PBICloudDatasetCapacitySkuType +{ + Unknown = 0, + Premium, + Shared, +} + +public enum PBICloudDatasetConnectionMode +{ + Unknown = 0, + Supported = 1, +} diff --git a/src/Models/PBIDesktopReport.cs b/src/Models/PBIDesktopReport.cs index 18923896..915a8ab5 100644 --- a/src/Models/PBIDesktopReport.cs +++ b/src/Models/PBIDesktopReport.cs @@ -1,217 +1,215 @@ -namespace Sqlbi.Bravo.Models +using System; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Net.NetworkInformation; +using System.Text.Json.Serialization; +using Sqlbi.Bravo.Infrastructure; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Models; +using SSAS = Microsoft.AnalysisServices; +using TOM = Microsoft.AnalysisServices.Tabular; + +namespace Sqlbi.Bravo.Models; + +[DebuggerDisplay("{ServerName} - {ReportName} - {ConnectionMode}")] +public class PBIDesktopReport : IDataModel { - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Models; - using Sqlbi.Bravo.Infrastructure.Security; - using System; - using System.ComponentModel.DataAnnotations; - using System.Diagnostics; - using System.Linq; - using System.Net; - using System.Net.NetworkInformation; - using System.Text.Json.Serialization; - using SSAS = Microsoft.AnalysisServices; - using TOM = Microsoft.AnalysisServices.Tabular; - - [DebuggerDisplay("{ServerName} - {ReportName} - {ConnectionMode}")] - public class PBIDesktopReport : IDataModel + public PBIDesktopReport() { - public PBIDesktopReport() - { - } + } - [Required] - [JsonPropertyName("id")] - public int? ProcessId { get; set; } + [Required] + [JsonPropertyName("id")] + public int? ProcessId { get; set; } - [JsonPropertyName("reportName")] - public string? ReportName { get; set; } + [JsonPropertyName("reportName")] + public string? ReportName { get; set; } - [JsonPropertyName("serverName")] - public string? ServerName { get; set; } + [JsonPropertyName("serverName")] + public string? ServerName { get; set; } - [JsonPropertyName("databaseName")] - public string? DatabaseName { get; set; } + [JsonPropertyName("databaseName")] + public string? DatabaseName { get; set; } - [JsonPropertyName("compatibilityMode")] - public SSAS.CompatibilityMode CompatibilityMode { get; set; } = SSAS.CompatibilityMode.Unknown; + [JsonPropertyName("compatibilityMode")] + public SSAS.CompatibilityMode CompatibilityMode { get; set; } = SSAS.CompatibilityMode.Unknown; - [JsonPropertyName("connectionMode")] - public PBIDesktopReportConnectionMode ConnectionMode { get; set; } = PBIDesktopReportConnectionMode.Unknown; + [JsonPropertyName("connectionMode")] + public PBIDesktopReportConnectionMode ConnectionMode { get; set; } = PBIDesktopReportConnectionMode.Unknown; - public override bool Equals(object? obj) - { - return Equals(obj as PBIDesktopReport); - } + public override bool Equals(object? obj) + { + return Equals(obj as PBIDesktopReport); + } + + public bool Equals(PBIDesktopReport? other) + { + return other != null && + ProcessId == other.ProcessId && + ServerName == other.ServerName && + DatabaseName == other.DatabaseName; + } - public bool Equals(PBIDesktopReport? other) + public override int GetHashCode() + { + return HashCode.Combine(ProcessId, ServerName, DatabaseName); + } + + internal static PBIDesktopReport? CreateFrom(int processId, bool connectionModeEnabled = true) + { + using var process = ProcessHelper.SafeGetProcessById(processId); + + if (process is not null) { - return other != null && - ProcessId == other.ProcessId && - ServerName == other.ServerName && - DatabaseName == other.DatabaseName; + var report = CreateFrom(process, connectionModeEnabled); + return report; } - public override int GetHashCode() + return null; + } + + internal static PBIDesktopReport CreateFrom(Process process, bool connectionModeEnabled = true) + { + var report = new PBIDesktopReport + { + ProcessId = process.Id, + ReportName = process.GetPBIDesktopMainWindowTitle(), + ServerName = null, + DatabaseName = null, + CompatibilityMode = SSAS.CompatibilityMode.Unknown, + ConnectionMode = PBIDesktopReportConnectionMode.Unknown, + }; + + if (connectionModeEnabled) { - return HashCode.Combine(ProcessId, ServerName, DatabaseName); + if (report.ReportName is null) + { + report.ConnectionMode = PBIDesktopReportConnectionMode.UnsupportedProcessNotReady; + } + else + { + GetConnectionMode(out var serverName, out var databaseName, out var compatibilityMode, out var connectionMode); + report.ServerName = serverName; + report.DatabaseName = databaseName; + report.CompatibilityMode = compatibilityMode; + report.ConnectionMode = connectionMode; + } } - internal static PBIDesktopReport? CreateFrom(int processId, bool connectionModeEnabled = true) + return report; + + void GetConnectionMode(out string? serverName, out string? databaseName, out SSAS.CompatibilityMode compatibilityMode, out PBIDesktopReportConnectionMode connectionMode) { - using var process = ProcessHelper.SafeGetProcessById(processId); + serverName = null; + databaseName = null; + compatibilityMode = SSAS.CompatibilityMode.Unknown; - if (process is not null) + var ssasPIDs = process.GetChildrenPIDs(childProcessImageName: AppEnvironment.PBIDesktopSSASProcessImageName).ToArray(); + if (ssasPIDs.Length != 1) { - var report = CreateFrom(process, connectionModeEnabled); - return report; + connectionMode = PBIDesktopReportConnectionMode.UnsupportedAnalysisServicesProcessNotFound; + return; } - return null; - } + var ssasPID = ssasPIDs.Single(); - internal static PBIDesktopReport CreateFrom(Process process, bool connectionModeEnabled = true) - { - var report = new PBIDesktopReport + var ssasConnection = NetworkHelper.GetTcpConnections((c) => c.ProcessId == ssasPID && c.State == TcpState.Listen && IPAddress.IsLoopback(c.EndPoint.Address)).FirstOrDefault(); + if (ssasConnection == default) { - ProcessId = process.Id, - ReportName = process.GetPBIDesktopMainWindowTitle(), - ServerName = null, - DatabaseName = null, - CompatibilityMode = SSAS.CompatibilityMode.Unknown, - ConnectionMode = PBIDesktopReportConnectionMode.Unknown, - }; - - if (connectionModeEnabled) + connectionMode = PBIDesktopReportConnectionMode.UnsupportedAnalysisServicesConnectionNotFound; + return; + } + + using var server = new TOM.Server(); + var connectionString = ConnectionStringHelper.BuildFor(ssasConnection.EndPoint); + try { - if (report.ReportName is null) - { - report.ConnectionMode = PBIDesktopReportConnectionMode.UnsupportedProcessNotReady; - } - else - { - GetConnectionMode(out var serverName, out var databaseName, out var compatibilityMode, out var connectionMode); - report.ServerName = serverName; - report.DatabaseName = databaseName; - report.CompatibilityMode = compatibilityMode; - report.ConnectionMode = connectionMode; - } + server.Connect(connectionString); + compatibilityMode = server.CompatibilityMode; } + catch (Exception ex) + { + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(name: $"{nameof(PBIDesktopReport)}.{nameof(CreateFrom)}.{nameof(GetConnectionMode)}", ex, DiagnosticMessageSeverity.Warning); - return report; + connectionMode = PBIDesktopReportConnectionMode.UnsupportedConnectionException; + return; + } + + if (server.CompatibilityMode != SSAS.CompatibilityMode.PowerBI) + { + connectionMode = PBIDesktopReportConnectionMode.UnsupportedAnalysisServicesCompatibilityMode; + return; + } + + if (server.Databases.Count == 0) + { + connectionMode = PBIDesktopReportConnectionMode.UnsupportedDatabaseCollectionEmpty; + return; + } - void GetConnectionMode(out string? serverName, out string? databaseName, out SSAS.CompatibilityMode compatibilityMode, out PBIDesktopReportConnectionMode connectionMode) + if (server.Databases.Count > 1) { - serverName = null; - databaseName = null; - compatibilityMode = SSAS.CompatibilityMode.Unknown; - - var ssasPIDs = process.GetChildrenPIDs(childProcessImageName: AppEnvironment.PBIDesktopSSASProcessImageName).ToArray(); - if (ssasPIDs.Length != 1) - { - connectionMode = PBIDesktopReportConnectionMode.UnsupportedAnalysisServicesProcessNotFound; - return; - } - - var ssasPID = ssasPIDs.Single(); - - var ssasConnection = NetworkHelper.GetTcpConnections((c) => c.ProcessId == ssasPID && c.State == TcpState.Listen && IPAddress.IsLoopback(c.EndPoint.Address)).FirstOrDefault(); - if (ssasConnection == default) - { - connectionMode = PBIDesktopReportConnectionMode.UnsupportedAnalysisServicesConnectionNotFound; - return; - } - - using var server = new TOM.Server(); - var connectionString = ConnectionStringHelper.BuildFor(ssasConnection.EndPoint); - try - { - server.Connect(connectionString); - compatibilityMode = server.CompatibilityMode; - } - catch (Exception ex) - { - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(name: $"{ nameof(PBIDesktopReport) }.{ nameof(CreateFrom) }.{ nameof(GetConnectionMode) }", ex, DiagnosticMessageSeverity.Warning); - - connectionMode = PBIDesktopReportConnectionMode.UnsupportedConnectionException; - return; - } - - if (server.CompatibilityMode != SSAS.CompatibilityMode.PowerBI) - { - connectionMode = PBIDesktopReportConnectionMode.UnsupportedAnalysisServicesCompatibilityMode; - return; - } - - if (server.Databases.Count == 0) - { - connectionMode = PBIDesktopReportConnectionMode.UnsupportedDatabaseCollectionEmpty; - return; - } - - if (server.Databases.Count > 1) - { - connectionMode = PBIDesktopReportConnectionMode.UnsupportedDatabaseCollectionUnexpectedCount; - return; - } - - var database = server.Databases[0]; - - // Do we need this check ?? (e.g UnsupportedDatabaseNotYetReadyOrUnloaded) - // if (database.IsLoaded == false) { } - - serverName = $"{ NetworkHelper.Localhost }:{ ssasConnection.EndPoint.Port }"; // we're using 'localhost:' instead of ':' in order to allow both ipv4 and ipv6 connections - databaseName = database.Name; - connectionMode = PBIDesktopReportConnectionMode.Supported; + connectionMode = PBIDesktopReportConnectionMode.UnsupportedDatabaseCollectionUnexpectedCount; + return; } + + var database = server.Databases[0]; + + // Do we need this check ?? (e.g UnsupportedDatabaseNotYetReadyOrUnloaded) + // if (database.IsLoaded == false) { } + + serverName = $"{NetworkHelper.Localhost}:{ssasConnection.EndPoint.Port}"; // we're using 'localhost:' instead of ':' in order to allow both ipv4 and ipv6 connections + databaseName = database.Name; + connectionMode = PBIDesktopReportConnectionMode.Supported; } } +} - public enum PBIDesktopReportConnectionMode - { - Unknown = 0, - - /// - /// Connection supported - /// - Supported = 1, - - /// - /// PBIDesktop process is opening or the Analysis Services instance/model is not yet ready - /// - UnsupportedProcessNotReady = 2, - - /// - /// PBIDesktop Analysis Services instance process not found. - /// - UnsupportedAnalysisServicesProcessNotFound = 3, - - /// - /// PBIDesktop Analysis Services TCP connection not found. - /// - UnsupportedAnalysisServicesConnectionNotFound = 4, - - /// - /// PBIDesktop Analysis Services instance compatibility mode is not PowerBI. - /// - UnsupportedAnalysisServicesCompatibilityMode = 5, - - /// - /// PBIDesktop Analysis Services instance does not contains any databases. The PBIDesktop report is connected to an external database/model like Power BI datasets or .. ?? - /// - UnsupportedDatabaseCollectionEmpty = 6, - - /// - /// PBIDesktop Analysis Services instance contains an unexpected number of databases (> 1) while we expect zero or one. - /// - UnsupportedDatabaseCollectionUnexpectedCount = 7, - - /// - /// An exception was raised when connecting to the PBIDesktop Analysis Services instance. - /// - UnsupportedConnectionException = 8, - } +public enum PBIDesktopReportConnectionMode +{ + Unknown = 0, + + /// + /// Connection supported + /// + Supported = 1, + + /// + /// PBIDesktop process is opening or the Analysis Services instance/model is not yet ready + /// + UnsupportedProcessNotReady = 2, + + /// + /// PBIDesktop Analysis Services instance process not found. + /// + UnsupportedAnalysisServicesProcessNotFound = 3, + + /// + /// PBIDesktop Analysis Services TCP connection not found. + /// + UnsupportedAnalysisServicesConnectionNotFound = 4, + + /// + /// PBIDesktop Analysis Services instance compatibility mode is not PowerBI. + /// + UnsupportedAnalysisServicesCompatibilityMode = 5, + + /// + /// PBIDesktop Analysis Services instance does not contains any databases. The PBIDesktop report is connected to an external database/model like Power BI datasets or .. ?? + /// + UnsupportedDatabaseCollectionEmpty = 6, + + /// + /// PBIDesktop Analysis Services instance contains an unexpected number of databases (> 1) while we expect zero or one. + /// + UnsupportedDatabaseCollectionUnexpectedCount = 7, + + /// + /// An exception was raised when connecting to the PBIDesktop Analysis Services instance. + /// + UnsupportedConnectionException = 8, } diff --git a/src/Models/TemplateDevelopment/CreateWorkspaceRequest.cs b/src/Models/TemplateDevelopment/CreateWorkspaceRequest.cs index 15109726..462a9a03 100644 --- a/src/Models/TemplateDevelopment/CreateWorkspaceRequest.cs +++ b/src/Models/TemplateDevelopment/CreateWorkspaceRequest.cs @@ -1,18 +1,17 @@ -namespace Sqlbi.Bravo.Models.TemplateDevelopment -{ - using Sqlbi.Bravo.Models.ManageDates; - using System.ComponentModel.DataAnnotations; - using System.Text.Json.Serialization; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; +using Sqlbi.Bravo.Models.ManageDates; + +namespace Sqlbi.Bravo.Models.TemplateDevelopment; - public class CreateWorkspaceRequest - { - [Required] - [JsonPropertyName("name")] - public string? Name { get; set; } +public class CreateWorkspaceRequest +{ + [Required] + [JsonPropertyName("name")] + public string? Name { get; set; } - [Required] - [JsonPropertyName("configuration")] - public DateConfiguration? Configuration { get; set; } - } + [Required] + [JsonPropertyName("configuration")] + public DateConfiguration? Configuration { get; set; } } diff --git a/src/Models/TemplateDevelopment/WorkspacePreviewChangesRequest.cs b/src/Models/TemplateDevelopment/WorkspacePreviewChangesRequest.cs index e08776da..0b143ad1 100644 --- a/src/Models/TemplateDevelopment/WorkspacePreviewChangesRequest.cs +++ b/src/Models/TemplateDevelopment/WorkspacePreviewChangesRequest.cs @@ -1,33 +1,32 @@ -namespace Sqlbi.Bravo.Models.TemplateDevelopment -{ - using System.ComponentModel.DataAnnotations; - using System.Text.Json.Serialization; +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; - public class WorkspacePreviewChangesRequest - { - [Required] - [JsonPropertyName("report")] - public PBIDesktopReport? Report { get; set; } +namespace Sqlbi.Bravo.Models.TemplateDevelopment; - [Required] - [JsonPropertyName("settings")] - public WorkspacePreviewChangesSettings? Settings { get; set; } - } +public class WorkspacePreviewChangesRequest +{ + [Required] + [JsonPropertyName("report")] + public PBIDesktopReport? Report { get; set; } - public class WorkspacePreviewChangesSettings - { - /// - /// Full path of the custom package to be applied - /// - [Required] - [JsonPropertyName("customPackagePath")] - public string? CustomPackagePath { get; set; } + [Required] + [JsonPropertyName("settings")] + public WorkspacePreviewChangesSettings? Settings { get; set; } +} + +public class WorkspacePreviewChangesSettings +{ + /// + /// Full path of the custom package to be applied + /// + [Required] + [JsonPropertyName("customPackagePath")] + public string? CustomPackagePath { get; set; } - /// - /// Number of records generated as a preview of requested changes - /// - [Required] - [JsonPropertyName("previewRows")] - public int PreviewRows { get; set; } = 0; - } + /// + /// Number of records generated as a preview of requested changes + /// + [Required] + [JsonPropertyName("previewRows")] + public int PreviewRows { get; set; } = 0; } diff --git a/src/Program.Host.cs b/src/Program.Host.cs index 1f1fc157..12d5f69e 100644 --- a/src/Program.Host.cs +++ b/src/Program.Host.cs @@ -1,68 +1,68 @@ -namespace Sqlbi.Bravo -{ - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Hosting; - using Microsoft.Extensions.Logging; - using Microsoft.Extensions.Logging.EventLog; - using System.Net; +using System; +using System.Net; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.EventLog; + +namespace Sqlbi.Bravo; - internal partial class Program +internal partial class Program +{ + private static IHost CreateHost() { - private static IHost CreateHost() - { - var hostBuilder = new HostBuilder(); + var hostBuilder = new HostBuilder(); - hostBuilder.UseEnvironment(Environments.Production); - hostBuilder.UseContentRoot(Environment.CurrentDirectory); - hostBuilder.ConfigureHostConfiguration((builder) => - { - builder.SetBasePath(Environment.CurrentDirectory); - }); + hostBuilder.UseEnvironment(Environments.Production); + hostBuilder.UseContentRoot(Environment.CurrentDirectory); + hostBuilder.ConfigureHostConfiguration((builder) => + { + builder.SetBasePath(Environment.CurrentDirectory); + }); - hostBuilder.ConfigureLogging((context, logging) => - { - logging.AddFilter((level) => level >= LogLevel.Warning); - logging.AddEventSourceLogger(); - logging.AddEventLog(); + hostBuilder.ConfigureLogging((context, logging) => + { + logging.AddFilter((level) => level >= LogLevel.Warning); + logging.AddEventSourceLogger(); + logging.AddEventLog(); #if DEBUG - logging.AddConsole(); - logging.AddDebug(); + logging.AddConsole(); + logging.AddDebug(); #endif - logging.Configure((options) => - { - options.ActivityTrackingOptions = ActivityTrackingOptions.SpanId | ActivityTrackingOptions.TraceId | ActivityTrackingOptions.ParentId; - }); + logging.Configure((options) => + { + options.ActivityTrackingOptions = ActivityTrackingOptions.SpanId | ActivityTrackingOptions.TraceId | ActivityTrackingOptions.ParentId; }); + }); - hostBuilder.UseDefaultServiceProvider((context, options) => - { + hostBuilder.UseDefaultServiceProvider((context, options) => + { #if DEBUG - options.ValidateOnBuild = options.ValidateScopes = true; + options.ValidateOnBuild = options.ValidateScopes = true; #endif - }); + }); - hostBuilder.ConfigureWebHostDefaults((webBuilder) => + hostBuilder.ConfigureWebHostDefaults((webBuilder) => + { + // Empty and ignore default URLs configured on the IWebHostBuilder - this remove the warning 'Microsoft.AspNetCore.Server.Kestrel: Warning: Overriding address(es) 'https://localhost:5001/, http://localhost:5000/'. Binding to endpoints defined in UseKestrel() instead.' + webBuilder.UseUrls(); + webBuilder.UseKestrel((serverOptions) => { - // Empty and ignore default URLs configured on the IWebHostBuilder - this remove the warning 'Microsoft.AspNetCore.Server.Kestrel: Warning: Overriding address(es) 'https://localhost:5001/, http://localhost:5000/'. Binding to endpoints defined in UseKestrel() instead.' - webBuilder.UseUrls(); - webBuilder.UseKestrel((serverOptions) => - { #if DEBUG - const int port = 5000; + const int port = 5000; #else - const int port = 0; // Use dynamic port assignment + const int port = 0; // Use dynamic port assignment #endif - serverOptions.Listen(new IPEndPoint(IPAddress.Loopback, port)); - serverOptions.AllowSynchronousIO = true; // required by ImportVpax - }); - - webBuilder.UseStartup(); + serverOptions.Listen(new IPEndPoint(IPAddress.Loopback, port)); + serverOptions.AllowSynchronousIO = true; // required by ImportVpax }); - var host = hostBuilder.Build(); - return host; - } + webBuilder.UseStartup(); + }); + + var host = hostBuilder.Build(); + return host; } } diff --git a/src/Program.cs b/src/Program.cs index b11cfd4e..f2865f54 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,43 +1,43 @@ -namespace Sqlbi.Bravo -{ - using Microsoft.Extensions.Hosting; - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.Configuration; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Telemetry; - using System.Windows.Forms; +using System; +using System.Windows.Forms; +using Microsoft.Extensions.Hosting; +using Sqlbi.Bravo.Infrastructure; +using Sqlbi.Bravo.Infrastructure.Configuration; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Telemetry; + +namespace Sqlbi.Bravo; - internal partial class Program +internal partial class Program +{ + [STAThread] + public static void Main() { - [STAThread] - public static void Main() + try { - try - { - StartupConfiguration.Configure(); + StartupConfiguration.Configure(); - using var instance = new AppInstance(); - if (instance.IsOwned) - { - using var host = CreateHost(); - host.Start(); - { - var window = new AppWindow(host.Services, instance); - Application.Run(window); - } - host.StopAsync().GetAwaiter().GetResult(); - } - else + using var instance = new AppInstance(); + if (instance.IsOwned) + { + using var host = CreateHost(); + host.Start(); { - instance.NotifyOwner(); + var window = new AppWindow(host.Services, instance); + Application.Run(window); } + host.StopAsync().GetAwaiter().GetResult(); } - catch (Exception ex) + else { - TelemetryService.Instance.TrackException(ex); - ExceptionHelper.ShowDialog(ex); - throw; + instance.NotifyOwner(); } } + catch (Exception ex) + { + TelemetryService.Instance.TrackException(ex); + ExceptionHelper.ShowDialog(ex); + throw; + } } } diff --git a/src/Properties/Resources.Designer.cs b/src/Properties/Resources.Designer.cs index ce644fb7..eb0ec240 100644 --- a/src/Properties/Resources.Designer.cs +++ b/src/Properties/Resources.Designer.cs @@ -9,6 +9,7 @@ //------------------------------------------------------------------------------ namespace Sqlbi.Bravo.Properties { + using System; diff --git a/src/Services/AnalyzeModelService.cs b/src/Services/AnalyzeModelService.cs index 59ef4349..4212c8e3 100644 --- a/src/Services/AnalyzeModelService.cs +++ b/src/Services/AnalyzeModelService.cs @@ -1,124 +1,127 @@ -namespace Sqlbi.Bravo.Services +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Sqlbi.Bravo.Infrastructure; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; +using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Authentication; +using Sqlbi.Bravo.Infrastructure.Services; +using Sqlbi.Bravo.Infrastructure.Services.PowerBI; +using Sqlbi.Bravo.Models; +using Sqlbi.Bravo.Models.AnalyzeModel; + +namespace Sqlbi.Bravo.Services; + +public interface IAnalyzeModelService { - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Authentication; - using Sqlbi.Bravo.Infrastructure.Services; - using Sqlbi.Bravo.Infrastructure.Services.PowerBI; - using Sqlbi.Bravo.Models; - using Sqlbi.Bravo.Models.AnalyzeModel; - - public interface IAnalyzeModelService - { - TabularDatabase GetDatabase(Stream stream, Stream? dictionaryStream = null); + TabularDatabase GetDatabase(Stream stream, Stream? dictionaryStream = null); + + TabularDatabase GetDatabase(PBIDesktopReport report, CancellationToken cancellationToken); + + TabularDatabase GetDatabase(PBICloudDataset dataset, string accessToken, CancellationToken cancellationToken); - TabularDatabase GetDatabase(PBIDesktopReport report, CancellationToken cancellationToken); + Task> GetDatasetsAsync(AuthenticatedSession session, CancellationToken cancellationToken); - TabularDatabase GetDatabase(PBICloudDataset dataset, string accessToken, CancellationToken cancellationToken); + IEnumerable GetReports(CancellationToken cancellationToken); - Task> GetDatasetsAsync(AuthenticatedSession session, CancellationToken cancellationToken); + void ExportVpax(PBIDesktopReport report, ExportVpaxMode mode, string path, CancellationToken cancellationToken); - IEnumerable GetReports(CancellationToken cancellationToken); + void ExportVpax(PBICloudDataset dataset, string accessToken, ExportVpaxMode mode, string path, CancellationToken cancellationToken); +} - void ExportVpax(PBIDesktopReport report, ExportVpaxMode mode, string path, CancellationToken cancellationToken); +internal sealed class AnalyzeModelService : IAnalyzeModelService +{ + private readonly ICloudApiClient _cloudApiClient; + private readonly IPBIDesktopService _pbidesktopService; - void ExportVpax(PBICloudDataset dataset, string accessToken, ExportVpaxMode mode, string path, CancellationToken cancellationToken); + public AnalyzeModelService(ICloudApiClient cloudApiClient, IPBIDesktopService pbidesktopService) + { + _cloudApiClient = cloudApiClient; + _pbidesktopService = pbidesktopService; } - internal sealed class AnalyzeModelService : IAnalyzeModelService + public TabularDatabase GetDatabase(Stream vpaxStream, Stream? obfuscatorDictionaryStream = null) { - private readonly ICloudApiClient _cloudApiClient; - private readonly IPBIDesktopService _pbidesktopService; + return TabularDatabase.CreateFrom(vpaxStream, obfuscatorDictionaryStream); + } - public AnalyzeModelService(ICloudApiClient cloudApiClient, IPBIDesktopService pbidesktopService) - { - _cloudApiClient = cloudApiClient; - _pbidesktopService = pbidesktopService; - } + public TabularDatabase GetDatabase(PBIDesktopReport report, CancellationToken cancellationToken) + { + using var connection = TabularConnectionWrapper.ConnectTo(report); + var database = TabularDatabase.CreateFrom(connection, cancellationToken); - public TabularDatabase GetDatabase(Stream vpaxStream, Stream? obfuscatorDictionaryStream = null) + if (connection.Server.IsPowerBIDesktop() == false) { - return TabularDatabase.CreateFrom(vpaxStream, obfuscatorDictionaryStream); + database.Features &= ~TabularDatabaseFeature.ManageDatesAll; + database.FeatureUnsupportedReasons |= TabularDatabaseFeatureUnsupportedReason.ManageDatesPBIDesktopModelOnly; } - public TabularDatabase GetDatabase(PBIDesktopReport report, CancellationToken cancellationToken) + return database; + } + + public TabularDatabase GetDatabase(PBICloudDataset dataset, string accessToken, CancellationToken cancellationToken) + { + BravoUnexpectedException.ThrowIfNull(dataset.DisplayName); + TabularDatabase database; + + if (dataset.IsXmlaEndPointSupported || dataset.IsOnPremModel == true) { - using var connection = TabularConnectionWrapper.ConnectTo(report); - var database = TabularDatabase.CreateFrom(connection, cancellationToken); - - if (connection.Server.IsPowerBIDesktop() == false) - { - database.Features &= ~TabularDatabaseFeature.ManageDatesAll; - database.FeatureUnsupportedReasons |= TabularDatabaseFeatureUnsupportedReason.ManageDatesPBIDesktopModelOnly; - } - - return database; + using var connection = TabularConnectionWrapper.ConnectTo(dataset, accessToken); + database = TabularDatabase.CreateFrom(connection, cancellationToken); } - - public TabularDatabase GetDatabase(PBICloudDataset dataset, string accessToken, CancellationToken cancellationToken) + else { - BravoUnexpectedException.ThrowIfNull(dataset.DisplayName); - TabularDatabase database; - - if (dataset.IsXmlaEndPointSupported || dataset.IsOnPremModel == true) - { - using var connection = TabularConnectionWrapper.ConnectTo(dataset, accessToken); - database = TabularDatabase.CreateFrom(connection, cancellationToken); - } - else - { - using var connection = AdomdConnectionWrapper.ConnectTo(dataset, accessToken); - database = TabularDatabase.CreateFromDmvSchema(connection); - - database.Features &= ~TabularDatabaseFeature.AnalyzeModelAll; - database.Features &= ~TabularDatabaseFeature.FormatDaxAll; - database.FeatureUnsupportedReasons |= TabularDatabaseFeatureUnsupportedReason.XmlaEndpointNotSupported; - } + using var connection = AdomdConnectionWrapper.ConnectTo(dataset, accessToken); + database = TabularDatabase.CreateFromDmvSchema(connection); - database.Features &= ~TabularDatabaseFeature.ManageDatesAll; - database.FeatureUnsupportedReasons |= TabularDatabaseFeatureUnsupportedReason.ManageDatesPBIDesktopModelOnly; - - return database; + database.Features &= ~TabularDatabaseFeature.AnalyzeModelAll; + database.Features &= ~TabularDatabaseFeature.FormatDaxAll; + database.FeatureUnsupportedReasons |= TabularDatabaseFeatureUnsupportedReason.XmlaEndpointNotSupported; } - public async Task> GetDatasetsAsync(AuthenticatedSession session, CancellationToken cancellationToken) - { - var datasets = await _cloudApiClient.GetDatasetsAsync(session, cancellationToken); - return datasets; - } + database.Features &= ~TabularDatabaseFeature.ManageDatesAll; + database.FeatureUnsupportedReasons |= TabularDatabaseFeatureUnsupportedReason.ManageDatesPBIDesktopModelOnly; - public IEnumerable GetReports(CancellationToken cancellationToken) - { - var reports = _pbidesktopService.GetReports(cancellationToken); - return reports; - } + return database; + } - public void ExportVpax(PBIDesktopReport report, ExportVpaxMode mode, string path, CancellationToken cancellationToken) - { - using var connection = TabularConnectionWrapper.ConnectTo(report); - ExportVpaxImpl(connection, mode, path, cancellationToken); - } + public async Task> GetDatasetsAsync(AuthenticatedSession session, CancellationToken cancellationToken) + { + var datasets = await _cloudApiClient.GetDatasetsAsync(session, cancellationToken); + return datasets; + } - public void ExportVpax(PBICloudDataset dataset, string accessToken, ExportVpaxMode mode, string path, CancellationToken cancellationToken) - { - using var connection = TabularConnectionWrapper.ConnectTo(dataset, accessToken); - ExportVpaxImpl(connection, mode, path, cancellationToken); - } + public IEnumerable GetReports(CancellationToken cancellationToken) + { + var reports = _pbidesktopService.GetReports(cancellationToken); + return reports; + } - public static void ExportVpaxImpl(TabularConnectionWrapper connection, ExportVpaxMode mode, string path, CancellationToken cancellationToken) - { - using var stream = new MemoryStream(); - VpaxHelper.ExportVpax(stream, connection, cancellationToken); + public void ExportVpax(PBIDesktopReport report, ExportVpaxMode mode, string path, CancellationToken cancellationToken) + { + using var connection = TabularConnectionWrapper.ConnectTo(report); + ExportVpaxImpl(connection, mode, path, cancellationToken); + } + + public void ExportVpax(PBICloudDataset dataset, string accessToken, ExportVpaxMode mode, string path, CancellationToken cancellationToken) + { + using var connection = TabularConnectionWrapper.ConnectTo(dataset, accessToken); + ExportVpaxImpl(connection, mode, path, cancellationToken); + } - if (mode == ExportVpaxMode.Obfuscated) - { - VpaxObfuscatorHelper.ObfuscateAndExportDictionary(stream, path: $"{path}.dict"); - } + public static void ExportVpaxImpl(TabularConnectionWrapper connection, ExportVpaxMode mode, string path, CancellationToken cancellationToken) + { + using var stream = new MemoryStream(); + VpaxHelper.ExportVpax(stream, connection, cancellationToken); - using var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write); - stream.CopyTo(fileStream); + if (mode == ExportVpaxMode.Obfuscated) + { + VpaxObfuscatorHelper.ObfuscateAndExportDictionary(stream, path: $"{path}.dict"); } + + using var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write); + stream.CopyTo(fileStream); } -} \ No newline at end of file +} diff --git a/src/Services/AuthenticationService.cs b/src/Services/AuthenticationService.cs index a22d7a50..a1f64024 100644 --- a/src/Services/AuthenticationService.cs +++ b/src/Services/AuthenticationService.cs @@ -1,45 +1,47 @@ -namespace Sqlbi.Bravo.Services +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; +using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Authentication; +using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Configuration; + +namespace Sqlbi.Bravo.Services; + +public interface IAuthenticationService { - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud; - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Authentication; - using Sqlbi.Bravo.Infrastructure.PowerBI.Cloud.Configuration; + Task> GetEnvironmentsAsync(string email, CancellationToken cancellationToken); - public interface IAuthenticationService - { - Task> GetEnvironmentsAsync(string email, CancellationToken cancellationToken); + Task EnsureSignedInAsync(CancellationToken cancellationToken); - Task EnsureSignedInAsync(CancellationToken cancellationToken); + Task SignInAsync(string email, CloudEnvironment environment, CancellationToken cancellationToken); - Task SignInAsync(string email, CloudEnvironment environment, CancellationToken cancellationToken); + Task SignOutAsync(CancellationToken cancellationToken); +} - Task SignOutAsync(CancellationToken cancellationToken); +internal class AuthenticationService( + ICloudAuthenticationService cloudAuthenticationService, + ICloudConfigurationService cloudConfigurationService) : IAuthenticationService +{ + private readonly ICloudAuthenticationService _cloudAuthenticationService = cloudAuthenticationService; + private readonly ICloudConfigurationService _cloudConfigurationService = cloudConfigurationService; + + public async Task> GetEnvironmentsAsync(string email, CancellationToken cancellationToken) + { + return await _cloudConfigurationService.DiscoverEnvironmentsAsync(email, cancellationToken); + } + + public async Task EnsureSignedInAsync(CancellationToken cancellationToken) + { + return await _cloudAuthenticationService.EnsureSignedInAsync(cancellationToken); + } + + public async Task SignInAsync(string email, CloudEnvironment environment, CancellationToken cancellationToken) + { + return await _cloudAuthenticationService.SignInAsync(email, environment, cancellationToken); } - internal class AuthenticationService( - ICloudAuthenticationService cloudAuthenticationService, - ICloudConfigurationService cloudConfigurationService) : IAuthenticationService + public async Task SignOutAsync(CancellationToken cancellationToken) { - private readonly ICloudAuthenticationService _cloudAuthenticationService = cloudAuthenticationService; - private readonly ICloudConfigurationService _cloudConfigurationService = cloudConfigurationService; - - public async Task> GetEnvironmentsAsync(string email, CancellationToken cancellationToken) - { - return await _cloudConfigurationService.DiscoverEnvironmentsAsync(email, cancellationToken); - } - - public async Task EnsureSignedInAsync(CancellationToken cancellationToken) - { - return await _cloudAuthenticationService.EnsureSignedInAsync(cancellationToken); - } - - public async Task SignInAsync(string email, CloudEnvironment environment, CancellationToken cancellationToken) - { - return await _cloudAuthenticationService.SignInAsync(email, environment, cancellationToken); - } - - public async Task SignOutAsync(CancellationToken cancellationToken) - { - await _cloudAuthenticationService.SignOutAsync(cancellationToken); - } + await _cloudAuthenticationService.SignOutAsync(cancellationToken); } } diff --git a/src/Services/BestPracticeAnalyzerService.cs b/src/Services/BestPracticeAnalyzerService.cs index c4bfdbe7..a7848845 100644 --- a/src/Services/BestPracticeAnalyzerService.cs +++ b/src/Services/BestPracticeAnalyzerService.cs @@ -1,14 +1,11 @@ -namespace Sqlbi.Bravo.Services -{ - using System; +namespace Sqlbi.Bravo.Services; - public interface IBestPracticeAnalyzerService - { +public interface IBestPracticeAnalyzerService +{ - } +} - internal class BestPracticeAnalyzerService : IBestPracticeAnalyzerService - { +internal class BestPracticeAnalyzerService : IBestPracticeAnalyzerService +{ - } -} \ No newline at end of file +} diff --git a/src/Services/ExportDataService.cs b/src/Services/ExportDataService.cs index e937167c..ab349441 100644 --- a/src/Services/ExportDataService.cs +++ b/src/Services/ExportDataService.cs @@ -1,587 +1,586 @@ -namespace Sqlbi.Bravo.Services +using System; +using System.Data; +using System.Drawing; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using CsvHelper; +using CsvHelper.Configuration; +using CsvHelper.TypeConversion; +using LargeXlsx; +using Microsoft.AnalysisServices.AdomdClient; +using Sqlbi.Bravo.Infrastructure; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Services; +using Sqlbi.Bravo.Infrastructure.Services.ExportData; +using Sqlbi.Bravo.Models; +using Sqlbi.Bravo.Models.ExportData; + +namespace Sqlbi.Bravo.Services; + +public interface IExportDataService { - using CsvHelper; - using CsvHelper.Configuration; - using CsvHelper.TypeConversion; - using LargeXlsx; - using Microsoft.AnalysisServices.AdomdClient; - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Services; - using Sqlbi.Bravo.Infrastructure.Services.ExportData; - using Sqlbi.Bravo.Models; - using Sqlbi.Bravo.Models.ExportData; - using System; - using System.Data; - using System.Drawing; - using System.Globalization; - using System.IO; - using System.Linq; - using System.Text; - using System.Threading; - - public interface IExportDataService - { - ExportDataJob ExportDelimitedTextFile(PBIDesktopReport report, ExportDelimitedTextSettings settings, string path, CancellationToken cancellationToken); + ExportDataJob ExportDelimitedTextFile(PBIDesktopReport report, ExportDelimitedTextSettings settings, string path, CancellationToken cancellationToken); - ExportDataJob ExportDelimitedTextFile(PBICloudDataset dataset, ExportDelimitedTextSettings settings, string path, string accessToken, CancellationToken cancellationToken); + ExportDataJob ExportDelimitedTextFile(PBICloudDataset dataset, ExportDelimitedTextSettings settings, string path, string accessToken, CancellationToken cancellationToken); - ExportDataJob ExportExcelFile(PBIDesktopReport report, ExportExcelSettings settings, string path, CancellationToken cancellationToken); + ExportDataJob ExportExcelFile(PBIDesktopReport report, ExportExcelSettings settings, string path, CancellationToken cancellationToken); - ExportDataJob ExportExcelFile(PBICloudDataset dataset, ExportExcelSettings settings, string path, string accessToken, CancellationToken cancellationToken); + ExportDataJob ExportExcelFile(PBICloudDataset dataset, ExportExcelSettings settings, string path, string accessToken, CancellationToken cancellationToken); - ExportDataJob? QueryExportJob(PBIDesktopReport report); + ExportDataJob? QueryExportJob(PBIDesktopReport report); - ExportDataJob? QueryExportJob(PBICloudDataset dataset); - } + ExportDataJob? QueryExportJob(PBICloudDataset dataset); +} - internal class ExportDataService : IExportDataService - { - private const int BatchSize = 10_000; - private const int ExcelMaxRows = 1_000_000; // Excel cannot exceed the limit of 1,048,576 rows and 16,384 columns +internal class ExportDataService : IExportDataService +{ + private const int BatchSize = 10_000; + private const int ExcelMaxRows = 1_000_000; // Excel cannot exceed the limit of 1,048,576 rows and 16,384 columns - private static readonly TypeConverterOptions _defaultDelimitedTextTypeConverterOptions = new() + private static readonly TypeConverterOptions _defaultDelimitedTextTypeConverterOptions = new() + { + Formats = new[] { - Formats = new[] - { - "yyyy-MM-dd HH:mm:ss.fff" // We force the '.' as the preferred separator between the time element and its fraction - see https://github.com/sql-bi/Bravo/issues/549 - } - }; - - private readonly ExportDataJobMap _datasetJobs = new(); - private readonly ExportDataJobMap _reportJobs = new(); + "yyyy-MM-dd HH:mm:ss.fff" // We force the '.' as the preferred separator between the time element and its fraction - see https://github.com/sql-bi/Bravo/issues/549 + } + }; - public ExportDataJob ExportDelimitedTextFile(PBIDesktopReport report, ExportDelimitedTextSettings settings, string path, CancellationToken cancellationToken) - { - settings.ExportPath = path; + private readonly ExportDataJobMap _datasetJobs = new(); + private readonly ExportDataJobMap _reportJobs = new(); - var job = _reportJobs.AddNew(report, settings); - try - { - using var connection = AdomdConnectionWrapper.ConnectTo(report); + public ExportDataJob ExportDelimitedTextFile(PBIDesktopReport report, ExportDelimitedTextSettings settings, string path, CancellationToken cancellationToken) + { + settings.ExportPath = path; - ExportDelimitedTextFileImpl(job, settings, connection, cancellationToken); - job.SetCompleted(); - } - catch (OperationCanceledException) - { - job.SetCanceled(); - } - catch (Exception ex) - { - job.SetFailed(); - throw new BravoException(BravoProblem.ExportDataFileError, ex.Message, ex); - } - finally - { - _reportJobs.Remove(report); - } + var job = _reportJobs.AddNew(report, settings); + try + { + using var connection = AdomdConnectionWrapper.ConnectTo(report); - return job; + ExportDelimitedTextFileImpl(job, settings, connection, cancellationToken); + job.SetCompleted(); } - - public ExportDataJob ExportDelimitedTextFile(PBICloudDataset dataset, ExportDelimitedTextSettings settings, string path, string accessToken, CancellationToken cancellationToken) + catch (OperationCanceledException) { - settings.ExportPath = path; - - var job = _datasetJobs.AddNew(dataset, settings); - try - { - using var connection = AdomdConnectionWrapper.ConnectTo(dataset, accessToken); + job.SetCanceled(); + } + catch (Exception ex) + { + job.SetFailed(); + throw new BravoException(BravoProblem.ExportDataFileError, ex.Message, ex); + } + finally + { + _reportJobs.Remove(report); + } - ExportDelimitedTextFileImpl(job, settings, connection, cancellationToken); - job.SetCompleted(); - } - catch (OperationCanceledException) - { - job.SetCanceled(); - } - catch (Exception ex) - { - job.SetFailed(); - throw new BravoException(BravoProblem.ExportDataFileError, ex.Message, ex); - } - finally - { - _datasetJobs.Remove(dataset); - } + return job; + } - return job; - } + public ExportDataJob ExportDelimitedTextFile(PBICloudDataset dataset, ExportDelimitedTextSettings settings, string path, string accessToken, CancellationToken cancellationToken) + { + settings.ExportPath = path; - public ExportDataJob ExportExcelFile(PBIDesktopReport report, ExportExcelSettings settings, string path, CancellationToken cancellationToken) + var job = _datasetJobs.AddNew(dataset, settings); + try { - settings.ExportPath = path; + using var connection = AdomdConnectionWrapper.ConnectTo(dataset, accessToken); - var job = _reportJobs.AddNew(report, settings); - try - { - using var connection = AdomdConnectionWrapper.ConnectTo(report); + ExportDelimitedTextFileImpl(job, settings, connection, cancellationToken); + job.SetCompleted(); + } + catch (OperationCanceledException) + { + job.SetCanceled(); + } + catch (Exception ex) + { + job.SetFailed(); + throw new BravoException(BravoProblem.ExportDataFileError, ex.Message, ex); + } + finally + { + _datasetJobs.Remove(dataset); + } - ExportExcelFileImpl(job, settings, connection, cancellationToken); - job.SetCompleted(); - } - catch (OperationCanceledException) - { - job.SetCanceled(); - } - catch (Exception ex) - { - job.SetFailed(); - throw new BravoException(BravoProblem.ExportDataFileError, ex.Message, ex); - } - finally - { - _reportJobs.Remove(report); - } + return job; + } - return job; - } + public ExportDataJob ExportExcelFile(PBIDesktopReport report, ExportExcelSettings settings, string path, CancellationToken cancellationToken) + { + settings.ExportPath = path; - public ExportDataJob ExportExcelFile(PBICloudDataset dataset, ExportExcelSettings settings, string path, string accessToken, CancellationToken cancellationToken) + var job = _reportJobs.AddNew(report, settings); + try { - settings.ExportPath = path; + using var connection = AdomdConnectionWrapper.ConnectTo(report); - var job = _datasetJobs.AddNew(dataset, settings); - try - { - using var connection = AdomdConnectionWrapper.ConnectTo(dataset, accessToken); + ExportExcelFileImpl(job, settings, connection, cancellationToken); + job.SetCompleted(); + } + catch (OperationCanceledException) + { + job.SetCanceled(); + } + catch (Exception ex) + { + job.SetFailed(); + throw new BravoException(BravoProblem.ExportDataFileError, ex.Message, ex); + } + finally + { + _reportJobs.Remove(report); + } - ExportExcelFileImpl(job, settings, connection, cancellationToken); - job.SetCompleted(); - } - catch (OperationCanceledException) - { - job.SetCanceled(); - } - catch (Exception ex) - { - job.SetFailed(); - throw new BravoException(BravoProblem.ExportDataFileError, ex.Message, ex); - } - finally - { - _datasetJobs.Remove(dataset); - } + return job; + } - return job; - } + public ExportDataJob ExportExcelFile(PBICloudDataset dataset, ExportExcelSettings settings, string path, string accessToken, CancellationToken cancellationToken) + { + settings.ExportPath = path; - public ExportDataJob? QueryExportJob(PBIDesktopReport report) + var job = _datasetJobs.AddNew(dataset, settings); + try { - _reportJobs.TryGet(report, out var job); + using var connection = AdomdConnectionWrapper.ConnectTo(dataset, accessToken); - return job; + ExportExcelFileImpl(job, settings, connection, cancellationToken); + job.SetCompleted(); } - - public ExportDataJob? QueryExportJob(PBICloudDataset dataset) + catch (OperationCanceledException) { - _datasetJobs.TryGet(dataset, out var job); - - return job; + job.SetCanceled(); } - - private static void ExportDelimitedTextFileImpl(ExportDataJob job, ExportDelimitedTextSettings settings, AdomdConnectionWrapper connection, CancellationToken cancellationToken) + catch (Exception ex) { - Directory.CreateDirectory(settings.ExportPath); + job.SetFailed(); + throw new BravoException(BravoProblem.ExportDataFileError, ex.Message, ex); + } + finally + { + _datasetJobs.Remove(dataset); + } - var config = new CsvConfiguration(CultureInfo.CurrentCulture); - { - config.Delimiter = settings.Delimiter.NullIfEmpty() ?? CultureInfo.CurrentCulture.TextInfo.ListSeparator; - config.Validate(); - } + return job; + } - using var command = connection.CreateAdomdCommand(); - using var _ = cancellationToken.Register(() => command.Cancel()); + public ExportDataJob? QueryExportJob(PBIDesktopReport report) + { + _reportJobs.TryGet(report, out var job); - foreach (var tableName in settings.Tables) - { - cancellationToken.ThrowIfCancellationRequested(); + return job; + } + + public ExportDataJob? QueryExportJob(PBICloudDataset dataset) + { + _datasetJobs.TryGet(dataset, out var job); - var table = job.AddNew(tableName.Name); - var fileTableName = tableName.Name.ReplaceInvalidFileNameChars(); - var fileName = Path.ChangeExtension(fileTableName, "csv"); - var path = Path.Combine(settings.ExportPath, fileName); - // Enable row batch mode only if TOPNSKIP is supported - var rowBatchMode = connection.IsServerVersion13OrGreater && tableName.IsDirectQuery == false; + return job; + } - Encoding encoding = settings.UnicodeEncoding - ? new UnicodeEncoding() - : new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + private static void ExportDelimitedTextFileImpl(ExportDataJob job, ExportDelimitedTextSettings settings, AdomdConnectionWrapper connection, CancellationToken cancellationToken) + { + Directory.CreateDirectory(settings.ExportPath); - using var streamWriter = new StreamWriter(path, append: false, encoding); - using var csvWriter = new CsvWriter(streamWriter, config); + var config = new CsvConfiguration(CultureInfo.CurrentCulture); + { + config.Delimiter = settings.Delimiter.NullIfEmpty() ?? CultureInfo.CurrentCulture.TextInfo.ListSeparator; + config.Validate(); + } - Export(command, table, csvWriter, settings.QuoteStringFields, rowBatchMode, cancellationToken); - } + using var command = connection.CreateAdomdCommand(); + using var _ = cancellationToken.Register(() => command.Cancel()); - static void Export(AdomdCommand command, ExportDataTable table, CsvWriter writer, bool quoteStringFields, bool rowBatchMode, CancellationToken cancellationToken) - { - var tableName = TabularModelHelper.GetDaxTableName(table.Name); + foreach (var tableName in settings.Tables) + { + cancellationToken.ThrowIfCancellationRequested(); - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Text, name: $"{nameof(ExportDataService)}.{nameof(ExportDelimitedTextFileImpl)}.{nameof(Export)}", content: $"Export table {tableName} in {(rowBatchMode ? "row batch" : "full")} mode"); + var table = job.AddNew(tableName.Name); + var fileTableName = tableName.Name.ReplaceInvalidFileNameChars(); + var fileName = Path.ChangeExtension(fileTableName, "csv"); + var path = Path.Combine(settings.ExportPath, fileName); + // Enable row batch mode only if TOPNSKIP is supported + var rowBatchMode = connection.IsServerVersion13OrGreater && tableName.IsDirectQuery == false; - if (rowBatchMode) - { - var batchCount = 0; - do - { - // Sort order based on RowNumber column. Order changes when the table is refreshed. - command.CommandText = $"EVALUATE TOPNSKIP({BatchSize}, {batchCount++ * BatchSize}, {tableName})"; - using var dataReader = command.ExecuteReader(CommandBehavior.SingleResult); + Encoding encoding = settings.UnicodeEncoding + ? new UnicodeEncoding() + : new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - if (batchCount == 1) WriteHeader(table, writer, dataReader, quoteStringFields); - var batchRows = WriteData(table, writer, dataReader, quoteStringFields, cancellationToken); - if (batchRows == 0) - break; - } - while (true); - } - else + using var streamWriter = new StreamWriter(path, append: false, encoding); + using var csvWriter = new CsvWriter(streamWriter, config); + + Export(command, table, csvWriter, settings.QuoteStringFields, rowBatchMode, cancellationToken); + } + + static void Export(AdomdCommand command, ExportDataTable table, CsvWriter writer, bool quoteStringFields, bool rowBatchMode, CancellationToken cancellationToken) + { + var tableName = TabularModelHelper.GetDaxTableName(table.Name); + + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Text, name: $"{nameof(ExportDataService)}.{nameof(ExportDelimitedTextFileImpl)}.{nameof(Export)}", content: $"Export table {tableName} in {(rowBatchMode ? "row batch" : "full")} mode"); + + if (rowBatchMode) + { + var batchCount = 0; + do { - command.CommandText = $"EVALUATE {tableName}"; + // Sort order based on RowNumber column. Order changes when the table is refreshed. + command.CommandText = $"EVALUATE TOPNSKIP({BatchSize}, {batchCount++ * BatchSize}, {tableName})"; using var dataReader = command.ExecuteReader(CommandBehavior.SingleResult); - WriteHeader(table, writer, dataReader, quoteStringFields); - WriteData(table, writer, dataReader, quoteStringFields, cancellationToken); + if (batchCount == 1) WriteHeader(table, writer, dataReader, quoteStringFields); + var batchRows = WriteData(table, writer, dataReader, quoteStringFields, cancellationToken); + if (batchRows == 0) + break; } + while (true); + } + else + { + command.CommandText = $"EVALUATE {tableName}"; + using var dataReader = command.ExecuteReader(CommandBehavior.SingleResult); - table.SetCompleted(); + WriteHeader(table, writer, dataReader, quoteStringFields); + WriteData(table, writer, dataReader, quoteStringFields, cancellationToken); } - static void WriteHeader(ExportDataTable table, CsvWriter writer, IDataReader reader, bool quoteStringFields) - { - // output dates using ISO 8601 format - writer.Context.TypeConverterOptionsCache.AddOptions(typeof(DateTime), options: _defaultDelimitedTextTypeConverterOptions); - table.Columns = reader.FieldCount; + table.SetCompleted(); + } - for (var i = 0; i < reader.FieldCount; i++) - { - var columnName = GetDaxColumnName(reader, i); + static void WriteHeader(ExportDataTable table, CsvWriter writer, IDataReader reader, bool quoteStringFields) + { + // output dates using ISO 8601 format + writer.Context.TypeConverterOptionsCache.AddOptions(typeof(DateTime), options: _defaultDelimitedTextTypeConverterOptions); + table.Columns = reader.FieldCount; - if (quoteStringFields) - writer.WriteField(columnName, shouldQuote: true); - else - writer.WriteField(columnName); // use default ConfigurationFunctions.ShouldQuote() - } + for (var i = 0; i < reader.FieldCount; i++) + { + var columnName = GetDaxColumnName(reader, i); - writer.NextRecord(); + if (quoteStringFields) + writer.WriteField(columnName, shouldQuote: true); + else + writer.WriteField(columnName); // use default ConfigurationFunctions.ShouldQuote() } - static int WriteData(ExportDataTable table, CsvWriter writer, IDataReader reader, bool quoteStringFields, CancellationToken cancellationToken) + writer.NextRecord(); + } + + static int WriteData(ExportDataTable table, CsvWriter writer, IDataReader reader, bool quoteStringFields, CancellationToken cancellationToken) + { + var rows = 0; + while (reader.Read()) { - var rows = 0; - while (reader.Read()) + for (var i = 0; i < reader.FieldCount; i++) { - for (var i = 0; i < reader.FieldCount; i++) - { - var field = reader[i]; + var field = reader[i]; - if (reader.GetFieldType(i) == typeof(string)) - { - var stringField = reader.IsDBNull(i) ? string.Empty : field.ToString(); + if (reader.GetFieldType(i) == typeof(string)) + { + var stringField = reader.IsDBNull(i) ? string.Empty : field.ToString(); - if (quoteStringFields) - writer.WriteField(stringField, shouldQuote: true); - else - writer.WriteField(field); // use default ConfigurationFunctions.ShouldQuote() - } + if (quoteStringFields) + writer.WriteField(stringField, shouldQuote: true); else - { - writer.WriteField(field); - } + writer.WriteField(field); // use default ConfigurationFunctions.ShouldQuote() } + else + { + writer.WriteField(field); + } + } - rows++; - table.Rows++; - writer.NextRecord(); + rows++; + table.Rows++; + writer.NextRecord(); - if (table.Rows % 1_000 == 0) - cancellationToken.ThrowIfCancellationRequested(); - } - return rows; + if (table.Rows % 1_000 == 0) + cancellationToken.ThrowIfCancellationRequested(); } + return rows; } + } - private static void ExportExcelFileImpl(ExportDataJob job, ExportExcelSettings settings, AdomdConnectionWrapper connection, CancellationToken cancellationToken) - { - var xlsxFile = new FileInfo(settings.ExportPath); + private static void ExportExcelFileImpl(ExportDataJob job, ExportExcelSettings settings, AdomdConnectionWrapper connection, CancellationToken cancellationToken) + { + var xlsxFile = new FileInfo(settings.ExportPath); - BravoUnexpectedException.ThrowIfNull(xlsxFile.Directory); - Directory.CreateDirectory(xlsxFile.Directory.FullName); + BravoUnexpectedException.ThrowIfNull(xlsxFile.Directory); + Directory.CreateDirectory(xlsxFile.Directory.FullName); - using var command = connection.CreateAdomdCommand(); - using var _ = cancellationToken.Register(() => command.Cancel()); + using var command = connection.CreateAdomdCommand(); + using var _ = cancellationToken.Register(() => command.Cancel()); - using var fileStream = new FileStream(xlsxFile.FullName, FileMode.Create, FileAccess.Write); - using var xlsxWriter = new XlsxWriter(fileStream, compressionLevel: XlsxCompressionLevel.Fastest); + using var fileStream = new FileStream(xlsxFile.FullName, FileMode.Create, FileAccess.Write); + using var xlsxWriter = new XlsxWriter(fileStream, compressionLevel: XlsxCompressionLevel.Fastest); - foreach (var (tableName, tableIndex) in settings.Tables.WithIndex()) - { - cancellationToken.ThrowIfCancellationRequested(); + foreach (var (tableName, tableIndex) in settings.Tables.WithIndex()) + { + cancellationToken.ThrowIfCancellationRequested(); - var table = job.AddNew(tableName.Name); - var worksheetName = GetWorksheetName(tableName.Name, tableIndex); - // Enable row batch mode only if TOPNSKIP is supported - var rowBatchMode = connection.IsServerVersion13OrGreater && tableName.IsDirectQuery == false; - - xlsxWriter.BeginWorksheet(worksheetName, splitRow: 1); - Export(command, table, xlsxWriter, rowBatchMode, cancellationToken); - - if (table.Rows > 0 && table.Columns > 0) - xlsxWriter.SetAutoFilter(fromRow: 1, fromColumn: 1, rowCount: table.Rows, columnCount: table.Columns); - } + var table = job.AddNew(tableName.Name); + var worksheetName = GetWorksheetName(tableName.Name, tableIndex); + // Enable row batch mode only if TOPNSKIP is supported + var rowBatchMode = connection.IsServerVersion13OrGreater && tableName.IsDirectQuery == false; - WriteSummary(job, settings, xlsxWriter); + xlsxWriter.BeginWorksheet(worksheetName, splitRow: 1); + Export(command, table, xlsxWriter, rowBatchMode, cancellationToken); - static string GetWorksheetName(string tableName, int tableIndex) - { - const char ApostropheChar = '\''; - const int WorksheetNameMaxLength = 31; - /* const */ var WorksheetNameForbiddenChars = new[] { '\\', '/', '?', '*', '[', ']', ':' }; + if (table.Rows > 0 && table.Columns > 0) + xlsxWriter.SetAutoFilter(fromRow: 1, fromColumn: 1, rowCount: table.Rows, columnCount: table.Columns); + } - var worksheetName = tableName; - var appendSuffix = false; - var suffix = $"#{ tableIndex }"; + WriteSummary(job, settings, xlsxWriter); - // Worksheet name cannot be left blank - if (worksheetName == string.Empty) - { - worksheetName = "Sheet"; - appendSuffix = true; - } + static string GetWorksheetName(string tableName, int tableIndex) + { + const char ApostropheChar = '\''; + const int WorksheetNameMaxLength = 31; + /* const */ var WorksheetNameForbiddenChars = new[] { '\\', '/', '?', '*', '[', ']', ':' }; - // Worksheet cannot be named 'history' because it's a reserved name - if (worksheetName.EqualsI("history")) - appendSuffix = true; + var worksheetName = tableName; + var appendSuffix = false; + var suffix = $"#{tableIndex}"; - // Apostrophe cannot be used at the beginning or end of the worksheet name - if (worksheetName.StartsWith(ApostropheChar) || worksheetName.EndsWith(ApostropheChar)) - { - worksheetName = worksheetName.TrimStart(ApostropheChar).TrimEnd(ApostropheChar); - appendSuffix = true; - } + // Worksheet name cannot be left blank + if (worksheetName == string.Empty) + { + worksheetName = "Sheet"; + appendSuffix = true; + } - // Worksheet name does not contain forbidden characters - if (worksheetName.IndexOfAny(WorksheetNameForbiddenChars) != -1) - { - foreach (var forbiddenChar in WorksheetNameForbiddenChars) - worksheetName = worksheetName.Replace(forbiddenChar, '_'); + // Worksheet cannot be named 'history' because it's a reserved name + if (worksheetName.EqualsI("history")) + appendSuffix = true; - appendSuffix = true; - } + // Apostrophe cannot be used at the beginning or end of the worksheet name + if (worksheetName.StartsWith(ApostropheChar) || worksheetName.EndsWith(ApostropheChar)) + { + worksheetName = worksheetName.TrimStart(ApostropheChar).TrimEnd(ApostropheChar); + appendSuffix = true; + } - // Worksheet name cannot exceed 31 characters - if (worksheetName.Length > WorksheetNameMaxLength) - { - worksheetName = worksheetName[..(WorksheetNameMaxLength - suffix.Length)]; - appendSuffix = true; - } + // Worksheet name does not contain forbidden characters + if (worksheetName.IndexOfAny(WorksheetNameForbiddenChars) != -1) + { + foreach (var forbiddenChar in WorksheetNameForbiddenChars) + worksheetName = worksheetName.Replace(forbiddenChar, '_'); - if (appendSuffix) - { - worksheetName += suffix; - } + appendSuffix = true; + } - return worksheetName; + // Worksheet name cannot exceed 31 characters + if (worksheetName.Length > WorksheetNameMaxLength) + { + worksheetName = worksheetName[..(WorksheetNameMaxLength - suffix.Length)]; + appendSuffix = true; } - static void Export(AdomdCommand command, ExportDataTable table, XlsxWriter writer, bool rowBatchMode, CancellationToken cancellationToken) + if (appendSuffix) { - var tableName = TabularModelHelper.GetDaxTableName(table.Name); + worksheetName += suffix; + } - if (AppEnvironment.IsDiagnosticLevelVerbose) - AppEnvironment.AddDiagnostics(DiagnosticMessageType.Text, name: $"{nameof(ExportDataService)}.{nameof(ExportExcelFileImpl)}.{nameof(Export)}", content: $"Export table {tableName} in {(rowBatchMode ? "row batch" : "full")} mode"); + return worksheetName; + } - if (rowBatchMode) - { - var batchCount = 0; - do - { - // Sort order based on RowNumber column. Order changes when the table is refreshed. - command.CommandText = $"EVALUATE TOPNSKIP({BatchSize}, {batchCount++ * BatchSize}, {tableName})"; - using var reader = command.ExecuteReader(CommandBehavior.SingleResult); + static void Export(AdomdCommand command, ExportDataTable table, XlsxWriter writer, bool rowBatchMode, CancellationToken cancellationToken) + { + var tableName = TabularModelHelper.GetDaxTableName(table.Name); - if (batchCount == 1) WriteHeader(table, writer, reader); - var batchRows = WriteData(table, writer, reader, cancellationToken); - if (batchRows == 0 || table.Status == ExportDataStatus.Truncated) - break; - } - while (true); - } - else + if (AppEnvironment.IsDiagnosticLevelVerbose) + AppEnvironment.AddDiagnostics(DiagnosticMessageType.Text, name: $"{nameof(ExportDataService)}.{nameof(ExportExcelFileImpl)}.{nameof(Export)}", content: $"Export table {tableName} in {(rowBatchMode ? "row batch" : "full")} mode"); + + if (rowBatchMode) + { + var batchCount = 0; + do { - command.CommandText = $"EVALUATE {tableName}"; + // Sort order based on RowNumber column. Order changes when the table is refreshed. + command.CommandText = $"EVALUATE TOPNSKIP({BatchSize}, {batchCount++ * BatchSize}, {tableName})"; using var reader = command.ExecuteReader(CommandBehavior.SingleResult); - WriteHeader(table, writer, reader); - WriteData(table, writer, reader, cancellationToken); + if (batchCount == 1) WriteHeader(table, writer, reader); + var batchRows = WriteData(table, writer, reader, cancellationToken); + if (batchRows == 0 || table.Status == ExportDataStatus.Truncated) + break; } + while (true); + } + else + { + command.CommandText = $"EVALUATE {tableName}"; + using var reader = command.ExecuteReader(CommandBehavior.SingleResult); - if (table.Status == ExportDataStatus.Running) - table.SetCompleted(); + WriteHeader(table, writer, reader); + WriteData(table, writer, reader, cancellationToken); } - static void WriteHeader(ExportDataTable table, XlsxWriter writer, IDataReader reader) - { - // TODO: improve the column format(XlsxStyle) by using the TOM.Column.FormatString property (see DaxStudio.UI.Utils.XlsxHelper.GetStyle) + if (table.Status == ExportDataStatus.Running) + table.SetCompleted(); + } - var headerStyle = new XlsxStyle( - font: new XlsxFont(XlsxFont.Default.Name, XlsxFont.Default.Size, Color.White, bold: true), - fill: new XlsxFill(Color.FromArgb(0, 0x45, 0x86)), - border: XlsxStyle.Default.Border, - numberFormat: XlsxStyle.Default.NumberFormat, - alignment: XlsxAlignment.Default); + static void WriteHeader(ExportDataTable table, XlsxWriter writer, IDataReader reader) + { + // TODO: improve the column format(XlsxStyle) by using the TOM.Column.FormatString property (see DaxStudio.UI.Utils.XlsxHelper.GetStyle) - writer.SetDefaultStyle(headerStyle).BeginRow(); - table.Columns = reader.FieldCount; + var headerStyle = new XlsxStyle( + font: new XlsxFont(XlsxFont.Default.Name, XlsxFont.Default.Size, Color.White, bold: true), + fill: new XlsxFill(Color.FromArgb(0, 0x45, 0x86)), + border: XlsxStyle.Default.Border, + numberFormat: XlsxStyle.Default.NumberFormat, + alignment: XlsxAlignment.Default); - for (var i = 0; i < reader.FieldCount; i++) - { - var columnName = GetDaxColumnName(reader, i); - writer.Write(columnName); - } + writer.SetDefaultStyle(headerStyle).BeginRow(); + table.Columns = reader.FieldCount; - // restore default style - writer.SetDefaultStyle(XlsxStyle.Default); + for (var i = 0; i < reader.FieldCount; i++) + { + var columnName = GetDaxColumnName(reader, i); + writer.Write(columnName); } - static int WriteData(ExportDataTable table, XlsxWriter writer, IDataReader reader, CancellationToken cancellationToken) + // restore default style + writer.SetDefaultStyle(XlsxStyle.Default); + } + + static int WriteData(ExportDataTable table, XlsxWriter writer, IDataReader reader, CancellationToken cancellationToken) + { + var dateTimeStyle = XlsxStyle.Default.With(new XlsxNumberFormat($"yyyy-mm-dd hh:mm:ss")); + var rows = 0; + + while (reader.Read()) { - var dateTimeStyle = XlsxStyle.Default.With(new XlsxNumberFormat($"yyyy-mm-dd hh:mm:ss")); - var rows = 0; + writer.BeginRow(); - while (reader.Read()) + for (var i = 0; i < reader.FieldCount; i++) { - writer.BeginRow(); + var value = reader[i]; - for (var i = 0; i < reader.FieldCount; i++) + switch (value) { - var value = reader[i]; - - switch (value) - { - case null: - writer.Write(); - break; - case int @int: - writer.Write(@int); - break; - case double @double when !double.IsNaN(@double) && !double.IsPositiveInfinity(@double) && !double.IsNegativeInfinity(@double): - writer.Write(@double); - break; - case decimal @decimal: - writer.Write(@decimal); - break; - case DateTime dateTime: - writer.Write(dateTime, dateTimeStyle); - break; - case string @string: - writer.Write(@string); - break; - case bool @bool: - writer.Write(@bool.ToString()); - break; - case long @long when @long >= int.MinValue && @long <= int.MaxValue: - writer.Write(Convert.ToInt32(@long)); - break; - default: - writer.Write(value.ToString()); - break; - } - } - - rows++; - - if (++table.Rows >= ExcelMaxRows) - { - table.SetTruncated(); - return rows; + case null: + writer.Write(); + break; + case int @int: + writer.Write(@int); + break; + case double @double when !double.IsNaN(@double) && !double.IsPositiveInfinity(@double) && !double.IsNegativeInfinity(@double): + writer.Write(@double); + break; + case decimal @decimal: + writer.Write(@decimal); + break; + case DateTime dateTime: + writer.Write(dateTime, dateTimeStyle); + break; + case string @string: + writer.Write(@string); + break; + case bool @bool: + writer.Write(@bool.ToString()); + break; + case long @long when @long >= int.MinValue && @long <= int.MaxValue: + writer.Write(Convert.ToInt32(@long)); + break; + default: + writer.Write(value.ToString()); + break; } - - if (table.Rows % 1_000 == 0) - cancellationToken.ThrowIfCancellationRequested(); } - return rows; - } + rows++; - static void WriteSummary(ExportDataJob job, ExportExcelSettings settings, XlsxWriter writer) - { - if (!settings.CreateExportSummary) - return; - - var headerStyle = new XlsxStyle( - font: new XlsxFont(XlsxFont.Default.Name, XlsxFont.Default.Size, Color.White, bold: true), - fill: new XlsxFill(Color.FromArgb(0, 0x45, 0x86)), - border: XlsxStyle.Default.Border, - numberFormat: XlsxStyle.Default.NumberFormat, - alignment: XlsxAlignment.Default); - var infoStyle = XlsxStyle.Default.With(font: new XlsxFont(XlsxFont.Default.Name, XlsxFont.Default.Size, XlsxFont.Default.Color, bold: true)); - var warningStyle = XlsxStyle.Default.With(fill: new XlsxFill(Color.FromArgb(0xff, 0xff, 0x88))).With(border: XlsxBorder.Around(around: new XlsxBorder.Line(Color.DeepPink, XlsxBorder.Style.Dashed))); - - writer.BeginWorksheet("Bravo Export Summary"); - writer.BeginRow().Write($"Exported with { AppEnvironment.ApplicationMainWindowTitle }", style: infoStyle); - writer.BeginRow().Write($"Version { AppEnvironment.VersionInfo.Version } (build { AppEnvironment.VersionInfo.Build })", style: infoStyle); - writer.SkipRows(1); - writer.SetDefaultStyle(headerStyle).BeginRow().Write("Worksheet").Write("Table").Write("Rows").Write("Status"); - writer.SetDefaultStyle(XlsxStyle.Default); - - foreach (var (tableName, tableIndex) in settings.Tables.WithIndex()) + if (++table.Rows >= ExcelMaxRows) { - var table = job.Tables.Single((t) => t.Name.Equals(tableName.Name)); - var statusStyle = table.Status == ExportDataStatus.Truncated ? warningStyle : XlsxStyle.Default; - var worksheetName = GetWorksheetName(tableName.Name, tableIndex); - - writer.BeginRow(); - writer.Write(worksheetName).Write(tableName.Name).Write(table.Rows).Write(table.Status.ToString(), statusStyle); + table.SetTruncated(); + return rows; } - writer.SetAutoFilter(fromRow: 4, fromColumn: 1, rowCount: writer.CurrentRowNumber, columnCount: 4); + if (table.Rows % 1_000 == 0) + cancellationToken.ThrowIfCancellationRequested(); } + + return rows; } - private static string? GetDaxColumnName(IDataReader reader, int fieldIndex) + static void WriteSummary(ExportDataJob job, ExportExcelSettings settings, XlsxWriter writer) { - var fullyQualifiedName = reader.GetName(fieldIndex); - if (fullyQualifiedName is not null) + if (!settings.CreateExportSummary) + return; + + var headerStyle = new XlsxStyle( + font: new XlsxFont(XlsxFont.Default.Name, XlsxFont.Default.Size, Color.White, bold: true), + fill: new XlsxFill(Color.FromArgb(0, 0x45, 0x86)), + border: XlsxStyle.Default.Border, + numberFormat: XlsxStyle.Default.NumberFormat, + alignment: XlsxAlignment.Default); + var infoStyle = XlsxStyle.Default.With(font: new XlsxFont(XlsxFont.Default.Name, XlsxFont.Default.Size, XlsxFont.Default.Color, bold: true)); + var warningStyle = XlsxStyle.Default.With(fill: new XlsxFill(Color.FromArgb(0xff, 0xff, 0x88))).With(border: XlsxBorder.Around(around: new XlsxBorder.Line(Color.DeepPink, XlsxBorder.Style.Dashed))); + + writer.BeginWorksheet("Bravo Export Summary"); + writer.BeginRow().Write($"Exported with {AppEnvironment.ApplicationMainWindowTitle}", style: infoStyle); + writer.BeginRow().Write($"Version {AppEnvironment.VersionInfo.Version} (build {AppEnvironment.VersionInfo.Build})", style: infoStyle); + writer.SkipRows(1); + writer.SetDefaultStyle(headerStyle).BeginRow().Write("Worksheet").Write("Table").Write("Rows").Write("Status"); + writer.SetDefaultStyle(XlsxStyle.Default); + + foreach (var (tableName, tableIndex) in settings.Tables.WithIndex()) { - var columnName = fullyQualifiedName.GetDaxName(); - return columnName; + var table = job.Tables.Single((t) => t.Name.Equals(tableName.Name)); + var statusStyle = table.Status == ExportDataStatus.Truncated ? warningStyle : XlsxStyle.Default; + var worksheetName = GetWorksheetName(tableName.Name, tableIndex); + + writer.BeginRow(); + writer.Write(worksheetName).Write(tableName.Name).Write(table.Rows).Write(table.Status.ToString(), statusStyle); } - return null; + writer.SetAutoFilter(fromRow: 4, fromColumn: 1, rowCount: writer.CurrentRowNumber, columnCount: 4); } + } - /* - private static IDataReader CreateTestData() - { - const int Columns = 100; - const int Rows = 10_000; + private static string? GetDaxColumnName(IDataReader reader, int fieldIndex) + { + var fullyQualifiedName = reader.GetName(fieldIndex); + if (fullyQualifiedName is not null) + { + var columnName = fullyQualifiedName.GetDaxName(); + return columnName; + } - var table = new DataTable(); + return null; + } - for (int c = 0; c < Columns; c++) - { - table.Columns.Add(new DataColumn($"col{ c }", typeof(string))); - } + /* + private static IDataReader CreateTestData() + { + const int Columns = 100; + const int Rows = 10_000; + + var table = new DataTable(); + + for (int c = 0; c < Columns; c++) + { + table.Columns.Add(new DataColumn($"col{ c }", typeof(string))); + } - for (int r = 0; r < Rows; r++) + for (int r = 0; r < Rows; r++) + { + var row = table.NewRow(); { - var row = table.NewRow(); + for (var c = 0; c < Columns; c++) { - for (var c = 0; c < Columns; c++) - { - row[c] = (r == c) ? null : $"Sample text [{ c },{ r }]"; - } + row[c] = (r == c) ? null : $"Sample text [{ c },{ r }]"; } - table.Rows.Add(row); } - - return table.CreateDataReader(); + table.Rows.Add(row); } - */ - } + + return table.CreateDataReader(); + } + */ } diff --git a/src/Services/FormatDaxService.cs b/src/Services/FormatDaxService.cs index dbf81a3f..e8795ce9 100644 --- a/src/Services/FormatDaxService.cs +++ b/src/Services/FormatDaxService.cs @@ -1,150 +1,149 @@ -namespace Sqlbi.Bravo.Services +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Dax.Formatter; +using Dax.Formatter.Models; +using Sqlbi.Bravo.Infrastructure; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Services; +using Sqlbi.Bravo.Models; +using Sqlbi.Bravo.Models.AnalyzeModel; +using Sqlbi.Bravo.Models.FormatDax; + +namespace Sqlbi.Bravo.Services; + +public interface IFormatDaxService { - using Dax.Formatter; - using Dax.Formatter.Models; - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Services; - using Sqlbi.Bravo.Models; - using Sqlbi.Bravo.Models.AnalyzeModel; - using Sqlbi.Bravo.Models.FormatDax; - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - - public interface IFormatDaxService - { - Task> FormatAsync(IEnumerable measures, FormatDaxOptions options); + Task> FormatAsync(IEnumerable measures, FormatDaxOptions options); - DatabaseUpdateResult Update(PBIDesktopReport report, IEnumerable measures); + DatabaseUpdateResult Update(PBIDesktopReport report, IEnumerable measures); - DatabaseUpdateResult Update(PBICloudDataset dataset, IEnumerable measures, string accessToken); - } + DatabaseUpdateResult Update(PBICloudDataset dataset, IEnumerable measures, string accessToken); +} - internal class FormatDaxService : IFormatDaxService +internal class FormatDaxService : IFormatDaxService +{ + private readonly IDaxFormatterClient _daxformatterClient; + + public FormatDaxService(IDaxFormatterClient daxformatterClient) { - private readonly IDaxFormatterClient _daxformatterClient; + _daxformatterClient = daxformatterClient; + } - public FormatDaxService(IDaxFormatterClient daxformatterClient) - { - _daxformatterClient = daxformatterClient; - } + public async Task> FormatAsync(IEnumerable measures, FormatDaxOptions options) + { + BravoUnexpectedException.ThrowIfNull(options.AutoLineBreakStyle); + BravoUnexpectedException.ThrowIfNull(options.LineBreakStyle); - public async Task> FormatAsync(IEnumerable measures, FormatDaxOptions options) + var daxformatterResponses = await CallDaxFormatterAsync(measures, options).ConfigureAwait(false); + var formattedMeasures = new List(); { - BravoUnexpectedException.ThrowIfNull(options.AutoLineBreakStyle); - BravoUnexpectedException.ThrowIfNull(options.LineBreakStyle); + var lineBreakStyle = options.LineBreakStyle == DaxLineBreakStyle.Auto + ? options.AutoLineBreakStyle + : options.LineBreakStyle; - var daxformatterResponses = await CallDaxFormatterAsync(measures, options).ConfigureAwait(false); - var formattedMeasures = new List(); + foreach (var (daxformatterResponse, index) in daxformatterResponses.WithIndex()) { - var lineBreakStyle = options.LineBreakStyle == DaxLineBreakStyle.Auto - ? options.AutoLineBreakStyle - : options.LineBreakStyle; - - foreach (var (daxformatterResponse, index) in daxformatterResponses.WithIndex()) + var requestedMeasure = measures.ElementAt(index); + var formattedMeasure = new FormattedMeasure { - var requestedMeasure = measures.ElementAt(index); - var formattedMeasure = new FormattedMeasure - { - ETag = requestedMeasure.ETag, - Name = requestedMeasure.Name, - TableName = requestedMeasure.TableName, - }; + ETag = requestedMeasure.ETag, + Name = requestedMeasure.Name, + TableName = requestedMeasure.TableName, + }; - if (daxformatterResponse.Errors is null || daxformatterResponse.Errors.Count == 0) - { - formattedMeasure.Expression = daxformatterResponse.Formatted; - formattedMeasure.LineBreakStyle = lineBreakStyle.Value; - } - else if (/* options.IgnoreEmptyExpressionError && */ requestedMeasure.Expression.IsNullOrWhiteSpace()) // TODO: parameterize this behavior (i.e. options.IgnoreEmptyExpressionError) - { - formattedMeasure.Expression = requestedMeasure.Expression; - formattedMeasure.LineBreakStyle = lineBreakStyle.Value; - } - else - { - formattedMeasure.Expression = requestedMeasure.Expression; // in case of errors returns the original expression, as requested by Daniele - formattedMeasure.Errors = daxformatterResponse.Errors?.Select(FormatterError.CreateFrom); - } - - formattedMeasures.Add(formattedMeasure); + if (daxformatterResponse.Errors is null || daxformatterResponse.Errors.Count == 0) + { + formattedMeasure.Expression = daxformatterResponse.Formatted; + formattedMeasure.LineBreakStyle = lineBreakStyle.Value; + } + else if (/* options.IgnoreEmptyExpressionError && */ requestedMeasure.Expression.IsNullOrWhiteSpace()) // TODO: parameterize this behavior (i.e. options.IgnoreEmptyExpressionError) + { + formattedMeasure.Expression = requestedMeasure.Expression; + formattedMeasure.LineBreakStyle = lineBreakStyle.Value; + } + else + { + formattedMeasure.Expression = requestedMeasure.Expression; // in case of errors returns the original expression, as requested by Daniele + formattedMeasure.Errors = daxformatterResponse.Errors?.Select(FormatterError.CreateFrom); } - } - return formattedMeasures; + formattedMeasures.Add(formattedMeasure); + } } - public DatabaseUpdateResult Update(PBIDesktopReport report, IEnumerable measures) - { - using var connection = TabularConnectionWrapper.ConnectTo(report); - var updateResult = TabularModelHelper.Update(connection.Database, measures); + return formattedMeasures; + } - return updateResult; - } + public DatabaseUpdateResult Update(PBIDesktopReport report, IEnumerable measures) + { + using var connection = TabularConnectionWrapper.ConnectTo(report); + var updateResult = TabularModelHelper.Update(connection.Database, measures); - public DatabaseUpdateResult Update(PBICloudDataset dataset, IEnumerable measures, string accessToken) - { - using var connection = TabularConnectionWrapper.ConnectTo(dataset, accessToken); - var updateResult = TabularModelHelper.Update(connection.Database, measures); + return updateResult; + } - return updateResult; - } + public DatabaseUpdateResult Update(PBICloudDataset dataset, IEnumerable measures, string accessToken) + { + using var connection = TabularConnectionWrapper.ConnectTo(dataset, accessToken); + var updateResult = TabularModelHelper.Update(connection.Database, measures); + + return updateResult; + } + + private async Task> CallDaxFormatterAsync(IEnumerable measures, FormatDaxOptions options) + { + // TODO: move add/remove FormatPrefix to the Dax.Formatter NugGet package + const string FormatPrefix = "[x] := "; + const int FormatPrefixLength = 7; - private async Task> CallDaxFormatterAsync(IEnumerable measures, FormatDaxOptions options) + var request = new DaxFormatterMultipleRequest { - // TODO: move add/remove FormatPrefix to the Dax.Formatter NugGet package - const string FormatPrefix = "[x] := "; - const int FormatPrefixLength = 7; + ServerName = options.ServerName, + ServerVersion = options.ServerVersion, + ServerType = null, // TODO: Dax.Formatter identify ServerType + ServerEdition = options.ServerEdition.TryParseTo(), + ServerMode = options.ServerMode.TryParseTo(), + ServerLocation = options.ServerLocation.TryParseTo(), + DatabaseName = options.DatabaseName, + DatabaseCompatibilityLevel = options.CompatibilityLevel is not null ? options.CompatibilityLevel.ToString() : null, // TODO: Dax.Formatter declare DatabaseCompatibilityLevel int? instead of string + MaxLineLength = options.LineStyle, + SkipSpaceAfterFunctionName = options.SpacingStyle, + ListSeparator = options.ListSeparator ?? ',', // TODO: Dax.Formatter declare ListSeparator nullable + DecimalSeparator = options.DecimalSeparator ?? '.', // TODO: Dax.Formatter declare DecimalSeparator nullable + CallerApp = AppEnvironment.ApplicationName, + CallerVersion = AppEnvironment.VersionInfo.Version, + }; + + foreach (var measure in measures) + { + request.Dax.Add(FormatPrefix + measure.Expression); + } - var request = new DaxFormatterMultipleRequest - { - ServerName = options.ServerName, - ServerVersion = options.ServerVersion, - ServerType = null, // TODO: Dax.Formatter identify ServerType - ServerEdition = options.ServerEdition.TryParseTo(), - ServerMode = options.ServerMode.TryParseTo(), - ServerLocation = options.ServerLocation.TryParseTo(), - DatabaseName = options.DatabaseName, - DatabaseCompatibilityLevel = options.CompatibilityLevel is not null ? options.CompatibilityLevel.ToString() : null, // TODO: Dax.Formatter declare DatabaseCompatibilityLevel int? instead of string - MaxLineLength = options.LineStyle, - SkipSpaceAfterFunctionName = options.SpacingStyle, - ListSeparator = options.ListSeparator ?? ',', // TODO: Dax.Formatter declare ListSeparator nullable - DecimalSeparator = options.DecimalSeparator ?? '.', // TODO: Dax.Formatter declare DecimalSeparator nullable - CallerApp = AppEnvironment.ApplicationName, - CallerVersion = AppEnvironment.VersionInfo.Version, - }; - - foreach (var measure in measures) + var responses = await _daxformatterClient.FormatAsync(request).ConfigureAwait(false); + + foreach (var response in responses) + { + if (response.Errors is null || response.Errors.Count == 0) { - request.Dax.Add(FormatPrefix + measure.Expression); + response.Formatted = response.Formatted?.Remove(0, FormatPrefixLength - 1); + response.Formatted = response.Formatted?.NormalizeDax().Expression; } - - var responses = await _daxformatterClient.FormatAsync(request).ConfigureAwait(false); - - foreach (var response in responses) + else { - if (response.Errors is null || response.Errors.Count == 0) - { - response.Formatted = response.Formatted?.Remove(0, FormatPrefixLength - 1); - response.Formatted = response.Formatted?.NormalizeDax().Expression; - } - else + foreach (var error in response.Errors) { - foreach (var error in response.Errors) + if (error.Line == 0) { - if (error.Line == 0) - { - // subtract the length of the prefix we removed only if the error is on the first line (zero-based index) - error.Column -= FormatPrefixLength; - // Don't 'break;' as we can have multilple errors reported for a single line - } + // subtract the length of the prefix we removed only if the error is on the first line (zero-based index) + error.Column -= FormatPrefixLength; + // Don't 'break;' as we can have multilple errors reported for a single line } } } - - return responses; } + + return responses; } } diff --git a/src/Services/ManageDatesService.cs b/src/Services/ManageDatesService.cs index 9039c94c..8d62ba96 100644 --- a/src/Services/ManageDatesService.cs +++ b/src/Services/ManageDatesService.cs @@ -1,171 +1,170 @@ -namespace Sqlbi.Bravo.Services +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using Dax.Template; +using Dax.Template.Exceptions; +using Dax.Template.Model; +using Microsoft.AnalysisServices.AdomdClient; +using Sqlbi.Bravo.Infrastructure; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Policies; +using Sqlbi.Bravo.Infrastructure.Services; +using Sqlbi.Bravo.Infrastructure.Services.DaxTemplate; +using Sqlbi.Bravo.Models; +using Sqlbi.Bravo.Models.ManageDates; + +namespace Sqlbi.Bravo.Services; + +public interface IManageDatesService { - using Dax.Template; - using Dax.Template.Exceptions; - using Dax.Template.Model; - using Microsoft.AnalysisServices.AdomdClient; - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Policies; - using Sqlbi.Bravo.Infrastructure.Services; - using Sqlbi.Bravo.Infrastructure.Services.DaxTemplate; - using Sqlbi.Bravo.Models; - using Sqlbi.Bravo.Models.ManageDates; - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading; - - public interface IManageDatesService - { - IEnumerable GetConfigurations(PBIDesktopReport report, CancellationToken cancellationToken); + IEnumerable GetConfigurations(PBIDesktopReport report, CancellationToken cancellationToken); - DateConfiguration ValidateConfiguration(PBIDesktopReport report, DateConfiguration configuration, CancellationToken cancellationToken); + DateConfiguration ValidateConfiguration(PBIDesktopReport report, DateConfiguration configuration, CancellationToken cancellationToken); - ModelChanges? GetPreviewChanges(PBIDesktopReport report, PreviewChangesSettings settings, CancellationToken cancellationToken); + ModelChanges? GetPreviewChanges(PBIDesktopReport report, PreviewChangesSettings settings, CancellationToken cancellationToken); - void ApplyConfiguration(PBIDesktopReport report, DateConfiguration configuration, CancellationToken cancellationToken); - } + void ApplyConfiguration(PBIDesktopReport report, DateConfiguration configuration, CancellationToken cancellationToken); +} + +internal class ManageDatesService : IManageDatesService +{ + private readonly DaxTemplateManager _templateManager; - internal class ManageDatesService : IManageDatesService + public ManageDatesService(IPolicies policies) { - private readonly DaxTemplateManager _templateManager; + _templateManager = new DaxTemplateManager(policies); + } - public ManageDatesService(IPolicies policies) + public IEnumerable GetConfigurations(PBIDesktopReport report, CancellationToken cancellationToken) + { + IEnumerable packages; + try { - _templateManager = new DaxTemplateManager(policies); + packages = _templateManager.GetPackages(); } - - public IEnumerable GetConfigurations(PBIDesktopReport report, CancellationToken cancellationToken) + catch (TemplateException ex) { - IEnumerable packages; - try - { - packages = _templateManager.GetPackages(); - } - catch (TemplateException ex) - { - throw new BravoException(BravoProblem.ManageDateTemplateError, ex.Message, ex); - } + throw new BravoException(BravoProblem.ManageDateTemplateError, ex.Message, ex); + } - var configurations = packages.Select(DateConfiguration.CreateFrom).ToList(); - using var connection = TabularConnectionWrapper.ConnectTo(report); - var currentConfiguration = DateConfiguration.GetCurrentFrom(connection.Model); + var configurations = packages.Select(DateConfiguration.CreateFrom).ToList(); + using var connection = TabularConnectionWrapper.ConnectTo(report); + var currentConfiguration = DateConfiguration.GetCurrentFrom(connection.Model); - if (currentConfiguration is not null) - configurations.Insert(0, currentConfiguration); + if (currentConfiguration is not null) + configurations.Insert(0, currentConfiguration); - Validate(report, configurations, assertValidation: false); + Validate(report, configurations, assertValidation: false); - return configurations; - } + return configurations; + } - public DateConfiguration ValidateConfiguration(PBIDesktopReport report, DateConfiguration configuration, CancellationToken cancellationToken) - { - Validate(report, configuration, assertValidation: false); + public DateConfiguration ValidateConfiguration(PBIDesktopReport report, DateConfiguration configuration, CancellationToken cancellationToken) + { + Validate(report, configuration, assertValidation: false); - return configuration; - } + return configuration; + } - public ModelChanges? GetPreviewChanges(PBIDesktopReport report, PreviewChangesSettings settings, CancellationToken cancellationToken) - { - BravoUnexpectedException.ThrowIfNull(settings.Configuration); - Validate(report, settings.Configuration, assertValidation: true); + public ModelChanges? GetPreviewChanges(PBIDesktopReport report, PreviewChangesSettings settings, CancellationToken cancellationToken) + { + BravoUnexpectedException.ThrowIfNull(settings.Configuration); + Validate(report, settings.Configuration, assertValidation: true); - using var connection = TabularConnectionWrapper.ConnectTo(report); - try - { - var modelChanges = _templateManager.GetPreviewChanges(settings.Configuration, settings.PreviewRows, connection, cancellationToken); - return modelChanges; - } - catch (Exception ex) when (ex is TemplateException || ex is AdomdException) - { - throw new BravoException(BravoProblem.ManageDateTemplateError, ex.Message, ex); - } + using var connection = TabularConnectionWrapper.ConnectTo(report); + try + { + var modelChanges = _templateManager.GetPreviewChanges(settings.Configuration, settings.PreviewRows, connection, cancellationToken); + return modelChanges; } - - public void ApplyConfiguration(PBIDesktopReport report, DateConfiguration configuration, CancellationToken cancellationToken) + catch (Exception ex) when (ex is TemplateException || ex is AdomdException) { - Validate(report, configuration, assertValidation: true); - - using var connection = TabularConnectionWrapper.ConnectTo(report); - try - { - _templateManager.ApplyConfiguration(configuration, connection, cancellationToken); - } - catch (TemplateException ex) - { - throw new BravoException(BravoProblem.ManageDateTemplateError, ex.Message, ex); - } + throw new BravoException(BravoProblem.ManageDateTemplateError, ex.Message, ex); } + } - private static void Validate(PBIDesktopReport report, DateConfiguration configuration, bool assertValidation) => Validate(report, new[] { configuration }, assertValidation); + public void ApplyConfiguration(PBIDesktopReport report, DateConfiguration configuration, CancellationToken cancellationToken) + { + Validate(report, configuration, assertValidation: true); - private static void Validate(PBIDesktopReport report, IEnumerable configurations, bool assertValidation) + using var connection = TabularConnectionWrapper.ConnectTo(report); + try { - using var connection = TabularConnectionWrapper.ConnectTo(report); + _templateManager.ApplyConfiguration(configuration, connection, cancellationToken); + } + catch (TemplateException ex) + { + throw new BravoException(BravoProblem.ManageDateTemplateError, ex.Message, ex); + } + } - foreach (var configuration in configurations) + private static void Validate(PBIDesktopReport report, DateConfiguration configuration, bool assertValidation) => Validate(report, new[] { configuration }, assertValidation); + + private static void Validate(PBIDesktopReport report, IEnumerable configurations, bool assertValidation) + { + using var connection = TabularConnectionWrapper.ConnectTo(report); + + foreach (var configuration in configurations) + { + if (configuration.DateEnabled) { - if (configuration.DateEnabled) - { - configuration.DateTableValidation = Validate(configuration.DateTableName); - configuration.DateReferenceTableValidation = Validate(configuration.DateReferenceTableName); - - if (assertValidation) - { - configuration.DateTableValidation.Assert(); - configuration.DateReferenceTableValidation.Assert(); - } - } + configuration.DateTableValidation = Validate(configuration.DateTableName); + configuration.DateReferenceTableValidation = Validate(configuration.DateReferenceTableName); - if (configuration.HolidaysEnabled) + if (assertValidation) { - configuration.HolidaysTableValidation = Validate(configuration.HolidaysTableName); - configuration.HolidaysDefinitionTableValidation = Validate(configuration.HolidaysDefinitionTableName); - - if (assertValidation) - { - configuration.HolidaysTableValidation.Assert(); - configuration.HolidaysDefinitionTableValidation.Assert(); - } + configuration.DateTableValidation.Assert(); + configuration.DateReferenceTableValidation.Assert(); } + } - if (configuration.TimeIntelligenceEnabled) + if (configuration.HolidaysEnabled) + { + configuration.HolidaysTableValidation = Validate(configuration.HolidaysTableName); + configuration.HolidaysDefinitionTableValidation = Validate(configuration.HolidaysDefinitionTableName); + + if (assertValidation) { - // nothing todo + configuration.HolidaysTableValidation.Assert(); + configuration.HolidaysDefinitionTableValidation.Assert(); } } - TableValidation Validate(string? tableName) + if (configuration.TimeIntelligenceEnabled) + { + // nothing todo + } + } + + TableValidation Validate(string? tableName) + { + var validation = TableValidation.Unknown; + + if (!TabularModelHelper.IsValidTableName(tableName)) + { + validation = TableValidation.InvalidNamingRequirements; + } + else { - var validation = TableValidation.Unknown; + var table = connection.Model.Tables.Find(tableName); - if (!TabularModelHelper.IsValidTableName(tableName)) + if (table is null) { - validation = TableValidation.InvalidNamingRequirements; + validation = TableValidation.ValidNotExists; + } + else if (table.IsCalculated()) + { + validation = TableValidation.ValidAlterable; } else { - var table = connection.Model.Tables.Find(tableName); - - if (table is null) - { - validation = TableValidation.ValidNotExists; - } - else if (table.IsCalculated()) - { - validation = TableValidation.ValidAlterable; - } - else - { - validation = TableValidation.InvalidExists; - } + validation = TableValidation.InvalidExists; } - - return validation; } + + return validation; } } } diff --git a/src/Services/TemplateDevelopmentService.cs b/src/Services/TemplateDevelopmentService.cs index a380ad4e..8c3f5da8 100644 --- a/src/Services/TemplateDevelopmentService.cs +++ b/src/Services/TemplateDevelopmentService.cs @@ -1,452 +1,449 @@ -namespace Sqlbi.Bravo.Services +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.Json; +using System.Threading; +using Dax.Template.Exceptions; +using Dax.Template.Model; +using Microsoft.AnalysisServices.AdomdClient; +using Sqlbi.Bravo.Infrastructure; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Sqlbi.Bravo.Infrastructure.Policies; +using Sqlbi.Bravo.Infrastructure.Services; +using Sqlbi.Bravo.Infrastructure.Services.DaxTemplate; +using Sqlbi.Bravo.Models; +using Sqlbi.Bravo.Models.ManageDates; +using Sqlbi.Bravo.Models.TemplateDevelopment; + +namespace Sqlbi.Bravo.Services; + +public interface ITemplateDevelopmentService { - using Dax.Template.Exceptions; - using Dax.Template.Model; - using Microsoft.AnalysisServices.AdomdClient; - using Microsoft.AspNetCore.Hosting.Server; - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Helpers; - using Sqlbi.Bravo.Infrastructure.Telemetry; - using Sqlbi.Bravo.Infrastructure.Services; - using Sqlbi.Bravo.Infrastructure.Services.DaxTemplate; - using Sqlbi.Bravo.Models; - using Sqlbi.Bravo.Models.ManageDates; - using Sqlbi.Bravo.Models.TemplateDevelopment; - using System; - using System.Collections.Generic; - using System.IO; - using System.Linq; - using System.Text.Json; - using System.Threading; - using Sqlbi.Bravo.Infrastructure.Policies; - - public interface ITemplateDevelopmentService - { - IEnumerable GetConfigurations(); + IEnumerable GetConfigurations(); - DateConfiguration? GetPackageConfiguration(string path); + DateConfiguration? GetPackageConfiguration(string path); - CustomPackage GetUserCustomPackage(string path); + CustomPackage GetUserCustomPackage(string path); - IEnumerable GetOrganizationCustomPackages(); + IEnumerable GetOrganizationCustomPackages(); - CustomPackage Validate(CustomPackage customPackage); + CustomPackage Validate(CustomPackage customPackage); - CustomPackage CreateWorkspace(string path, string name, DateConfiguration configuration); + CustomPackage CreateWorkspace(string path, string name, DateConfiguration configuration); - bool ConfigureWorkspace(string path, bool openCodeWorkspace); + bool ConfigureWorkspace(string path, bool openCodeWorkspace); - ModelChanges? GetPreviewChanges(PBIDesktopReport report, WorkspacePreviewChangesSettings settings, CancellationToken cancellationToken); - } + ModelChanges? GetPreviewChanges(PBIDesktopReport report, WorkspacePreviewChangesSettings settings, CancellationToken cancellationToken); +} - internal class TemplateDevelopmentService : ITemplateDevelopmentService +internal class TemplateDevelopmentService : ITemplateDevelopmentService +{ + private const string WorkspaceSourceFolderName = "src"; + private const string WorkspaceDistributionFolderName = "dist"; + private const string WorkspaceGitignoreFileName = ".gitignore"; + private const string WorkspaceConfigFileName = "bravo-config.json"; + private const string VSCodeExtensionsFileName = "extensions.json"; + private const string VSCodeExtensionName = "sqlbi.bravo-template-editor"; + private const string VSCodeWorkspaceFileExtension = ".code-workspace"; + private const string CustomPackageFileExtension = ".package.json"; + + private readonly JsonSerializerOptions _serializerOptions; + private readonly DaxTemplateManager _templateManager; + private readonly IServerAddressProvider _serverAddressProvider; + private readonly IPolicies _policies; + + public TemplateDevelopmentService(IServerAddressProvider serverAddressProvider, IPolicies policies) { - private const string WorkspaceSourceFolderName = "src"; - private const string WorkspaceDistributionFolderName = "dist"; - private const string WorkspaceGitignoreFileName = ".gitignore"; - private const string WorkspaceConfigFileName = "bravo-config.json"; - private const string VSCodeExtensionsFileName = "extensions.json"; - private const string VSCodeExtensionName = "sqlbi.bravo-template-editor"; - private const string VSCodeWorkspaceFileExtension = ".code-workspace"; - private const string CustomPackageFileExtension = ".package.json"; - - private readonly JsonSerializerOptions _serializerOptions; - private readonly DaxTemplateManager _templateManager; - private readonly IServerAddressProvider _serverAddressProvider; - private readonly IPolicies _policies; - - public TemplateDevelopmentService(IServerAddressProvider serverAddressProvider, IPolicies policies) - { - _serverAddressProvider = serverAddressProvider; - _policies = policies; - _templateManager = new DaxTemplateManager(policies); - _serializerOptions = new JsonSerializerOptions(AppEnvironment.DefaultJsonOptions) { WriteIndented = true }; - } + _serverAddressProvider = serverAddressProvider; + _policies = policies; + _templateManager = new DaxTemplateManager(policies); + _serializerOptions = new JsonSerializerOptions(AppEnvironment.DefaultJsonOptions) { WriteIndented = true }; + } - public IEnumerable GetConfigurations() - { - var packages = _templateManager.GetPackages(); - var configurations = packages.Select(DateConfiguration.CreateFrom).ToArray(); + public IEnumerable GetConfigurations() + { + var packages = _templateManager.GetPackages(); + var configurations = packages.Select(DateConfiguration.CreateFrom).ToArray(); - return configurations; - } + return configurations; + } - public DateConfiguration? GetPackageConfiguration(string path) + public DateConfiguration? GetPackageConfiguration(string path) + { + if (File.Exists(path)) { - if (File.Exists(path)) - { - var package = _templateManager.GetPackage(path); - var configuration = DateConfiguration.CreateFrom(package); + var package = _templateManager.GetPackage(path); + var configuration = DateConfiguration.CreateFrom(package); - configuration.IsCustom = true; - return configuration; - } - - return null; + configuration.IsCustom = true; + return configuration; } - public CustomPackage GetUserCustomPackage(string path) + return null; + } + + public CustomPackage GetUserCustomPackage(string path) + { + var customPackage = new CustomPackage { - var customPackage = new CustomPackage - { - Type = CustomPackageType.User, - }; + Type = CustomPackageType.User, + }; - var fileInfo = new FileInfo(path); - if (fileInfo.Exists == false) - return customPackage; + var fileInfo = new FileInfo(path); + if (fileInfo.Exists == false) + return customPackage; - if (fileInfo.Name.EndsWithI(CustomPackageFileExtension)) + if (fileInfo.Name.EndsWithI(CustomPackageFileExtension)) + { + if (fileInfo.Directory?.Parent is not null) { - if (fileInfo.Directory?.Parent is not null) - { - SetPackageFileDetails(fileInfo.FullName, customPackage); + SetPackageFileDetails(fileInfo.FullName, customPackage); - var workspaceName = fileInfo.Directory.Parent.Name; - if (workspaceName.EqualsI(customPackage.Name)) - { - var workspacePath = fileInfo.Directory.Parent.FullName; - var codeworkspaceFileName = Path.ChangeExtension(workspaceName, VSCodeWorkspaceFileExtension); - var codeworkspaceFilePath = Path.Combine(workspacePath, codeworkspaceFileName); + var workspaceName = fileInfo.Directory.Parent.Name; + if (workspaceName.EqualsI(customPackage.Name)) + { + var workspacePath = fileInfo.Directory.Parent.FullName; + var codeworkspaceFileName = Path.ChangeExtension(workspaceName, VSCodeWorkspaceFileExtension); + var codeworkspaceFilePath = Path.Combine(workspacePath, codeworkspaceFileName); - SetWorkspaceFileDetails(codeworkspaceFilePath, customPackage); - } + SetWorkspaceFileDetails(codeworkspaceFilePath, customPackage); } } - else if (fileInfo.Extension.EqualsI(VSCodeWorkspaceFileExtension)) + } + else if (fileInfo.Extension.EqualsI(VSCodeWorkspaceFileExtension)) + { + if (fileInfo.Directory is not null) { - if (fileInfo.Directory is not null) - { - SetWorkspaceFileDetails(fileInfo.FullName, customPackage); + SetWorkspaceFileDetails(fileInfo.FullName, customPackage); - if (customPackage.HasWorkspace) - { - var pacakgeFileName = Path.ChangeExtension(customPackage.WorkspaceName!, CustomPackageFileExtension); - var packageFilePath = Path.Combine(customPackage.WorkspacePath!, WorkspaceDistributionFolderName, pacakgeFileName); + if (customPackage.HasWorkspace) + { + var pacakgeFileName = Path.ChangeExtension(customPackage.WorkspaceName!, CustomPackageFileExtension); + var packageFilePath = Path.Combine(customPackage.WorkspacePath!, WorkspaceDistributionFolderName, pacakgeFileName); - SetPackageFileDetails(packageFilePath, customPackage); - } + SetPackageFileDetails(packageFilePath, customPackage); } } + } - return customPackage; + return customPackage; - void SetPackageFileDetails(string path, CustomPackage customPackage) + void SetPackageFileDetails(string path, CustomPackage customPackage) + { + if (File.Exists(path)) { - if (File.Exists(path)) - { - var package = _templateManager.GetPackage(path); + var package = _templateManager.GetPackage(path); - customPackage.Path = path; - customPackage.Name = package.Configuration.Name; - customPackage.Description = package.Configuration.Description; - customPackage.HasPackage = true; - } + customPackage.Path = path; + customPackage.Name = package.Configuration.Name; + customPackage.Description = package.Configuration.Description; + customPackage.HasPackage = true; } + } - void SetWorkspaceFileDetails(string path, CustomPackage customPackage) + void SetWorkspaceFileDetails(string path, CustomPackage customPackage) + { + var codeworkspaceFileInfo = new FileInfo(path); + if (codeworkspaceFileInfo.Exists && codeworkspaceFileInfo.Directory is not null) { - var codeworkspaceFileInfo = new FileInfo(path); - if (codeworkspaceFileInfo.Exists && codeworkspaceFileInfo.Directory is not null) + var workspaceName = Path.GetFileNameWithoutExtension(codeworkspaceFileInfo.Name); + if (workspaceName.EqualsI(codeworkspaceFileInfo.Directory.Name)) { - var workspaceName = Path.GetFileNameWithoutExtension(codeworkspaceFileInfo.Name); - if (workspaceName.EqualsI(codeworkspaceFileInfo.Directory.Name)) - { - customPackage.WorkspaceName = workspaceName; - customPackage.WorkspacePath = codeworkspaceFileInfo.Directory.FullName; - customPackage.HasWorkspace = true; - } + customPackage.WorkspaceName = workspaceName; + customPackage.WorkspacePath = codeworkspaceFileInfo.Directory.FullName; + customPackage.HasWorkspace = true; } } } + } - public IEnumerable GetOrganizationCustomPackages() - { - var customPackages = new List(); + public IEnumerable GetOrganizationCustomPackages() + { + var customPackages = new List(); - var repositoryPath = _policies.CustomTemplatesOrganizationRepositoryPath; - if (repositoryPath is not null && Directory.Exists(repositoryPath)) + var repositoryPath = _policies.CustomTemplatesOrganizationRepositoryPath; + if (repositoryPath is not null && Directory.Exists(repositoryPath)) + { + var packagePaths = Directory.EnumerateFiles(repositoryPath, searchPattern: $"*{CustomPackageFileExtension}", new EnumerationOptions { - var packagePaths = Directory.EnumerateFiles(repositoryPath, searchPattern: $"*{CustomPackageFileExtension}", new EnumerationOptions - { - IgnoreInaccessible = true, - //RecurseSubdirectories = true, - }); + IgnoreInaccessible = true, + //RecurseSubdirectories = true, + }); - foreach (var packagePath in packagePaths) + foreach (var packagePath in packagePaths) + { + var package = _templateManager.GetPackage(packagePath); + var customPackage = new CustomPackage { - var package = _templateManager.GetPackage(packagePath); - var customPackage = new CustomPackage - { - Type = CustomPackageType.Organization, - Path = packagePath, - Name = package.Configuration.Name, - Description = package.Configuration.Description, - HasPackage = true, - }; - customPackages.Add(customPackage); - } + Type = CustomPackageType.Organization, + Path = packagePath, + Name = package.Configuration.Name, + Description = package.Configuration.Description, + HasPackage = true, + }; + customPackages.Add(customPackage); } - - return customPackages; } - public CustomPackage Validate(CustomPackage customPackage) - { - customPackage.HasWorkspace = false; - customPackage.HasPackage = false; + return customPackages; + } - if (customPackage.Path is not null) - { - var existingCustomPackage = GetUserCustomPackage(customPackage.Path); + public CustomPackage Validate(CustomPackage customPackage) + { + customPackage.HasWorkspace = false; + customPackage.HasPackage = false; - if (customPackage.HasWorkspace == false && existingCustomPackage.HasWorkspace == true) - { - customPackage.WorkspaceName = existingCustomPackage.WorkspaceName; - customPackage.WorkspacePath = existingCustomPackage.WorkspacePath; - } + if (customPackage.Path is not null) + { + var existingCustomPackage = GetUserCustomPackage(customPackage.Path); - customPackage.HasWorkspace = existingCustomPackage.HasWorkspace; - customPackage.HasPackage = existingCustomPackage.HasPackage; - } - else if (customPackage.WorkspacePath is not null && customPackage.WorkspaceName is not null) + if (customPackage.HasWorkspace == false && existingCustomPackage.HasWorkspace == true) { - var workspaceCodeworkspaceName = Path.ChangeExtension(customPackage.WorkspaceName, VSCodeWorkspaceFileExtension); - var workspaceCodeworkspacePath = Path.Combine(customPackage.WorkspacePath, workspaceCodeworkspaceName); - var existingCustomPackage = GetUserCustomPackage(workspaceCodeworkspacePath); + customPackage.WorkspaceName = existingCustomPackage.WorkspaceName; + customPackage.WorkspacePath = existingCustomPackage.WorkspacePath; + } - if (customPackage.HasPackage == false && existingCustomPackage.HasPackage == true) - { - customPackage.Path = existingCustomPackage.Path; - customPackage.Name = existingCustomPackage.Name; - customPackage.Description = existingCustomPackage.Description; - } + customPackage.HasWorkspace = existingCustomPackage.HasWorkspace; + customPackage.HasPackage = existingCustomPackage.HasPackage; + } + else if (customPackage.WorkspacePath is not null && customPackage.WorkspaceName is not null) + { + var workspaceCodeworkspaceName = Path.ChangeExtension(customPackage.WorkspaceName, VSCodeWorkspaceFileExtension); + var workspaceCodeworkspacePath = Path.Combine(customPackage.WorkspacePath, workspaceCodeworkspaceName); + var existingCustomPackage = GetUserCustomPackage(workspaceCodeworkspacePath); - customPackage.HasWorkspace = existingCustomPackage.HasWorkspace; - customPackage.HasPackage = existingCustomPackage.HasPackage; + if (customPackage.HasPackage == false && existingCustomPackage.HasPackage == true) + { + customPackage.Path = existingCustomPackage.Path; + customPackage.Name = existingCustomPackage.Name; + customPackage.Description = existingCustomPackage.Description; } - return customPackage; + customPackage.HasWorkspace = existingCustomPackage.HasWorkspace; + customPackage.HasPackage = existingCustomPackage.HasPackage; } - public CustomPackage CreateWorkspace(string path, string name, DateConfiguration configuration) + return customPackage; + } + + public CustomPackage CreateWorkspace(string path, string name, DateConfiguration configuration) + { + BravoUnexpectedException.Assert(configuration.IsCustom == false); + + var workspaceName = name.ReplaceInvalidFileNameChars(); + var workspacePath = Path.Combine(path, workspaceName); + var customPackage = new CustomPackage + { + Type = CustomPackageType.User, + Path = null, + Name = name, + Description = null, + WorkspaceName = workspaceName, + WorkspacePath = workspacePath, + HasWorkspace = true, + HasPackage = false, + }; + var package = configuration.LoadPackage(configure: false); + + // src\*.json files { - BravoUnexpectedException.Assert(configuration.IsCustom == false); + var templatePath = Path.Combine(workspacePath, WorkspaceSourceFolderName); + Directory.CreateDirectory(templatePath); - var workspaceName = name.ReplaceInvalidFileNameChars(); - var workspacePath = Path.Combine(path, workspaceName); - var customPackage = new CustomPackage + BravoUnexpectedException.ThrowIfNull(package.Configuration.TemplateUri); { - Type = CustomPackageType.User, - Path = null, - Name = name, - Description = null, - WorkspaceName = workspaceName, - WorkspacePath = workspacePath, - HasWorkspace = true, - HasPackage = false, - }; - var package = configuration.LoadPackage(configure: false); - - // src\*.json files - { - var templatePath = Path.Combine(workspacePath, WorkspaceSourceFolderName); - Directory.CreateDirectory(templatePath); + package.Configuration.Name = customPackage.Name; + package.Configuration.Description = customPackage.Description; - BravoUnexpectedException.ThrowIfNull(package.Configuration.TemplateUri); - { - package.Configuration.Name = customPackage.Name; - package.Configuration.Description = customPackage.Description; - - var configJson = JsonSerializer.Serialize(package.Configuration, new JsonSerializerOptions() { WriteIndented = true }); - var configPath = Path.Combine(templatePath, Path.GetFileName(package.Configuration.TemplateUri)); - File.WriteAllText(configPath, configJson); - } + var configJson = JsonSerializer.Serialize(package.Configuration, new JsonSerializerOptions() { WriteIndented = true }); + var configPath = Path.Combine(templatePath, Path.GetFileName(package.Configuration.TemplateUri)); + File.WriteAllText(configPath, configJson); + } - if (package.Configuration.Templates is not null) + if (package.Configuration.Templates is not null) + { + foreach (var template in package.Configuration.Templates) { - foreach (var template in package.Configuration.Templates) + if (template.Template is not null) { - if (template.Template is not null) - { - var sourcePath = Path.Combine(DaxTemplateManager.CachePath, template.Template); - var destinationPath = Path.Combine(templatePath, template.Template); - File.Copy(sourcePath, destinationPath, overwrite: false); - } + var sourcePath = Path.Combine(DaxTemplateManager.CachePath, template.Template); + var destinationPath = Path.Combine(templatePath, template.Template); + File.Copy(sourcePath, destinationPath, overwrite: false); } } + } - if (package.Configuration.LocalizationFiles is not null) + if (package.Configuration.LocalizationFiles is not null) + { + foreach (var localizationFile in package.Configuration.LocalizationFiles) { - foreach (var localizationFile in package.Configuration.LocalizationFiles) - { - var sourcePath = Path.Combine(DaxTemplateManager.CachePath, localizationFile); - var destinationPath = Path.Combine(templatePath, localizationFile); - File.Copy(sourcePath, destinationPath, overwrite: false); - } + var sourcePath = Path.Combine(DaxTemplateManager.CachePath, localizationFile); + var destinationPath = Path.Combine(templatePath, localizationFile); + File.Copy(sourcePath, destinationPath, overwrite: false); } } + } - // dist\[name].package.json - { - var packageDistributionFolderPath = Path.Combine(workspacePath, WorkspaceDistributionFolderName); - var packageFileName = Path.ChangeExtension(workspaceName, CustomPackageFileExtension); - var packageFilePath = Path.Combine(packageDistributionFolderPath, packageFileName); + // dist\[name].package.json + { + var packageDistributionFolderPath = Path.Combine(workspacePath, WorkspaceDistributionFolderName); + var packageFileName = Path.ChangeExtension(workspaceName, CustomPackageFileExtension); + var packageFilePath = Path.Combine(packageDistributionFolderPath, packageFileName); - Directory.CreateDirectory(packageDistributionFolderPath); - package.SaveTo(packageFilePath); + Directory.CreateDirectory(packageDistributionFolderPath); + package.SaveTo(packageFilePath); - customPackage.Path = packageFilePath; - customPackage.HasPackage = true; - } + customPackage.Path = packageFilePath; + customPackage.HasPackage = true; + } - // .vscode\extensions.json - var vscodePath = Path.Combine(workspacePath, ".vscode"); - var vscodeExtensionsFile = Path.Combine(vscodePath, VSCodeExtensionsFileName); + // .vscode\extensions.json + var vscodePath = Path.Combine(workspacePath, ".vscode"); + var vscodeExtensionsFile = Path.Combine(vscodePath, VSCodeExtensionsFileName); + { + if (File.Exists(vscodeExtensionsFile) == false) { - if (File.Exists(vscodeExtensionsFile) == false) - { - Directory.CreateDirectory(vscodePath); + Directory.CreateDirectory(vscodePath); - var content = GetVSCodeExtensionsContent(); - File.WriteAllText(vscodeExtensionsFile, content); - } + var content = GetVSCodeExtensionsContent(); + File.WriteAllText(vscodeExtensionsFile, content); } + } - // .code-workspace - var codeworkspaceName = Path.ChangeExtension(workspaceName, VSCodeWorkspaceFileExtension); - var codeworkspaceFile = Path.Combine(workspacePath, codeworkspaceName); + // .code-workspace + var codeworkspaceName = Path.ChangeExtension(workspaceName, VSCodeWorkspaceFileExtension); + var codeworkspaceFile = Path.Combine(workspacePath, codeworkspaceName); + { + if (File.Exists(codeworkspaceFile) == false) { - if (File.Exists(codeworkspaceFile) == false) - { - var content = GetVSCodeCodeworkspaceContent(); - File.WriteAllText(codeworkspaceFile, content); - } + var content = GetVSCodeCodeworkspaceContent(); + File.WriteAllText(codeworkspaceFile, content); } + } - // .gitignore - var gitignoreFile = Path.Combine(workspacePath, WorkspaceGitignoreFileName); + // .gitignore + var gitignoreFile = Path.Combine(workspacePath, WorkspaceGitignoreFileName); + { + if (File.Exists(gitignoreFile) == false) { - if (File.Exists(gitignoreFile) == false) - { - var content = GetWorkspaceGitignoreContent(); - File.WriteAllText(gitignoreFile, content); - } + var content = GetWorkspaceGitignoreContent(); + File.WriteAllText(gitignoreFile, content); } + } - // bravo-config.json - var configFile = Path.Combine(workspacePath, WorkspaceConfigFileName); + // bravo-config.json + var configFile = Path.Combine(workspacePath, WorkspaceConfigFileName); + { + if (File.Exists(configFile) == false) { - if (File.Exists(configFile) == false) - { - File.WriteAllText(configFile, string.Empty); - } + File.WriteAllText(configFile, string.Empty); } - - return customPackage; } - public bool ConfigureWorkspace(string path, bool openCodeWorkspace) - { - var workspacePath = path; - - // bravo-config.json - var configFile = Path.Combine(workspacePath, WorkspaceConfigFileName); - { - if (File.Exists(configFile) == false) - { - return false; // Not a workspace folder - } + return customPackage; + } - var configContent = GetWorkspaceConfigContent(); - File.WriteAllText(configFile, configContent); - } + public bool ConfigureWorkspace(string path, bool openCodeWorkspace) + { + var workspacePath = path; - if (openCodeWorkspace) + // bravo-config.json + var configFile = Path.Combine(workspacePath, WorkspaceConfigFileName); + { + if (File.Exists(configFile) == false) { - var codeworkspaceFiles = Directory.GetFiles(workspacePath, $"*{VSCodeWorkspaceFileExtension}", new EnumerationOptions - { - IgnoreInaccessible = true, - RecurseSubdirectories = false, - }); - - if (codeworkspaceFiles.Length == 1) - { - _ = ProcessHelper.OpenShellExecute(codeworkspaceFiles[0], waitForStarted: false, out var _); - } + return false; // Not a workspace folder } - return true; + var configContent = GetWorkspaceConfigContent(); + File.WriteAllText(configFile, configContent); } - public ModelChanges? GetPreviewChanges(PBIDesktopReport report, WorkspacePreviewChangesSettings settings, CancellationToken cancellationToken) + if (openCodeWorkspace) { - BravoUnexpectedException.ThrowIfNull(settings.CustomPackagePath); - - using var connection = TabularConnectionWrapper.ConnectTo(report); - try + var codeworkspaceFiles = Directory.GetFiles(workspacePath, $"*{VSCodeWorkspaceFileExtension}", new EnumerationOptions { - var package = Dax.Template.Package.LoadFromFile(settings.CustomPackagePath); - var modelChanges = _templateManager.GetPreviewChanges(package, settings.PreviewRows, connection, cancellationToken); + IgnoreInaccessible = true, + RecurseSubdirectories = false, + }); - return modelChanges; - } - catch (Exception ex) when (ex is TemplateException || ex is AdomdException) + if (codeworkspaceFiles.Length == 1) { - throw new BravoException(BravoProblem.TemplateDevelopmentError, ex.Message, ex); + _ = ProcessHelper.OpenShellExecute(codeworkspaceFiles[0], waitForStarted: false, out var _); } } - private string GetWorkspaceConfigContent() + return true; + } + + public ModelChanges? GetPreviewChanges(PBIDesktopReport report, WorkspacePreviewChangesSettings settings, CancellationToken cancellationToken) + { + BravoUnexpectedException.ThrowIfNull(settings.CustomPackagePath); + + using var connection = TabularConnectionWrapper.ConnectTo(report); + try { - var config = new - { - Address = _serverAddressProvider.GetListeningAddress(), - Token = AppEnvironment.ApiAuthenticationTokenTemplateDevelopment, - }; + var package = Dax.Template.Package.LoadFromFile(settings.CustomPackagePath); + var modelChanges = _templateManager.GetPreviewChanges(package, settings.PreviewRows, connection, cancellationToken); - var content = JsonSerializer.Serialize(config, _serializerOptions); - return content; + return modelChanges; + } + catch (Exception ex) when (ex is TemplateException || ex is AdomdException) + { + throw new BravoException(BravoProblem.TemplateDevelopmentError, ex.Message, ex); } + } - private string GetWorkspaceGitignoreContent() + private string GetWorkspaceConfigContent() + { + var config = new { - var content = $@"# Bravo workspace config file + Address = _serverAddressProvider.GetListeningAddress(), + Token = AppEnvironment.ApiAuthenticationTokenTemplateDevelopment, + }; + + var content = JsonSerializer.Serialize(config, _serializerOptions); + return content; + } + + private string GetWorkspaceGitignoreContent() + { + var content = $@"# Bravo workspace config file {WorkspaceConfigFileName} "; - return content; - } + return content; + } - private string GetVSCodeExtensionsContent() + private string GetVSCodeExtensionsContent() + { + var extensions = new { - var extensions = new + Recommendations = new[] { - Recommendations = new[] - { - VSCodeExtensionName - }, - }; + VSCodeExtensionName + }, + }; - var content = JsonSerializer.Serialize(extensions , _serializerOptions); - return content; - } + var content = JsonSerializer.Serialize(extensions, _serializerOptions); + return content; + } - private string GetVSCodeCodeworkspaceContent() + private string GetVSCodeCodeworkspaceContent() + { + var codeworkspace = new { - var codeworkspace = new + Folders = new[] { - Folders = new[] + new { - new - { - Path = "." - } + Path = "." } - }; + } + }; - var content = JsonSerializer.Serialize(codeworkspace, _serializerOptions); - return content; - } + var content = JsonSerializer.Serialize(codeworkspace, _serializerOptions); + return content; } -} \ No newline at end of file +} diff --git a/src/Startup.cs b/src/Startup.cs index ba95ccb7..eb41ad44 100644 --- a/src/Startup.cs +++ b/src/Startup.cs @@ -1,79 +1,78 @@ -namespace Sqlbi.Bravo +using Dax.Formatter; +using Hellang.Middleware.ProblemDetails; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Sqlbi.Bravo.Infrastructure.Configuration.Settings; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Infrastructure.Policies; +using Sqlbi.Bravo.Infrastructure.PowerBI; +using Sqlbi.Bravo.Infrastructure.Services; +using Sqlbi.Bravo.Infrastructure.Services.PowerBI; +using Sqlbi.Bravo.Infrastructure.Telemetry; +using Sqlbi.Bravo.Services; + +namespace Sqlbi.Bravo; + +internal class Startup { - using Dax.Formatter; - using Hellang.Middleware.ProblemDetails; - using Microsoft.AspNetCore.Builder; - using Microsoft.AspNetCore.Hosting; - using Microsoft.Extensions.Configuration; - using Microsoft.Extensions.DependencyInjection; - using Sqlbi.Bravo.Infrastructure.Configuration.Settings; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Infrastructure.Policies; - using Sqlbi.Bravo.Infrastructure.PowerBI; - using Sqlbi.Bravo.Infrastructure.Services; - using Sqlbi.Bravo.Infrastructure.Services.PowerBI; - using Sqlbi.Bravo.Infrastructure.Telemetry; - using Sqlbi.Bravo.Services; + public IConfiguration Configuration { get; } - internal class Startup + public Startup(IConfiguration configuration) { - public IConfiguration Configuration { get; } - - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } + Configuration = configuration; + } - public void ConfigureServices(IServiceCollection services) - { - services.AddAndConfigureControllers(); - services.AddAndConfigureCors(); - services.AddAndConfigureAuthorization(); - services.AddAndConfigureAuthentication(); - services.AddAndConfigureProblemDetails(); + public void ConfigureServices(IServiceCollection services) + { + services.AddAndConfigureControllers(); + services.AddAndConfigureCors(); + services.AddAndConfigureAuthorization(); + services.AddAndConfigureAuthentication(); + services.AddAndConfigureProblemDetails(); #if DEBUG - services.AddAndConfigureSwaggerGen(); + services.AddAndConfigureSwaggerGen(); #endif - services.AddHttpClient(); - services.AddOptions().Configure((settings) => settings.FromCommandLineArguments()); //.ValidateDataAnnotations(); + services.AddHttpClient(); + services.AddOptions().Configure((settings) => settings.FromCommandLineArguments()); //.ValidateDataAnnotations(); - services.AddGroupPolicies(); - services.AddSingleton(_ => TelemetryService.Instance); - services.AddPowerBI(); + services.AddGroupPolicies(); + services.AddSingleton(_ => TelemetryService.Instance); + services.AddPowerBI(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - } + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } - public void Configure(IApplicationBuilder application, IWebHostEnvironment environment) - { + public void Configure(IApplicationBuilder application, IWebHostEnvironment environment) + { #if DEBUG - application.UseSwagger(); - application.UseSwaggerUI(); + application.UseSwagger(); + application.UseSwaggerUI(); #endif - application.UseProblemDetails(); - application.UseRouting(); - application.UseCors(); // this call must appear after UseRouting(), but before UseAuthorization() and UseEndpoints() for the middleware to function correctly - application.UseAuthentication(); - application.UseAuthorization(); // this call must appear after UseRouting(), but before UseEndpoints() for the middleware to function correctly + application.UseProblemDetails(); + application.UseRouting(); + application.UseCors(); // this call must appear after UseRouting(), but before UseAuthorization() and UseEndpoints() for the middleware to function correctly + application.UseAuthentication(); + application.UseAuthorization(); // this call must appear after UseRouting(), but before UseEndpoints() for the middleware to function correctly - application.UseEndpoints((endpoints) => - { + application.UseEndpoints((endpoints) => + { #if DEBUG - endpoints.MapControllers(); + endpoints.MapControllers(); #else - // Map controllers and marks them as RequireAuthorization so that all requests must be authorized - endpoints.MapControllers().RequireAuthorization(); + // Map controllers and marks them as RequireAuthorization so that all requests must be authorized + endpoints.MapControllers().RequireAuthorization(); #endif - }); - } + }); } } diff --git a/test/Bravo.Tests/GlobalUsings.cs b/test/Bravo.Tests/GlobalUsings.cs deleted file mode 100644 index b99c99bc..00000000 --- a/test/Bravo.Tests/GlobalUsings.cs +++ /dev/null @@ -1,13 +0,0 @@ -global using System; -global using System.Collections.Generic; -global using System.Diagnostics; -global using System.Diagnostics.CodeAnalysis; -global using System.IO; -global using System.Linq; -global using System.Net; -global using System.Net.Mime; -global using System.Text; -global using System.Text.Json; -global using System.Threading; -global using System.Threading.Tasks; -global using System.Globalization; diff --git a/test/Bravo.Tests/Infrastructure/Extensions/CommonExtensionsTests.cs b/test/Bravo.Tests/Infrastructure/Extensions/CommonExtensionsTests.cs index 8e7e3cf3..1187b207 100644 --- a/test/Bravo.Tests/Infrastructure/Extensions/CommonExtensionsTests.cs +++ b/test/Bravo.Tests/Infrastructure/Extensions/CommonExtensionsTests.cs @@ -1,32 +1,31 @@ -namespace Bravo.Tests.Infrastructure.Extensions -{ - using Sqlbi.Bravo.Infrastructure.Configuration.Settings; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Xunit; +using Sqlbi.Bravo.Infrastructure.Configuration.Settings; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Xunit; + +namespace Bravo.Tests.Infrastructure.Extensions; - public class CommonExtensionsTests +public class CommonExtensionsTests +{ + [Fact] + public void TryParseTo_IntTest() { - [Fact] - public void TryParseTo_IntTest() - { - var expected = UpdateChannelType.Dev; - var actual = EnumExtensions.TryParseTo((int)expected); + var expected = UpdateChannelType.Dev; + var actual = EnumExtensions.TryParseTo((int)expected); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void TryParseTo_IntNullTest1() - { - var @enum = EnumExtensions.TryParseTo((int?)null); - Assert.Null(@enum); - } + [Fact] + public void TryParseTo_IntNullTest1() + { + var @enum = EnumExtensions.TryParseTo((int?)null); + Assert.Null(@enum); + } - [Fact] - public void TryParseTo_IntNullTest2() - { - var @enum = EnumExtensions.TryParseTo(int.MaxValue); - Assert.Null(@enum); - } + [Fact] + public void TryParseTo_IntNullTest2() + { + var @enum = EnumExtensions.TryParseTo(int.MaxValue); + Assert.Null(@enum); } } diff --git a/test/Bravo.Tests/Infrastructure/Extensions/StringExtensionsTests.cs b/test/Bravo.Tests/Infrastructure/Extensions/StringExtensionsTests.cs index ce55680c..77580f6c 100644 --- a/test/Bravo.Tests/Infrastructure/Extensions/StringExtensionsTests.cs +++ b/test/Bravo.Tests/Infrastructure/Extensions/StringExtensionsTests.cs @@ -1,122 +1,121 @@ -namespace Bravo.Tests.Infrastructure.Extensions +using System; +using System.Collections; +using System.IO; +using System.Linq; +using System.Reflection; +using Sqlbi.Bravo.Infrastructure; +using Sqlbi.Bravo.Infrastructure.Extensions; +using Sqlbi.Bravo.Models.FormatDax; +using Xunit; +using Xunit.Abstractions; + +namespace Bravo.Tests.Infrastructure.Extensions; + +public class StringExtensionsTests { - using Sqlbi.Bravo.Infrastructure; - using Sqlbi.Bravo.Infrastructure.Extensions; - using Sqlbi.Bravo.Models.FormatDax; - using System; - using System.Collections; - using System.IO; - using System.Linq; - using System.Reflection; - using Xunit; - using Xunit.Abstractions; - - public class StringExtensionsTests + private readonly ITestOutputHelper _output; + + public StringExtensionsTests(ITestOutputHelper output) { - private readonly ITestOutputHelper _output; + _output = output; + } - public StringExtensionsTests(ITestOutputHelper output) - { - _output = output; - } + [Theory] + [InlineData("", "", DaxLineBreakStyle.None)] + [InlineData(null, null, DaxLineBreakStyle.None)] + [InlineData("\n", "", DaxLineBreakStyle.InitialLineBreak)] + [InlineData("\r\n", "", DaxLineBreakStyle.InitialLineBreak)] + public void NormalizeDax_EmptyTest(string? expression, string? expectedExpression, DaxLineBreakStyle expectedLineBreakStyle) + { + var (actualExpression, actualLineBreakStyle) = expression.NormalizeDax(); - [Theory] - [InlineData("", "", DaxLineBreakStyle.None)] - [InlineData(null, null, DaxLineBreakStyle.None)] - [InlineData("\n", "", DaxLineBreakStyle.InitialLineBreak)] - [InlineData("\r\n", "", DaxLineBreakStyle.InitialLineBreak)] - public void NormalizeDax_EmptyTest(string? expression, string? expectedExpression, DaxLineBreakStyle expectedLineBreakStyle) - { - var (actualExpression, actualLineBreakStyle) = expression.NormalizeDax(); + Assert.Equal(expectedExpression, actualExpression); + Assert.Equal(expectedLineBreakStyle, actualLineBreakStyle); + } - Assert.Equal(expectedExpression, actualExpression); - Assert.Equal(expectedLineBreakStyle, actualLineBreakStyle); - } + [Theory] + [InlineData("CALCULATE\r\n(\r\n[Amount]\r\n)", "CALCULATE\n(\n[Amount]\n)")] + public void NormalizeDax_DefaultEolCharacterTest(string expression, string expectedExpression) + { + var (actualExpression, actualLineBreakStyle) = expression.NormalizeDax(); - [Theory] - [InlineData("CALCULATE\r\n(\r\n[Amount]\r\n)", "CALCULATE\n(\n[Amount]\n)")] - public void NormalizeDax_DefaultEolCharacterTest(string expression, string expectedExpression) - { - var (actualExpression, actualLineBreakStyle) = expression.NormalizeDax(); + Assert.Equal(expectedExpression, actualExpression); + Assert.Equal(DaxLineBreakStyle.None, actualLineBreakStyle); + } - Assert.Equal(expectedExpression, actualExpression); - Assert.Equal(DaxLineBreakStyle.None, actualLineBreakStyle); - } + [Theory] + [InlineData("CALCULATE([Amount])\n", "CALCULATE([Amount])")] + [InlineData("CALCULATE([Amount])\n\n", "CALCULATE([Amount])\n\n")] + [InlineData("CALCULATE([Amount])\n\n\n", "CALCULATE([Amount])\n\n\n")] + [InlineData("CALCULATE([Amount]) ", "CALCULATE([Amount])")] + [InlineData("CALCULATE([Amount]) ", "CALCULATE([Amount]) ")] + [InlineData("CALCULATE([Amount]) ", "CALCULATE([Amount]) ")] + [InlineData("CALCULATE([Amount])\n ", "CALCULATE([Amount])\n")] + public void NormalizeDax_EolTrailingCharacterTest(string expression, string expectedExpression) + { + var (actualExpression, actualLineBreakStyle) = expression.NormalizeDax(); - [Theory] - [InlineData("CALCULATE([Amount])\n", "CALCULATE([Amount])")] - [InlineData("CALCULATE([Amount])\n\n", "CALCULATE([Amount])\n\n")] - [InlineData("CALCULATE([Amount])\n\n\n", "CALCULATE([Amount])\n\n\n")] - [InlineData("CALCULATE([Amount]) ", "CALCULATE([Amount])")] - [InlineData("CALCULATE([Amount]) ", "CALCULATE([Amount]) ")] - [InlineData("CALCULATE([Amount]) ", "CALCULATE([Amount]) ")] - [InlineData("CALCULATE([Amount])\n ", "CALCULATE([Amount])\n")] - public void NormalizeDax_EolTrailingCharacterTest(string expression, string expectedExpression) - { - var (actualExpression, actualLineBreakStyle) = expression.NormalizeDax(); + Assert.Equal(expectedExpression, actualExpression); + Assert.Equal(DaxLineBreakStyle.None, actualLineBreakStyle); + } - Assert.Equal(expectedExpression, actualExpression); - Assert.Equal(DaxLineBreakStyle.None, actualLineBreakStyle); - } + [Fact] + public void AppendApplicationVersion_Test() + { + var actual = "Bravo for Power BI".AppendApplicationVersion(); - [Fact] - public void AppendApplicationVersion_Test() - { - var actual = "Bravo for Power BI".AppendApplicationVersion(); + Assert.NotNull(actual); + Assert.StartsWith("Bravo for Power BI", actual); + } - Assert.NotNull(actual); - Assert.StartsWith("Bravo for Power BI", actual); - } + [Fact] + public void IsPBIDesktopMainWindowTitle_Test() + { + var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Microsoft Power BI Desktop", "bin"); - [Fact] - public void IsPBIDesktopMainWindowTitle_Test() + if (!Directory.Exists(path)) { - var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Microsoft Power BI Desktop", "bin"); - - if (!Directory.Exists(path)) - { - // Skip test if the BPIDesktop folder does not exist (i.e. build agent) - return; - } + // Skip test if the BPIDesktop folder does not exist (i.e. build agent) + return; + } - foreach (var file in Directory.EnumerateFiles(path, "Microsoft.PowerBI.Client.Windows.Resources.dll", SearchOption.AllDirectories)) - { - _output.WriteLine("Resource file '{0}'", file); + foreach (var file in Directory.EnumerateFiles(path, "Microsoft.PowerBI.Client.Windows.Resources.dll", SearchOption.AllDirectories)) + { + _output.WriteLine("Resource file '{0}'", file); - var assembly = Assembly.LoadFrom(file); - var name = assembly.GetManifestResourceNames().SingleOrDefault((name) => name.StartsWith($"Microsoft.PowerBI.Client.Windows.PowerBIStringResources.")); + var assembly = Assembly.LoadFrom(file); + var name = assembly.GetManifestResourceNames().SingleOrDefault((name) => name.StartsWith($"Microsoft.PowerBI.Client.Windows.PowerBIStringResources.")); - if (name is null) - Assert.Fail($"Resource name not found in file '{file}'"); + if (name is null) + Assert.Fail($"Resource name not found in file '{file}'"); - using var stream = assembly.GetManifestResourceStream(name); + using var stream = assembly.GetManifestResourceStream(name); - if (name is null) - Assert.Fail($"Resource stream not found in file '{file}'"); + if (name is null) + Assert.Fail($"Resource stream not found in file '{file}'"); - using var reader = new System.Resources.ResourceReader(stream!); + using var reader = new System.Resources.ResourceReader(stream!); - var found = false; - foreach (DictionaryEntry entry in reader) + var found = false; + foreach (DictionaryEntry entry in reader) + { + if (entry.Key is string key && key == "PowerBIWindowTitle" && entry.Value is string value) { - if (entry.Key is string key && key == "PowerBIWindowTitle" && entry.Value is string value) - { - var unicodeValue = string.Join(" ", value.EnumerateRunes().Select((rune) => $"U+{rune.Value:X4}")); - _output.WriteLine("\t{0} > {1}", unicodeValue, value); + var unicodeValue = string.Join(" ", value.EnumerateRunes().Select((rune) => $"U+{rune.Value:X4}")); + _output.WriteLine("\t{0} > {1}", unicodeValue, value); - var formattedTitle = string.Format(/*System.Globalization.CultureInfo.CurrentCulture,*/ value, "Contoso", "Power BI Desktop"); - var isSupported = AppEnvironment.PBIDesktopMainWindowTitleSuffixes.Any(formattedTitle.EndsWith); + var formattedTitle = string.Format(/*System.Globalization.CultureInfo.CurrentCulture,*/ value, "Contoso", "Power BI Desktop"); + var isSupported = AppEnvironment.PBIDesktopMainWindowTitleSuffixes.Any(formattedTitle.EndsWith); - Assert.True(isSupported, $"Unsupported 'PowerBIWindowTitle' format string in resource file '{file}'"); + Assert.True(isSupported, $"Unsupported 'PowerBIWindowTitle' format string in resource file '{file}'"); - found = true; - break; - } + found = true; + break; } - - if (!found) - Assert.Fail($"Resource key 'PowerBIWindowTitle' not found in file '{file}'"); } + + if (!found) + Assert.Fail($"Resource key 'PowerBIWindowTitle' not found in file '{file}'"); } } } diff --git a/test/Bravo.Tests/Infrastructure/Helpers/CommonHelperTests.cs b/test/Bravo.Tests/Infrastructure/Helpers/CommonHelperTests.cs index f53ab04b..ec8187de 100644 --- a/test/Bravo.Tests/Infrastructure/Helpers/CommonHelperTests.cs +++ b/test/Bravo.Tests/Infrastructure/Helpers/CommonHelperTests.cs @@ -1,25 +1,24 @@ -namespace Bravo.Tests.Infrastructure.Helpers -{ - using Sqlbi.Bravo.Infrastructure.Helpers; - using System; - using Xunit; +using System; +using Sqlbi.Bravo.Infrastructure.Helpers; +using Xunit; + +namespace Bravo.Tests.Infrastructure.Helpers; - public class CommonHelperTests +public class CommonHelperTests +{ + [Theory] + [InlineData("https://www.contoso.com", "https://www.contoso.com/")] + [InlineData("https://www.CONTOSO.com", "https://www.contoso.com/")] + public void NormalizeUriString_SimpleTest(string uriString, string expectedUriString) { - [Theory] - [InlineData("https://www.contoso.com", "https://www.contoso.com/")] - [InlineData("https://www.CONTOSO.com", "https://www.contoso.com/")] - public void NormalizeUriString_SimpleTest(string uriString, string expectedUriString) - { - var actualUriString = CommonHelper.NormalizeUriString(uriString); - Assert.Equal(expectedUriString, actualUriString); - } + var actualUriString = CommonHelper.NormalizeUriString(uriString); + Assert.Equal(expectedUriString, actualUriString); + } - [Fact] - public void NormalizeUriString_RelativeUriTest() - { - var relativeUri = "path/index.htm?key=value"; - Assert.Throws(() => CommonHelper.NormalizeUriString(relativeUri)); - } + [Fact] + public void NormalizeUriString_RelativeUriTest() + { + var relativeUri = "path/index.htm?key=value"; + Assert.Throws(() => CommonHelper.NormalizeUriString(relativeUri)); } } diff --git a/test/Bravo.Tests/Infrastructure/Policies/PoliciesFactoryTests.cs b/test/Bravo.Tests/Infrastructure/Policies/PoliciesFactoryTests.cs index b30201e5..507d42c1 100644 --- a/test/Bravo.Tests/Infrastructure/Policies/PoliciesFactoryTests.cs +++ b/test/Bravo.Tests/Infrastructure/Policies/PoliciesFactoryTests.cs @@ -1,10 +1,10 @@ -namespace Bravo.Tests.Infrastructure.Policies; - +using System.Collections.Generic; using Sqlbi.Bravo.Infrastructure.Configuration.Settings; using Sqlbi.Bravo.Infrastructure.Policies; -using System.Collections.Generic; using Xunit; +namespace Bravo.Tests.Infrastructure.Policies; + /// /// Exercises PoliciesFactory's parsing/precedence logic using an in-memory fake instead of the /// real registry. Registry-adapter behavior itself is covered separately by @@ -38,8 +38,8 @@ public static IEnumerable BoolPolicyNames() => new[] new object[] { nameof(IPolicies.CustomTemplatesEnabled) }, }; - private static bool? GetBoolProperty(Policies policies, string propertyName) - => (bool?)typeof(Policies).GetProperty(propertyName)!.GetValue(policies); + private static bool? GetBoolProperty(Sqlbi.Bravo.Infrastructure.Policies.Policies policies, string propertyName) + => (bool?)typeof(Sqlbi.Bravo.Infrastructure.Policies.Policies).GetProperty(propertyName)!.GetValue(policies); [Fact] public void FromSource_EmptySource_AllPropertiesAreNull() @@ -171,7 +171,7 @@ public void Create_DoesNotThrow_AndReturnsAnInstance() Assert.NotNull(policies); } - private static readonly Policies AllNull = new( + private static readonly Sqlbi.Bravo.Infrastructure.Policies.Policies AllNull = new( TelemetryEnabled: null, UpdateChannel: null, UpdateCheckEnabled: null, @@ -229,7 +229,7 @@ public void Merge_EachPropertyResolvedIndependently() // Guards against a copy-paste wiring mistake in Merge() (e.g. reading the wrong // property from machine/user) by exercising all 7 properties in a single assertion, // each with a distinct machine/user combination. - var machine = new Policies( + var machine = new Sqlbi.Bravo.Infrastructure.Policies.Policies( TelemetryEnabled: true, UpdateChannel: null, UpdateCheckEnabled: null, @@ -238,7 +238,7 @@ public void Merge_EachPropertyResolvedIndependently() CustomTemplatesEnabled: null, CustomTemplatesOrganizationRepositoryPath: null); - var user = new Policies( + var user = new Sqlbi.Bravo.Infrastructure.Policies.Policies( TelemetryEnabled: false, // machine wins UpdateChannel: UpdateChannelType.Dev, // machine unset -> user wins UpdateCheckEnabled: true, // machine unset -> user wins diff --git a/test/Bravo.Tests/Infrastructure/Policies/RegistryPolicySourceTests.cs b/test/Bravo.Tests/Infrastructure/Policies/RegistryPolicySourceTests.cs index 3e9b4261..68261a3e 100644 --- a/test/Bravo.Tests/Infrastructure/Policies/RegistryPolicySourceTests.cs +++ b/test/Bravo.Tests/Infrastructure/Policies/RegistryPolicySourceTests.cs @@ -1,10 +1,10 @@ -namespace Bravo.Tests.Infrastructure.Policies; - +using System; using Microsoft.Win32; using Sqlbi.Bravo.Infrastructure.Policies; -using System; using Xunit; +namespace Bravo.Tests.Infrastructure.Policies; + /// /// Covers only the thin RegistryKey-to-IPolicySource bridging contract (missing key, missing /// value, type mismatches). Uses a real, isolated key under HKEY_CURRENT_USER (writable without diff --git a/test/Bravo.Tests/Infrastructure/Security/CryptographyExtensionsTests.cs b/test/Bravo.Tests/Infrastructure/Security/CryptographyExtensionsTests.cs index 22b86ac9..495428e4 100644 --- a/test/Bravo.Tests/Infrastructure/Security/CryptographyExtensionsTests.cs +++ b/test/Bravo.Tests/Infrastructure/Security/CryptographyExtensionsTests.cs @@ -1,35 +1,34 @@ -namespace Bravo.Tests.Infrastructure.Security -{ - using Sqlbi.Bravo.Infrastructure.Security; - using System.Net; - using Xunit; +using System.Net; +using Sqlbi.Bravo.Infrastructure.Security; +using Xunit; + +namespace Bravo.Tests.Infrastructure.Security; - public class CryptographyExtensionsTests +public class CryptographyExtensionsTests +{ + [Theory] + [InlineData("MyValue123456789$", "5f45bfb8ab5c9ea6fe0762974f7bbbe3602155c66d1139496fc2246c360a874b")] + [InlineData("1234567890??=", "cb0f4399a2850ee589414b82de85b736ced269035c48e4275be850a3162ba284")] + [InlineData("abcdefghiABCDEFGHI??=", "1bca6736f96f84e35fa921938f45ba981a6e3f6aa02bcf46763009d3614cf89d")] + [InlineData("", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")] + public void ToSHA256Hash_SimpleTest(string value, string expected) { - [Theory] - [InlineData("MyValue123456789$", "5f45bfb8ab5c9ea6fe0762974f7bbbe3602155c66d1139496fc2246c360a874b")] - [InlineData("1234567890??=", "cb0f4399a2850ee589414b82de85b736ced269035c48e4275be850a3162ba284")] - [InlineData("abcdefghiABCDEFGHI??=", "1bca6736f96f84e35fa921938f45ba981a6e3f6aa02bcf46763009d3614cf89d")] - [InlineData("", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")] - public void ToSHA256Hash_SimpleTest(string value, string expected) - { - var actual = value.ToSHA256Hash(); - Assert.Equal(expected, actual); - } + var actual = value.ToSHA256Hash(); + Assert.Equal(expected, actual); + } - [Theory] - [InlineData("MySecret^ìfd56486-+{6 ♠ ⌂¿EFE==")] - [InlineData("AsDfJkIl123456")] - [InlineData("123==")] - [InlineData("")] - public void ToProtectedStringToSecureString_SimpleTest(string expected) - { - var secureString = new NetworkCredential(string.Empty, expected).SecurePassword; - var protectedString = secureString.ToProtectedString(); - var secureStringActual = protectedString.ToSecureString(); - var actual = new NetworkCredential(string.Empty, secureStringActual).Password; + [Theory] + [InlineData("MySecret^ìfd56486-+{6 ♠ ⌂¿EFE==")] + [InlineData("AsDfJkIl123456")] + [InlineData("123==")] + [InlineData("")] + public void ToProtectedStringToSecureString_SimpleTest(string expected) + { + var secureString = new NetworkCredential(string.Empty, expected).SecurePassword; + var protectedString = secureString.ToProtectedString(); + var secureStringActual = protectedString.ToSecureString(); + var actual = new NetworkCredential(string.Empty, secureStringActual).Password; - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); } } diff --git a/test/Bravo.Tests/Infrastructure/Security/CryptographyTests.cs b/test/Bravo.Tests/Infrastructure/Security/CryptographyTests.cs index 0ce8f1fe..0e392add 100644 --- a/test/Bravo.Tests/Infrastructure/Security/CryptographyTests.cs +++ b/test/Bravo.Tests/Infrastructure/Security/CryptographyTests.cs @@ -1,71 +1,70 @@ -namespace Bravo.Tests.Infrastructure.Security -{ - using Sqlbi.Bravo.Infrastructure.Security; - using System; - using System.Text; - using Xunit; +using System; +using System.Text; +using Sqlbi.Bravo.Infrastructure.Security; +using Xunit; + +namespace Bravo.Tests.Infrastructure.Security; - public class CryptographyTests +public class CryptographyTests +{ + [Fact] + public void ProtectUnprotect_SimpleTest() { - [Fact] - public void ProtectUnprotect_SimpleTest() - { - var expected = "MySecret^ìfd56486-+{6 ♠ ⌂¿EFE=="; + var expected = "MySecret^ìfd56486-+{6 ♠ ⌂¿EFE=="; - var userData = Encoding.Unicode.GetBytes(expected); - var encryptedData = Cryptography.Protect(userData); - var unprotectedData = Cryptography.Unprotect(encryptedData); + var userData = Encoding.Unicode.GetBytes(expected); + var encryptedData = Cryptography.Protect(userData); + var unprotectedData = Cryptography.Unprotect(encryptedData); - var actual = Encoding.Unicode.GetString(unprotectedData); + var actual = Encoding.Unicode.GetString(unprotectedData); - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void MD5Hash_SimpleTest() - { - var buffer = Encoding.UTF8.GetBytes("Bravo"); + [Fact] + public void MD5Hash_SimpleTest() + { + var buffer = Encoding.UTF8.GetBytes("Bravo"); - var actual = Cryptography.MD5Hash(buffer); - var expected = "01A2DA07BF36766155F48FC670D53FE8"; + var actual = Cryptography.MD5Hash(buffer); + var expected = "01A2DA07BF36766155F48FC670D53FE8"; - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Fact] - public void MD5Hash_Simple2DTest() + [Fact] + public void MD5Hash_Simple2DTest() + { + var buffers = new byte[][] { - var buffers = new byte[][] - { - Encoding.UTF8.GetBytes("Bravo"), - BitConverter.GetBytes(255), - }; + Encoding.UTF8.GetBytes("Bravo"), + BitConverter.GetBytes(255), + }; - var actual = Cryptography.MD5Hash(buffers); - var expected = "E9F057A4A3A7B760F6D0C588DF6DAC91"; + var actual = Cryptography.MD5Hash(buffers); + var expected = "E9F057A4A3A7B760F6D0C588DF6DAC91"; - Assert.Equal(expected, actual); - } + Assert.Equal(expected, actual); + } - [Theory] - [InlineData("", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")] - [InlineData("Bravo", "8123f58e72483f148509ae2da7feda62076dbe2ae3a045323bea4458a62d0952")] - [InlineData("LAPTOP-12C9A7VU\\SYSTEM", "e4f87d099028e128ea2b413f4fa6fc741426bef314de588b0920e89f22015bd0")] - public void SHA256Hash_SimpleTest(string input, string expected) - { - var actual = Cryptography.SHA256Hash(input); - Assert.Equal(expected, actual); - } + [Theory] + [InlineData("", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")] + [InlineData("Bravo", "8123f58e72483f148509ae2da7feda62076dbe2ae3a045323bea4458a62d0952")] + [InlineData("LAPTOP-12C9A7VU\\SYSTEM", "e4f87d099028e128ea2b413f4fa6fc741426bef314de588b0920e89f22015bd0")] + public void SHA256Hash_SimpleTest(string input, string expected) + { + var actual = Cryptography.SHA256Hash(input); + Assert.Equal(expected, actual); + } - [Fact] - public void GenerateSimpleToken_SimpleTest() - { - var actual = Cryptography.GenerateSimpleToken(); + [Fact] + public void GenerateSimpleToken_SimpleTest() + { + var actual = Cryptography.GenerateSimpleToken(); - Assert.NotNull(actual); - Assert.NotEmpty(actual); - Assert.Equal(100, actual.Length); - Assert.EndsWith("==", actual); - } + Assert.NotNull(actual); + Assert.NotEmpty(actual); + Assert.Equal(100, actual.Length); + Assert.EndsWith("==", actual); } } diff --git a/test/Bravo.Tests/Infrastructure/Telemetry/DefaultTelemetryProcessorTests.cs b/test/Bravo.Tests/Infrastructure/Telemetry/DefaultTelemetryProcessorTests.cs index dc71087f..76b20ff4 100644 --- a/test/Bravo.Tests/Infrastructure/Telemetry/DefaultTelemetryProcessorTests.cs +++ b/test/Bravo.Tests/Infrastructure/Telemetry/DefaultTelemetryProcessorTests.cs @@ -1,13 +1,13 @@ -namespace Bravo.Tests.Infrastructure.Telemetry; - +using System; +using System.Linq; using Microsoft.ApplicationInsights.DataContracts; using Microsoft.ApplicationInsights.Extensibility; using NSubstitute; using Sqlbi.Bravo.Infrastructure.Telemetry; -using System; -using System.Linq; using Xunit; +namespace Bravo.Tests.Infrastructure.Telemetry; + public class DefaultTelemetryProcessorTests { private readonly ITelemetryProcessor _next;