diff --git a/src/Dataverse.ConfigurationMigrationTool/.editorconfig b/src/Dataverse.ConfigurationMigrationTool/.editorconfig index bb12916..10552b9 100644 --- a/src/Dataverse.ConfigurationMigrationTool/.editorconfig +++ b/src/Dataverse.ConfigurationMigrationTool/.editorconfig @@ -2,3 +2,90 @@ # CS8603: Possible null reference return. dotnet_diagnostic.CS8603.severity = none +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = file_scoped:silent +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_prefer_system_threading_lock = true:suggestion +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false: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 = false:silent +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion + +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +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 + +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 + +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 + +# Symbol specifications + +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 = + +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 = + +# Naming styles + +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 + +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 + +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 +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion +dotnet_style_namespace_match_folder = true:suggestion diff --git a/src/Dataverse.ConfigurationMigrationTool/Console.Tests/CodeCoverage.runsettings b/src/Dataverse.ConfigurationMigrationTool/Console.Tests/CodeCoverage.runsettings index 3032f27..4bfc5f9 100644 --- a/src/Dataverse.ConfigurationMigrationTool/Console.Tests/CodeCoverage.runsettings +++ b/src/Dataverse.ConfigurationMigrationTool/Console.Tests/CodeCoverage.runsettings @@ -5,7 +5,7 @@ cobertura,opencover - [*]Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse* + [*]Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse.Connection.*,[*]Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse.Configuration.* **/Program.cs, true diff --git a/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Features/Import/ImportTaskProcessorServiceTests.cs b/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Features/Import/ImportTaskProcessorServiceTests.cs index dda161a..333607c 100644 --- a/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Features/Import/ImportTaskProcessorServiceTests.cs +++ b/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Features/Import/ImportTaskProcessorServiceTests.cs @@ -1,8 +1,9 @@ using Dataverse.ConfigurationMigrationTool.Console.Features.Import; +using Dataverse.ConfigurationMigrationTool.Console.Features.Import.Interceptors; using Dataverse.ConfigurationMigrationTool.Console.Features.Import.Model; using Dataverse.ConfigurationMigrationTool.Console.Features.Import.ValueConverters; using Dataverse.ConfigurationMigrationTool.Console.Features.Shared; -using Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse; +using Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse.Connection; using Dataverse.ConfigurationMigrationTool.Console.Tests.Extensions; using Dataverse.ConfigurationMigrationTool.Console.Tests.FakeBuilders; using Microsoft.Extensions.Logging; @@ -20,6 +21,7 @@ public class ImportTaskProcessorServiceTests private readonly IDataverseValueConverter _dataverseValueConverter; private readonly IBulkOrganizationService bulkOrganizationService; private readonly ImportTaskProcessorService importService; + private readonly IEntityInterceptor _entityInterceptor = Substitute.For(); public ImportTaskProcessorServiceTests() { @@ -27,7 +29,9 @@ public ImportTaskProcessorServiceTests() this.logger = Substitute.For>(); _dataverseValueConverter = Substitute.For(); this.bulkOrganizationService = Substitute.For(); - this.importService = new ImportTaskProcessorService(metadataService, logger, _dataverseValueConverter, bulkOrganizationService); + this.importService = new ImportTaskProcessorService(metadataService, logger, _dataverseValueConverter, bulkOrganizationService, _entityInterceptor); + _entityInterceptor.InterceptAsync(Arg.Any()) + .Returns(x => x.Arg()); } [Fact] public async Task GivenAnImportTaskWhereEntitySchemaIsNotFound_WhenExecuted_ThenItShouldReturnCompleted() diff --git a/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Features/Import/Interceptors/BaseEntityInterceptorTests.cs b/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Features/Import/Interceptors/BaseEntityInterceptorTests.cs new file mode 100644 index 0000000..4fec664 --- /dev/null +++ b/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Features/Import/Interceptors/BaseEntityInterceptorTests.cs @@ -0,0 +1,46 @@ +using Dataverse.ConfigurationMigrationTool.Console.Features.Import.Interceptors; +using Microsoft.Xrm.Sdk; +using NSubstitute; +using Shouldly; + +namespace Dataverse.ConfigurationMigrationTool.Console.Tests.Features.Import.Interceptors; +public abstract class BaseEntityInterceptorTests + where T : IEntityInterceptor +{ + private IEntityInterceptor Interceptor { get; set; } + protected readonly IEntityInterceptor Successor = Substitute.For(); + protected BaseEntityInterceptorTests() + { + Interceptor = CreateInterceptor(); + Interceptor.SetSuccessor(Successor); + Successor.InterceptAsync(Arg.Any()) + .Returns(x => x.Arg()); + } + protected abstract T CreateInterceptor(); + protected async Task InterceptAsync(Entity entity, bool ShouldSuccessorBeCalled = true) + { + var result = await Interceptor.InterceptAsync(entity); + if (ShouldSuccessorBeCalled) + { + await Successor.Received(1).InterceptAsync(entity); + } + else + { + await Successor.DidNotReceive().InterceptAsync(entity); + } + return result; + } + [Fact] + public async Task GivenAnEntityInterceptorWith_NoSuccessor_ThenItShouldReturnEntity() + { + //Arrange + var interceptor = CreateInterceptor(); + + var entity = new Entity(); + //Act + var result = await interceptor.InterceptAsync(entity); + //Assert + result.ShouldBe(entity); + await Successor.DidNotReceive().InterceptAsync(entity); + } +} diff --git a/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Features/Import/Interceptors/BusinessUnitInterceptorTests.cs b/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Features/Import/Interceptors/BusinessUnitInterceptorTests.cs new file mode 100644 index 0000000..dd65c95 --- /dev/null +++ b/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Features/Import/Interceptors/BusinessUnitInterceptorTests.cs @@ -0,0 +1,89 @@ +using Dataverse.ConfigurationMigrationTool.Console.Features.Import.Interceptors; +using Dataverse.ConfigurationMigrationTool.Console.Features.Shared; +using Microsoft.Xrm.Sdk; +using NSubstitute; +using Shouldly; + +namespace Dataverse.ConfigurationMigrationTool.Console.Tests.Features.Import.Interceptors; +public class BusinessUnitInterceptorTests : BaseEntityInterceptorTests +{ + private readonly IBusinessUnitRepository _businessUnitRepository = Substitute.For(); + + + + + [Fact] + public async Task GivenAnEntityWithBusinessUnitField_WhenItsIntercepted_ThenItShouldResolveBusinessUnitByNameWithRepository() + { + //Arrange + var buName = "Test Business Unit"; + var expectedBu = new Entity("businessunit") + { + Id = Guid.NewGuid(), + ["name"] = buName + }; + _businessUnitRepository.GetByNameAsync(buName).Returns(expectedBu); + + var entity = new Entity("account") + { + Id = Guid.NewGuid(), + ["name"] = "Test Account", + ["businessunitid"] = new EntityReference("businessunit", Guid.NewGuid()) { Name = buName } + }; + + //Act + var result = await InterceptAsync(entity); + //Assert + result.GetAttributeValue("businessunitid").Id.ShouldBe(expectedBu.Id); + } + [Fact] + public async Task GivenAnEntityWithBusinessUnitField_WhenItsIntercepted_ThenItShouldResolveBusinessUnitByIdWithRepository() + { + //Arrange + var buName = "Test Business Unit"; + var expectedBu = new Entity("businessunit") + { + Id = Guid.NewGuid(), + ["name"] = buName + }; + _businessUnitRepository.GetByIdAsync(expectedBu.Id).Returns(expectedBu); + + var entity = new Entity("account") + { + Id = Guid.NewGuid(), + ["name"] = "Test Account", + ["businessunitid"] = expectedBu.ToEntityReference() + }; + + //Act + var result = await InterceptAsync(entity); + //Assert + result.GetAttributeValue("businessunitid").Id.ShouldBe(expectedBu.Id); + } + [Fact] + public async Task GivenAnEntityWithBusinessUnitField_WhenItsInterceptedAndCantBeResolved_ThenItShouldRemoveFieldFromEntity() + { + //Arrange + var buName = "Test Business Unit"; + var expectedBu = new Entity("businessunit") + { + Id = Guid.NewGuid(), + ["name"] = buName + }; + + var entity = new Entity("account") + { + Id = Guid.NewGuid(), + ["name"] = "Test Account", + ["businessunitid"] = expectedBu.ToEntityReference() + }; + + //Act + var result = await InterceptAsync(entity); + //Assert + result.Attributes.FirstOrDefault(kv => kv.Key == "businessunitid").ShouldBe(default); + } + + protected override BusinessUnitInterceptor CreateInterceptor() => new BusinessUnitInterceptor(_businessUnitRepository); + +} diff --git a/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Features/Import/Interceptors/TargetTeamInterceptorTests.cs b/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Features/Import/Interceptors/TargetTeamInterceptorTests.cs new file mode 100644 index 0000000..7670f35 --- /dev/null +++ b/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Features/Import/Interceptors/TargetTeamInterceptorTests.cs @@ -0,0 +1,83 @@ +using Dataverse.ConfigurationMigrationTool.Console.Features.Import.Interceptors; +using Dataverse.ConfigurationMigrationTool.Console.Features.Shared; +using Microsoft.Xrm.Sdk; +using NSubstitute; +using Shouldly; + +namespace Dataverse.ConfigurationMigrationTool.Console.Tests.Features.Import.Interceptors; +public class TargetTeamInterceptorTests : BaseEntityInterceptorTests +{ + private readonly ITeamRepository _teamRepository = Substitute.For(); + protected override TargetTeamInterceptor CreateInterceptor() => new TargetTeamInterceptor(_teamRepository); + [Fact] + public async Task GivenAnEntityWithTeamField_WhenItsIntercepted_ThenItShouldResolveTeamByNameWithRepository() + { + //Arrange + var team = "Test Team"; + var expectedTeam = new Entity("team") + { + Id = Guid.NewGuid(), + ["name"] = team + }; + _teamRepository.GetByNameAsync(team).Returns(expectedTeam); + + var entity = new Entity("account") + { + Id = Guid.NewGuid(), + ["name"] = "Test Account", + ["ownerid"] = new EntityReference("team", Guid.NewGuid()) { Name = team } + }; + + //Act + var result = await InterceptAsync(entity); + //Assert + result.GetAttributeValue("ownerid").Id.ShouldBe(expectedTeam.Id); + } + [Fact] + public async Task GivenAnEntityWithTeamField_WhenItsIntercepted_ThenItShouldResolveTeamByIdWithRepository() + { + //Arrange + var teamName = "Test team"; + var expectedTeam = new Entity("team") + { + Id = Guid.NewGuid(), + ["name"] = teamName + }; + _teamRepository.GetByIdAsync(expectedTeam.Id).Returns(expectedTeam); + + var entity = new Entity("account") + { + Id = Guid.NewGuid(), + ["name"] = "Test Account", + ["ownerid"] = expectedTeam.ToEntityReference() + }; + + //Act + var result = await InterceptAsync(entity); + //Assert + result.GetAttributeValue("ownerid").Id.ShouldBe(expectedTeam.Id); + } + [Fact] + public async Task GivenAnEntityWithTeamField_WhenItsInterceptedAndCantBeResolved_ThenItShouldRemoveFieldFromEntity() + { + //Arrange + var teamName = "Test team"; + var expectedTeam = new Entity("team") + { + Id = Guid.NewGuid(), + ["name"] = teamName + }; + + var entity = new Entity("account") + { + Id = Guid.NewGuid(), + ["name"] = "Test Account", + ["ownerid"] = expectedTeam.ToEntityReference() + }; + + //Act + var result = await InterceptAsync(entity); + //Assert + result.Attributes.FirstOrDefault(kv => kv.Key == "ownerid").ShouldBe(default); + } +} diff --git a/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Features/Import/Interceptors/TargetUserInterceptorTests.cs b/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Features/Import/Interceptors/TargetUserInterceptorTests.cs new file mode 100644 index 0000000..036167f --- /dev/null +++ b/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Features/Import/Interceptors/TargetUserInterceptorTests.cs @@ -0,0 +1,85 @@ +using Dataverse.ConfigurationMigrationTool.Console.Features.Import.Interceptors; +using Dataverse.ConfigurationMigrationTool.Console.Features.Shared; +using Microsoft.Xrm.Sdk; +using NSubstitute; +using Shouldly; + +namespace Dataverse.ConfigurationMigrationTool.Console.Tests.Features.Import.Interceptors; +public class TargetUserInterceptorTests : BaseEntityInterceptorTests +{ + private readonly ISystemUserRepository systemUserRepository = Substitute.For(); + protected override TargetUserInterceptor CreateInterceptor() => new TargetUserInterceptor(systemUserRepository); + + + [Fact] + public async Task GivenAnEntityWithUserField_WhenItsIntercepted_ThenItShouldResolveUserByNameWithRepository() + { + //Arrange + var userFullName = "Test user"; + var expectedUser = new Entity("systemuser") + { + Id = Guid.NewGuid(), + ["fullname"] = userFullName + }; + systemUserRepository.GetByName(userFullName).Returns(expectedUser); + + var entity = new Entity("account") + { + Id = Guid.NewGuid(), + ["name"] = "Test Account", + ["ownerid"] = new EntityReference("systemuser", Guid.NewGuid()) { Name = userFullName } + }; + + //Act + var result = await InterceptAsync(entity); + //Assert + result.GetAttributeValue("ownerid").Id.ShouldBe(expectedUser.Id); + } + [Fact] + public async Task GivenAnEntityWithUserField_WhenItsIntercepted_ThenItShouldResolveUserByIdWithRepository() + { + //Arrange + var userFullname = "Test User"; + var expectedUser = new Entity("systemuser") + { + Id = Guid.NewGuid(), + ["fullname"] = userFullname + }; + systemUserRepository.GetByIdAsync(expectedUser.Id).Returns(expectedUser); + + var entity = new Entity("account") + { + Id = Guid.NewGuid(), + ["name"] = "Test Account", + ["ownerid"] = expectedUser.ToEntityReference() + }; + + //Act + var result = await InterceptAsync(entity); + //Assert + result.GetAttributeValue("ownerid").Id.ShouldBe(expectedUser.Id); + } + [Fact] + public async Task GivenAnEntityWithUserField_WhenItsInterceptedAndCantBeResolved_ThenItShouldRemoveFieldFromEntity() + { + //Arrange + var userFullname = "Test User"; + var expectedUser = new Entity("systemuser") + { + Id = Guid.NewGuid(), + ["fullname"] = userFullname + }; + + var entity = new Entity("account") + { + Id = Guid.NewGuid(), + ["name"] = "Test Account", + ["ownerid"] = expectedUser.ToEntityReference() + }; + + //Act + var result = await InterceptAsync(entity); + //Assert + result.Attributes.FirstOrDefault(kv => kv.Key == "ownerid").ShouldBe(default); + } +} diff --git a/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Services/Dataverse/DataverseBusinessUnitRepositoryTests.cs b/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Services/Dataverse/DataverseBusinessUnitRepositoryTests.cs new file mode 100644 index 0000000..c62ef6d --- /dev/null +++ b/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Services/Dataverse/DataverseBusinessUnitRepositoryTests.cs @@ -0,0 +1,78 @@ +using Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse; +using FakeXrmEasy.Abstractions; +using FakeXrmEasy.Abstractions.Enums; +using FakeXrmEasy.Middleware; +using FakeXrmEasy.Middleware.Crud; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Xrm.Sdk; +using NSubstitute; +using Shouldly; + +namespace Dataverse.ConfigurationMigrationTool.Console.Tests.Services.Dataverse; +public class DataverseBusinessUnitRepositoryTests +{ + private readonly IXrmFakedContext _fakedContext; + private readonly IMemoryCache _memoryCache = Substitute.For(); + private readonly DataverseBusinessUnitRepository _repository; + public DataverseBusinessUnitRepositoryTests() + { + // Arrange + _fakedContext = MiddlewareBuilder.New() + .SetLicense(FakeXrmEasyLicense.NonCommercial) + .AddCrud() + .UseCrud() + .Build(); + _repository = new DataverseBusinessUnitRepository(_memoryCache, _fakedContext.GetAsyncOrganizationService2()); + } + [Fact] + public async Task GivenABusinessUnitRepository_WhenItFetchesByName_ThenItShouldCacheItsResult() + { + //Arrange + var buName = "Test Business Unit"; + var cacheKey = $"bu.GetByNameAsync.{buName}"; + var bu = new Entity("businessunit") + { + Id = Guid.NewGuid(), + ["name"] = buName + }; + _fakedContext.Initialize(bu); + var CacheEntry = Substitute.For(); + _memoryCache.TryGetValue(cacheKey, out Arg.Any()).Returns(false); + _memoryCache.CreateEntry(cacheKey).Returns(CacheEntry); + + //Act + + var result = await _repository.GetByNameAsync(buName); + + //Assert + result.Id.ShouldBe(bu.Id); + CacheEntry.Received().SetValue(Arg.Is(e => e.Id == bu.Id)); + + } + [Fact] + public async Task GivenABusinessUnitRepository_WhenItFetchesById_ThenItShouldCacheItsResult() + { + //Arrange + var buName = "Test Business Unit"; + + var bu = new Entity("businessunit") + { + Id = Guid.NewGuid(), + ["name"] = buName + }; + var cacheKey = $"bu.GetByIdAsync.{bu.Id}"; + _fakedContext.Initialize(bu); + var CacheEntry = Substitute.For(); + _memoryCache.TryGetValue(cacheKey, out Arg.Any()).Returns(false); + _memoryCache.CreateEntry(cacheKey).Returns(CacheEntry); + + //Act + + var result = await _repository.GetByIdAsync(bu.Id); + + //Assert + result.Id.ShouldBe(bu.Id); + CacheEntry.Received().SetValue(Arg.Is(e => e.Id == bu.Id)); + + } +} diff --git a/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Services/Dataverse/DataverseMetadataServiceTests.cs b/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Services/Dataverse/DataverseMetadataServiceTests.cs deleted file mode 100644 index e69de29..0000000 diff --git a/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Services/Dataverse/DataverseTeamRepositoryTests.cs b/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Services/Dataverse/DataverseTeamRepositoryTests.cs new file mode 100644 index 0000000..89e4202 --- /dev/null +++ b/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Services/Dataverse/DataverseTeamRepositoryTests.cs @@ -0,0 +1,78 @@ +using Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse; +using FakeXrmEasy.Abstractions; +using FakeXrmEasy.Abstractions.Enums; +using FakeXrmEasy.Middleware; +using FakeXrmEasy.Middleware.Crud; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Xrm.Sdk; +using NSubstitute; +using Shouldly; + +namespace Dataverse.ConfigurationMigrationTool.Console.Tests.Services.Dataverse; +public class DataverseTeamRepositoryTests +{ + private readonly IXrmFakedContext _fakedContext; + private readonly IMemoryCache _memoryCache = Substitute.For(); + private readonly DataverseTeamRepository _repository; + public DataverseTeamRepositoryTests() + { + // Arrange + _fakedContext = MiddlewareBuilder.New() + .SetLicense(FakeXrmEasyLicense.NonCommercial) + .AddCrud() + .UseCrud() + .Build(); + _repository = new DataverseTeamRepository(_fakedContext.GetAsyncOrganizationService2(), _memoryCache); + } + [Fact] + public async Task GivenATeamRepository_WhenItFetchesByName_ThenItShouldCacheItsResult() + { + //Arrange + var recordname = "Test Team"; + var cacheKey = $"team.GetByNameAsync.{recordname}"; + var team = new Entity("team") + { + Id = Guid.NewGuid(), + ["name"] = recordname + }; + _fakedContext.Initialize(team); + var CacheEntry = Substitute.For(); + _memoryCache.TryGetValue(cacheKey, out Arg.Any()).Returns(false); + _memoryCache.CreateEntry(cacheKey).Returns(CacheEntry); + + //Act + + var result = await _repository.GetByNameAsync(recordname); + + //Assert + result.Id.ShouldBe(team.Id); + CacheEntry.Received().SetValue(Arg.Is(e => e.Id == team.Id)); + + } + [Fact] + public async Task GivenATeamRepository_WhenItFetchesById_ThenItShouldCacheItsResult() + { + //Arrange + var recordname = "Test Team"; + + var team = new Entity("team") + { + Id = Guid.NewGuid(), + ["name"] = recordname + }; + var cacheKey = $"team.GetByIdAsync.{team.Id}"; + _fakedContext.Initialize(team); + var CacheEntry = Substitute.For(); + _memoryCache.TryGetValue(cacheKey, out Arg.Any()).Returns(false); + _memoryCache.CreateEntry(cacheKey).Returns(CacheEntry); + + //Act + + var result = await _repository.GetByIdAsync(team.Id); + + //Assert + result.Id.ShouldBe(team.Id); + CacheEntry.Received().SetValue(Arg.Is(e => e.Id == team.Id)); + + } +} diff --git a/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Services/Dataverse/DataverseUserRepositoryTests.cs b/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Services/Dataverse/DataverseUserRepositoryTests.cs new file mode 100644 index 0000000..debd881 --- /dev/null +++ b/src/Dataverse.ConfigurationMigrationTool/Console.Tests/Services/Dataverse/DataverseUserRepositoryTests.cs @@ -0,0 +1,78 @@ +using Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse; +using FakeXrmEasy.Abstractions; +using FakeXrmEasy.Abstractions.Enums; +using FakeXrmEasy.Middleware; +using FakeXrmEasy.Middleware.Crud; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Xrm.Sdk; +using NSubstitute; +using Shouldly; + +namespace Dataverse.ConfigurationMigrationTool.Console.Tests.Services.Dataverse; +public class DataverseUserRepositoryTests +{ + private readonly IXrmFakedContext _fakedContext; + private readonly IMemoryCache _memoryCache = Substitute.For(); + private readonly DataverseUserRepository _repository; + public DataverseUserRepositoryTests() + { + // Arrange + _fakedContext = MiddlewareBuilder.New() + .SetLicense(FakeXrmEasyLicense.NonCommercial) + .AddCrud() + .UseCrud() + .Build(); + _repository = new DataverseUserRepository(_fakedContext.GetAsyncOrganizationService2(), _memoryCache); + } + [Fact] + public async Task GivenATeamRepository_WhenItFetchesByName_ThenItShouldCacheItsResult() + { + //Arrange + var recordname = "Test User"; + var cacheKey = $"user.GetByName.{recordname}"; + var user = new Entity("systemuser") + { + Id = Guid.NewGuid(), + ["fullname"] = recordname + }; + _fakedContext.Initialize(user); + var CacheEntry = Substitute.For(); + _memoryCache.TryGetValue(cacheKey, out Arg.Any()).Returns(false); + _memoryCache.CreateEntry(cacheKey).Returns(CacheEntry); + + //Act + + var result = await _repository.GetByName(recordname); + + //Assert + result.Id.ShouldBe(user.Id); + CacheEntry.Received().SetValue(Arg.Is(e => e.Id == user.Id)); + + } + [Fact] + public async Task GivenATeamRepository_WhenItFetchesById_ThenItShouldCacheItsResult() + { + //Arrange + var recordname = "Test User"; + + var user = new Entity("systemuser") + { + Id = Guid.NewGuid(), + ["fullname"] = recordname + }; + var cacheKey = $"user.GetByIdAsync.{user.Id}"; + _fakedContext.Initialize(user); + var CacheEntry = Substitute.For(); + _memoryCache.TryGetValue(cacheKey, out Arg.Any()).Returns(false); + _memoryCache.CreateEntry(cacheKey).Returns(CacheEntry); + + //Act + + var result = await _repository.GetByIdAsync(user.Id); + + //Assert + result.Id.ShouldBe(user.Id); + CacheEntry.Received().SetValue(Arg.Is(e => e.Id == user.Id)); + + } +} diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/IServiceCollectionExtensions.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/IServiceCollectionExtensions.cs index e91770b..c5fc540 100644 --- a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/IServiceCollectionExtensions.cs +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/IServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using Dataverse.ConfigurationMigrationTool.Console.Features.Import.Commands; +using Dataverse.ConfigurationMigrationTool.Console.Features.Import.Interceptors; using Dataverse.ConfigurationMigrationTool.Console.Features.Import.Model; using Dataverse.ConfigurationMigrationTool.Console.Features.Import.Validators; using Dataverse.ConfigurationMigrationTool.Console.Features.Import.Validators.Rules.EntitySchemas; @@ -30,9 +31,23 @@ public static IServiceCollection AddImportFeature(this IServiceCollection servic .AddTransient, SchemaValidator>() .AddTransient, EntitySchemaValidator>() .AddSingleton() + .AddSingleton((sp) => + { + var BusinessUnitInterceptor = sp.BuildService(); + var UserInterceptor = sp.BuildService(); + var TeamInterceptor = sp.BuildService(); + BusinessUnitInterceptor.SetSuccessor(UserInterceptor) + .SetSuccessor(TeamInterceptor); + return BusinessUnitInterceptor; + }) .Configure(Configuration); } + public static T BuildService(this IServiceProvider serviceProvider) where T : class + { + + return ActivatorUtilities.CreateInstance(serviceProvider); + } public static IServiceCollection UseCommands(this IServiceCollection services, params string[] args) { services.AddSingleton>(Options.Create(new CommandProcessorHostingServiceOptions() { CommandVerb = args[0] })); diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/ImportTaskProcessorService.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/ImportTaskProcessorService.cs index 5f0b9dc..887b135 100644 --- a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/ImportTaskProcessorService.cs +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/ImportTaskProcessorService.cs @@ -1,7 +1,8 @@ -using Dataverse.ConfigurationMigrationTool.Console.Features.Import.Model; +using Dataverse.ConfigurationMigrationTool.Console.Features.Import.Interceptors; +using Dataverse.ConfigurationMigrationTool.Console.Features.Import.Model; using Dataverse.ConfigurationMigrationTool.Console.Features.Import.ValueConverters; using Dataverse.ConfigurationMigrationTool.Console.Features.Shared; -using Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse; +using Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse.Connection; using Microsoft.Extensions.Logging; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Messages; @@ -25,16 +26,18 @@ public class ImportTaskProcessorService : IImportTaskProcessorService private readonly ILogger logger; private readonly IDataverseValueConverter _dataverseValueConverter; private readonly IBulkOrganizationService bulkOrganizationService; - const int MAX_RETRIES = 3; + private readonly IEntityInterceptor _entityInterceptor; public ImportTaskProcessorService(IMetadataService metadataService, ILogger logger, IDataverseValueConverter dataverseValueConverter, - IBulkOrganizationService bulkOrganizationService) + IBulkOrganizationService bulkOrganizationService, + IEntityInterceptor entityInterceptor) { this.metadataService = metadataService; this.logger = logger; _dataverseValueConverter = dataverseValueConverter; this.bulkOrganizationService = bulkOrganizationService; + _entityInterceptor = entityInterceptor; } public async Task Execute(ImportDataTask task, Entities dataImport) @@ -120,9 +123,14 @@ private async Task> ProcessDepend var results = new List(); var recordsCanBeProcessed = records.Where(r => !r.Field.Any(f => f.Lookupentity == entityImport.Name && - records.Any(r2 => r2.Id != r.Id && r2.Id.ToString() == f.Value))).Select(r => BuildUpsertRequest(entity, entityImport, r)).ToList(); - logger.LogInformation("Processing {count} records", recordsCanBeProcessed.Count); - if (recordsCanBeProcessed.Count == 0) + records.Any(r2 => r2.Id != r.Id && r2.Id.ToString() == f.Value))).ToList(); + var requests = new List(); + foreach (var record in recordsCanBeProcessed) + { + requests.Add(await BuildUpsertRequest(entity, entityImport, record)); + } + logger.LogInformation("Processing {count} records", requests.Count); + if (requests.Count == 0) { if (records.Any()) { @@ -130,14 +138,14 @@ private async Task> ProcessDepend } return results; } - var responses = await bulkOrganizationService.UpsertBulk(recordsCanBeProcessed); + var responses = await bulkOrganizationService.UpsertBulk(requests); results.AddRange(responses); - responses = await ProcessDependantRecords(records.Where(r => !recordsCanBeProcessed.Any(r2 => r.Id == r2.Target.Id)), entity, entityImport); + responses = await ProcessDependantRecords(records.Where(r => !requests.Any(r2 => r.Id == r2.Target.Id)), entity, entityImport); results.AddRange(responses); return results; } - private UpsertRequest BuildUpsertRequest(EntityMetadata entityMD, EntityImport entityImport, Record record) + private async Task BuildUpsertRequest(EntityMetadata entityMD, EntityImport entityImport, Record record) { var entity = new Entity(entityImport.Name, record.Id); entity[entityMD.PrimaryIdAttribute] = record.Id; @@ -145,14 +153,13 @@ private UpsertRequest BuildUpsertRequest(EntityMetadata entityMD, EntityImport e { var attrMD = entityMD.Attributes.FirstOrDefault(a => a.LogicalName == field.Name); if ((attrMD.IsValidForCreate != true && attrMD.LogicalName != "statecode") - || attrMD.IsValidForUpdate != true || - attrMD.AttributeType == AttributeTypeCode.Owner) continue; + || attrMD.IsValidForUpdate != true) continue; entity[field.Name] = _dataverseValueConverter.Convert(attrMD, field); } return new UpsertRequest { - Target = entity + Target = await _entityInterceptor.InterceptAsync(entity) }; } diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/Interceptors/BaseEntityInterceptor.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/Interceptors/BaseEntityInterceptor.cs new file mode 100644 index 0000000..d1317a0 --- /dev/null +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/Interceptors/BaseEntityInterceptor.cs @@ -0,0 +1,21 @@ +using Microsoft.Xrm.Sdk; + +namespace Dataverse.ConfigurationMigrationTool.Console.Features.Import.Interceptors; +public abstract class BaseEntityInterceptor : IEntityInterceptor +{ + private IEntityInterceptor Successor { get; set; } + public async virtual Task InterceptAsync(Entity entity) + { + if (Successor == null) + { + return entity; + } + return await Successor.InterceptAsync(entity); + } + + public IEntityInterceptor SetSuccessor(IEntityInterceptor successor) + { + this.Successor = successor; + return this.Successor; + } +} diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/Interceptors/BusinessUnitInterceptor.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/Interceptors/BusinessUnitInterceptor.cs new file mode 100644 index 0000000..48cd998 --- /dev/null +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/Interceptors/BusinessUnitInterceptor.cs @@ -0,0 +1,38 @@ +using Dataverse.ConfigurationMigrationTool.Console.Features.Shared; +using Microsoft.Xrm.Sdk; + +namespace Dataverse.ConfigurationMigrationTool.Console.Features.Import.Interceptors; +public class BusinessUnitInterceptor : BaseEntityInterceptor +{ + private readonly IBusinessUnitRepository _businessUnitRepository; + public BusinessUnitInterceptor(IBusinessUnitRepository businessUnitRepository) + { + _businessUnitRepository = businessUnitRepository; + } + public override async Task InterceptAsync(Entity entity) + { + var attributes = entity.Attributes.Where(kv => kv.Value is EntityReference ef && ef.LogicalName == "businessunit").ToList(); + foreach (var attribute in attributes) + { + var reference = attribute.Value as EntityReference; + var resolvedBu = await _businessUnitRepository.GetByIdAsync(reference.Id); + if (resolvedBu != null) + { + continue; + + } + resolvedBu = await _businessUnitRepository.GetByNameAsync(reference.Name); + if (resolvedBu != null) + { + entity[attribute.Key] = new EntityReference("businessunit", resolvedBu.Id); + + } + else + { + entity.Attributes.Remove(attribute.Key); + } + } + + return await base.InterceptAsync(entity); + } +} diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/Interceptors/IEntityInterceptor.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/Interceptors/IEntityInterceptor.cs new file mode 100644 index 0000000..f45a4d1 --- /dev/null +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/Interceptors/IEntityInterceptor.cs @@ -0,0 +1,18 @@ +using Microsoft.Xrm.Sdk; + +namespace Dataverse.ConfigurationMigrationTool.Console.Features.Import.Interceptors; +public interface IEntityInterceptor +{ + /// + /// Intercepts the entity before it is processed. + /// + /// The entity to intercept. + /// The intercepted entity. + Task InterceptAsync(Entity entity); + /// + /// Intercepts the entity after it has been processed. + /// + /// The entity to intercept. + /// The intercepted entity. + IEntityInterceptor SetSuccessor(IEntityInterceptor successor); +} diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/Interceptors/TargetTeamInterceptor.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/Interceptors/TargetTeamInterceptor.cs new file mode 100644 index 0000000..09b18f5 --- /dev/null +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/Interceptors/TargetTeamInterceptor.cs @@ -0,0 +1,37 @@ +using Dataverse.ConfigurationMigrationTool.Console.Features.Shared; +using Microsoft.Xrm.Sdk; + +namespace Dataverse.ConfigurationMigrationTool.Console.Features.Import.Interceptors; +public class TargetTeamInterceptor : BaseEntityInterceptor +{ + private readonly ITeamRepository _teamRepository; + public TargetTeamInterceptor(ITeamRepository teamRepository) + { + _teamRepository = teamRepository; + } + public override async Task InterceptAsync(Entity entity) + { + var attributes = entity.Attributes.Where(kv => kv.Value is EntityReference ef && ef.LogicalName == "team").ToList(); + foreach (var attribute in attributes) + { + var reference = attribute.Value as EntityReference; + var resolvedTeam = await _teamRepository.GetByIdAsync(reference.Id); + if (resolvedTeam != null) + { + continue; + + } + resolvedTeam = await _teamRepository.GetByNameAsync(reference.Name); + if (resolvedTeam != null) + { + entity[attribute.Key] = new EntityReference("team", resolvedTeam.Id); + + } + else + { + entity.Attributes.Remove(attribute.Key); + } + } + return await base.InterceptAsync(entity); + } +} diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/Interceptors/TargetUserInterceptor.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/Interceptors/TargetUserInterceptor.cs new file mode 100644 index 0000000..5630aef --- /dev/null +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/Interceptors/TargetUserInterceptor.cs @@ -0,0 +1,37 @@ +using Dataverse.ConfigurationMigrationTool.Console.Features.Shared; +using Microsoft.Xrm.Sdk; + +namespace Dataverse.ConfigurationMigrationTool.Console.Features.Import.Interceptors; +public class TargetUserInterceptor : BaseEntityInterceptor +{ + private readonly ISystemUserRepository _systemUserRepository; + public TargetUserInterceptor(ISystemUserRepository systemUserRepository) + { + _systemUserRepository = systemUserRepository; + } + public override async Task InterceptAsync(Entity entity) + { + var attributes = entity.Attributes.Where(kv => kv.Value is EntityReference ef && ef.LogicalName == "systemuser").ToList(); + foreach (var attribute in attributes) + { + var reference = attribute.Value as EntityReference; + var resolvedUser = await _systemUserRepository.GetByIdAsync(reference.Id); + if (resolvedUser != null) + { + continue; + + } + resolvedUser = await _systemUserRepository.GetByName(reference.Name); + if (resolvedUser != null) + { + entity[attribute.Key] = new EntityReference("systemuser", resolvedUser.Id); + + } + else + { + entity.Attributes.Remove(attribute.Key); + } + } + return await base.InterceptAsync(entity); + } +} diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/ValueConverters/DataverseValueConverter.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/ValueConverters/DataverseValueConverter.cs index d326f5e..97389aa 100644 --- a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/ValueConverters/DataverseValueConverter.cs +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/ValueConverters/DataverseValueConverter.cs @@ -47,9 +47,11 @@ public object Convert(AttributeMetadata attributeMetadata, Field field) return _mainConverter.Convert(value); case AttributeTypeCode.Customer: case AttributeTypeCode.Lookup: + case AttributeTypeCode.Owner: var properties = new Dictionary() { - ["lookuptype"] = field.Lookupentity + ["lookuptype"] = field.Lookupentity, + ["lookupentityname"] = field.Lookupentityname }; return _mainConverter.Convert(value, properties); default: diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/ValueConverters/EntityReferenceValueConverter.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/ValueConverters/EntityReferenceValueConverter.cs index a9722df..966337c 100644 --- a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/ValueConverters/EntityReferenceValueConverter.cs +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Import/ValueConverters/EntityReferenceValueConverter.cs @@ -1,23 +1,23 @@ using Microsoft.Xrm.Sdk; -namespace Dataverse.ConfigurationMigrationTool.Console.Features.Import.ValueConverters +namespace Dataverse.ConfigurationMigrationTool.Console.Features.Import.ValueConverters; + +public class EntityReferenceValueConverter : BaseValueConverter { - public class EntityReferenceValueConverter : BaseValueConverter + const string LookupProperty = "lookuptype"; + const string LookupName = "lookupentityname"; + protected override EntityReference ConvertValue(string value, Dictionary ExtraProperties) { - const string LookupProperty = "lookuptype"; - protected override EntityReference ConvertValue(string value, Dictionary ExtraProperties) + var lookupType = (ExtraProperties?.ContainsKey(LookupProperty) ?? false) ? ExtraProperties[LookupProperty] : string.Empty; + var lookupName = (ExtraProperties?.ContainsKey(LookupName) ?? false) ? ExtraProperties[LookupName] : string.Empty; + if (string.IsNullOrEmpty(lookupType)) { return null; } + if (Guid.TryParse(value, out var id)) { - - var lookupType = (ExtraProperties?.ContainsKey(LookupProperty) ?? false) ? ExtraProperties[LookupProperty] : string.Empty; - if (string.IsNullOrEmpty(lookupType)) { return null; } - if (Guid.TryParse(value, out var id)) - { - return new EntityReference(lookupType, id); - } + return new EntityReference(lookupType, id) { Name = lookupName }; + } - return null; - } + return null; } } diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Shared/IBusinessUnitRepository.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Shared/IBusinessUnitRepository.cs new file mode 100644 index 0000000..7a8c2c7 --- /dev/null +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Shared/IBusinessUnitRepository.cs @@ -0,0 +1,8 @@ +using Microsoft.Xrm.Sdk; + +namespace Dataverse.ConfigurationMigrationTool.Console.Features.Shared; +public interface IBusinessUnitRepository +{ + Task GetByNameAsync(string businessUnitName); + Task GetByIdAsync(Guid Id); +} diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Shared/ISystemUserRepository.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Shared/ISystemUserRepository.cs new file mode 100644 index 0000000..2fff309 --- /dev/null +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Shared/ISystemUserRepository.cs @@ -0,0 +1,8 @@ +using Microsoft.Xrm.Sdk; + +namespace Dataverse.ConfigurationMigrationTool.Console.Features.Shared; +public interface ISystemUserRepository +{ + Task GetByName(string fullname); + Task GetByIdAsync(Guid Id); +} diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Shared/ITeamRepository.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Shared/ITeamRepository.cs new file mode 100644 index 0000000..f6b83ad --- /dev/null +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Features/Shared/ITeamRepository.cs @@ -0,0 +1,8 @@ +using Microsoft.Xrm.Sdk; + +namespace Dataverse.ConfigurationMigrationTool.Console.Features.Shared; +public interface ITeamRepository +{ + Task GetByNameAsync(string teamName); + Task GetByIdAsync(Guid Id); +} diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Program.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Program.cs index a3659b2..1fcebc1 100644 --- a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Program.cs +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Program.cs @@ -2,6 +2,7 @@ using Dataverse.ConfigurationMigrationTool.Console.Features.Shared; using Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse; using Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse.Configuration; +using Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse.Connection; using Dataverse.ConfigurationMigrationTool.Console.Services.Filesystem; using Dataverse.ConfigurationMigrationTool.Console.Services.Metadata; using Microsoft.Extensions.Configuration; @@ -49,6 +50,10 @@ .AddSingleton() .AddDataverseClient() .UseCommands(args) + .AddMemoryCache() + .AddTransient() + .AddTransient() + .AddTransient() .AddImportFeature(context.Configuration); // Configure other services. }); diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/Connection/DataverseServiceProviderExtensions.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/Connection/DataverseServiceProviderExtensions.cs new file mode 100644 index 0000000..bafaed4 --- /dev/null +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/Connection/DataverseServiceProviderExtensions.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.PowerPlatform.Dataverse.Client; + +namespace Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse.Connection; + +public static class DataverseServiceProviderExtensions +{ + public static IServiceCollection AddDataverseClient(this IServiceCollection services, ServiceLifetime clientLifetime = ServiceLifetime.Transient) + { + var serviceItem = new ServiceDescriptor(typeof(IOrganizationServiceAsync2), + (sp) => + { + var factory = sp.GetRequiredService(); + return factory.Create(); + }, clientLifetime); + services.Add(serviceItem); + return services.AddSingleton(); + } + + +} diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/IBulkOrganizationService.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/Connection/IBulkOrganizationService.cs similarity index 96% rename from src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/IBulkOrganizationService.cs rename to src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/Connection/IBulkOrganizationService.cs index 1d6f7b0..d88e774 100644 --- a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/IBulkOrganizationService.cs +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/Connection/IBulkOrganizationService.cs @@ -2,7 +2,7 @@ using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Messages; -namespace Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse +namespace Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse.Connection { public interface IBulkOrganizationService { diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/IDataverseClientFactory.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/Connection/IDataverseClientFactory.cs similarity index 90% rename from src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/IDataverseClientFactory.cs rename to src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/Connection/IDataverseClientFactory.cs index d13e277..4cf8c4c 100644 --- a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/IDataverseClientFactory.cs +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/Connection/IDataverseClientFactory.cs @@ -1,6 +1,6 @@ using Microsoft.PowerPlatform.Dataverse.Client; -namespace Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse; +namespace Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse.Connection; public interface IDataverseClientFactory { diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/ParallelismBulkOrganizationService.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/Connection/ParallelismBulkOrganizationService.cs similarity index 96% rename from src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/ParallelismBulkOrganizationService.cs rename to src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/Connection/ParallelismBulkOrganizationService.cs index 9504bc4..76098b6 100644 --- a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/ParallelismBulkOrganizationService.cs +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/Connection/ParallelismBulkOrganizationService.cs @@ -9,7 +9,7 @@ using System.Diagnostics; using System.ServiceModel; -namespace Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse +namespace Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse.Connection { public class OrganizationResponseFaultedResult { @@ -62,7 +62,7 @@ await Parallel.ForEachAsync( } }; logger.LogInformation("Starting a batch of {count} requests", requests.Count); - var response = (await scopedService.ExecuteAsync(request)) as ExecuteMultipleResponse; + var response = await scopedService.ExecuteAsync(request) as ExecuteMultipleResponse; var faultedResponses = response.Responses .Where(r => r.Fault != null && (faultToSkips?.All(f => !r.Fault.Message.Contains(f)) ?? true)) .Select(fr => new OrganizationResponseFaultedResult { Fault = fr.Fault, OriginalRequest = requests[fr.RequestIndex] }).ToArray(); @@ -102,7 +102,7 @@ public async Task> Ups } try { - var response = (await _serviceClient.ExecuteAsync(request)) as UpsertResponse; + var response = await _serviceClient.ExecuteAsync(request) as UpsertResponse; if (setStateRquest.Target != null) { setStateRquest.Target.LogicalName = response.Target.LogicalName; @@ -157,7 +157,7 @@ await Parallel.ForEachAsync(source: batches, parallelOptions: parallelOptions, } try { - var response = (await scopedService.ExecuteAsync(request)) as UpsertResponse; + var response = await scopedService.ExecuteAsync(request) as UpsertResponse; if (setStateRquest.Target != null) { setStateRquest.Target.LogicalName = response.Target.LogicalName; diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/SdkDataverseServiceFactory.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/Connection/SdkDataverseServiceFactory.cs similarity index 95% rename from src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/SdkDataverseServiceFactory.cs rename to src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/Connection/SdkDataverseServiceFactory.cs index 50f07f4..042b79e 100644 --- a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/SdkDataverseServiceFactory.cs +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/Connection/SdkDataverseServiceFactory.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Options; using Microsoft.PowerPlatform.Dataverse.Client; -namespace Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse; +namespace Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse.Connection; public class SdkDataverseServiceFactory : IDataverseClientFactory { @@ -13,7 +13,7 @@ public class SdkDataverseServiceFactory : IDataverseClientFactory public SdkDataverseServiceFactory(IOptions options, ILogger logger) { - this._options = options.Value; + _options = options.Value; _logger = logger; } public IOrganizationServiceAsync2 Create() diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/DataverseBusinessUnitRepository.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/DataverseBusinessUnitRepository.cs new file mode 100644 index 0000000..8cc9342 --- /dev/null +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/DataverseBusinessUnitRepository.cs @@ -0,0 +1,58 @@ +using Dataverse.ConfigurationMigrationTool.Console.Features.Shared; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse; +public class DataverseBusinessUnitRepository : IBusinessUnitRepository +{ + private readonly IOrganizationServiceAsync2 _orgService; + private readonly IMemoryCache _memoryCache; + + public DataverseBusinessUnitRepository(IMemoryCache memoryCache, IOrganizationServiceAsync2 orgService) + { + _memoryCache = memoryCache; + _orgService = orgService; + } + + public async Task GetByIdAsync(Guid Id) + { + return await _memoryCache.GetOrCreateAsync($"bu.GetByIdAsync.{Id}", async (_) => + { + var query = new QueryExpression("businessunit") + { + ColumnSet = new ColumnSet("businessunitid", "name"), + Criteria = new FilterExpression + { + Conditions = + { + new ConditionExpression("businessunitid", ConditionOperator.Equal, Id) + } + } + }; + var result = await _orgService.RetrieveMultipleAsync(query); + return result.Entities.FirstOrDefault(); + }); + } + + public async Task GetByNameAsync(string businessUnitName) + { + return await _memoryCache.GetOrCreateAsync($"bu.GetByNameAsync.{businessUnitName}", async (_) => + { + var query = new QueryExpression("businessunit") + { + ColumnSet = new ColumnSet("businessunitid", "name"), + Criteria = new FilterExpression + { + Conditions = + { + new ConditionExpression("name", ConditionOperator.Equal, businessUnitName) + } + } + }; + var result = await _orgService.RetrieveMultipleAsync(query); + return result.Entities.FirstOrDefault(); + }); + } +} diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/DataverseServiceProviderExtensions.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/DataverseServiceProviderExtensions.cs deleted file mode 100644 index cfb3760..0000000 --- a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/DataverseServiceProviderExtensions.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; -using Microsoft.PowerPlatform.Dataverse.Client; - -namespace Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse -{ - public static class DataverseServiceProviderExtensions - { - public static IServiceCollection AddDataverseClient(this IServiceCollection services, ServiceLifetime clientLifetime = ServiceLifetime.Transient) - { - var serviceItem = new ServiceDescriptor(typeof(IOrganizationServiceAsync2), - (sp) => - { - var factory = sp.GetRequiredService(); - return factory.Create(); - }, clientLifetime); - services.Add(serviceItem); - return services.AddSingleton(); - } - - - } -} diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/DataverseTeamRepository.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/DataverseTeamRepository.cs new file mode 100644 index 0000000..ca66610 --- /dev/null +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/DataverseTeamRepository.cs @@ -0,0 +1,57 @@ +using Dataverse.ConfigurationMigrationTool.Console.Features.Shared; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse; +public class DataverseTeamRepository : ITeamRepository +{ + private readonly IOrganizationServiceAsync2 _orgService; + private readonly IMemoryCache _memoryCache; + + public DataverseTeamRepository(IOrganizationServiceAsync2 orgService, IMemoryCache memoryCache) + { + _orgService = orgService; + _memoryCache = memoryCache; + } + public async Task GetByIdAsync(Guid Id) + { + return await _memoryCache.GetOrCreateAsync($"team.GetByIdAsync.{Id}", async (_) => + { + var query = new QueryExpression("team") + { + ColumnSet = new ColumnSet("teamid", "name"), + Criteria = new FilterExpression + { + Conditions = + { + new ConditionExpression("teamid", ConditionOperator.Equal, Id) + } + } + }; + var result = await _orgService.RetrieveMultipleAsync(query); + return result.Entities.FirstOrDefault(); + }); + } + public async Task GetByNameAsync(string teamName) + { + + return await _memoryCache.GetOrCreateAsync($"team.GetByNameAsync.{teamName}", async (_) => + { + var query = new QueryExpression("team") + { + ColumnSet = new ColumnSet("teamid", "name"), + Criteria = new FilterExpression + { + Conditions = + { + new ConditionExpression("name", ConditionOperator.Equal, teamName) + } + } + }; + var result = await _orgService.RetrieveMultipleAsync(query); + return result.Entities.FirstOrDefault(); + }); + } +} diff --git a/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/DataverseUserRepository.cs b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/DataverseUserRepository.cs new file mode 100644 index 0000000..94dba34 --- /dev/null +++ b/src/Dataverse.ConfigurationMigrationTool/Dataverse.ConfigurationMigrationTool.Console/Services/Dataverse/DataverseUserRepository.cs @@ -0,0 +1,57 @@ +using Dataverse.ConfigurationMigrationTool.Console.Features.Shared; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.PowerPlatform.Dataverse.Client; +using Microsoft.Xrm.Sdk; +using Microsoft.Xrm.Sdk.Query; + +namespace Dataverse.ConfigurationMigrationTool.Console.Services.Dataverse; +public class DataverseUserRepository : ISystemUserRepository +{ + private readonly IOrganizationServiceAsync2 _orgService; + + public DataverseUserRepository(IOrganizationServiceAsync2 orgService, IMemoryCache memoryCache) + { + _orgService = orgService; + _memoryCache = memoryCache; + } + + private readonly IMemoryCache _memoryCache; + public async Task GetByIdAsync(Guid Id) + { + return await _memoryCache.GetOrCreateAsync($"user.GetByIdAsync.{Id}", async (_) => + { + var query = new QueryExpression("systemuser") + { + ColumnSet = new ColumnSet("systemuserid", "fullname"), + Criteria = new FilterExpression + { + Conditions = + { + new ConditionExpression("systemuserid", ConditionOperator.Equal, Id) + } + } + }; + var result = await _orgService.RetrieveMultipleAsync(query); + return result.Entities.FirstOrDefault(); + }); + } + public async Task GetByName(string fullname) + { + return await _memoryCache.GetOrCreateAsync($"user.GetByName.{fullname}", async (_) => + { + var query = new QueryExpression("systemuser") + { + ColumnSet = new ColumnSet("systemuserid", "fullname"), + Criteria = new FilterExpression + { + Conditions = + { + new ConditionExpression("fullname", ConditionOperator.Equal, fullname) + } + } + }; + var result = await _orgService.RetrieveMultipleAsync(query); + return result.Entities.FirstOrDefault(); + }); + } +}