diff --git a/1.README_SolutionStructure.md b/1.README_SolutionStructure.md index c971c7c..d24b1c1 100644 --- a/1.README_SolutionStructure.md +++ b/1.README_SolutionStructure.md @@ -10,43 +10,40 @@ That is where contracts come in. They are the possible hand-off values of a serv The simpler your contracts are, the less you are able to have un-expected ties between services, and the less likely you are to break isolation. As a rule of thumb, if you can't tell exactly what you can and can't do based on the the interface signature (and maybe a contract definition), it is too complex. -# IDesign +#IDesign The key to understanding the code layout is a pattern called IDesign. - IDesign is a layered, service-oriented architecture pattern. IDesign has five layers. - __Clients__ - responsible for consumption of your program. For example, user interfaces, apis, windows services. - __Managers__ - Organize the order of execution. These are the primary flows of your application - __Engines__ - The algorithms, business logic, data-manipulation type stuff - __Accessor__ - Abstract external resources for use by the application. For example, database access, file system access, external apis - __Resources__ - Anything not controlled by your code (databases, external apis, etc). These are what accessors abstract. +Clients - responsible for consumption of your program. For example, user interfaces, apis, windows services. +Managers - Organize the order of execution. These are the primary flows of your application +Engines - The algorithms, business logic, data-manipulation type stuff +Accessor - Abstract external resources for use by the application. For example, database access, file system access, external apis +Resources - Anything not controlled by your code (databases, external apis, etc). These are what accessors abstract. Viewed in terms of orthogonality and information hiding, each layer is responsible for isolating/hiding a particular type of design concern. - __Accessors__ - Third party code or components not fully in your application's control - __Engines__ - Computation - __Managers__ - Composition of functionality - __Clients__ - Application representation and interaction +Accessors - Third party code or components not fully in your application's control +Engines - Computation +Managers - Composition of functionality +Clients - Application representation and interaction Layers may only call into the adjacent lower layer. (I.E. engines only call accessors, accessors only call resources). The one exception is that managers can call to both engines and accessors. Sometimes a client may call an engine, but it is rare that this is a good choice. - -However, engines never call managers. That would mean your processing is calling your orchestration, which can lead to many unexpected execution order problems. - +However, engines never call managers. That would mean your processing is calling your orchestration, which can lead to many unexpected +execution order problems. These simple rules categorize and organize the vast majority of programming responsibility types. The simple rules allow you to quickly find a piece of code and limit what other units of code it could be working with, thus reducing complexity. By considering the few rules of IDesign, you are also led to follow other known best practices like SOLID, information hiding, stable dependencies and others. - One principle worth calling out specifically is single responsibility principle. IDesign draws attention to sneaky violations of single responsibility principle by differentiating responsibility types. -# Folder Structure +#Folder Structure The folder structure reflects the primary responsibilities of IDesign, and other primary concerns like testing. It allows the solution to be more easily navigated and not have an overwhelming number of choices as the number of projects grows. @@ -56,6 +53,6 @@ The numbers in the folders allow us to set the folder order to match the layerin The shared folder is for meta-infrastructure that is needed across projects. For example, dependency injection config, data contracts, and possibly logging. Do not be tempted to put app functionality in this folder. -# Project Name Prefixes +#Project Name Prefixes Adding some namespacing to our projects (e.g. Accessors.Project or Tests.Project) allows our underlying file structure to -be organized by responsibility type and easily navigated. +be organized by responsibility type and easily navigated. \ No newline at end of file diff --git a/Accessors.DatabaseAccessors/TodoItemAccessor.cs b/Accessors.DatabaseAccessors/TodoItemAccessor.cs index 35d1e1f..caeff6e 100644 --- a/Accessors.DatabaseAccessors/TodoItemAccessor.cs +++ b/Accessors.DatabaseAccessors/TodoItemAccessor.cs @@ -54,7 +54,15 @@ public SaveResult SaveTodoItem(TodoItem todoItem) { TodoItemDTO dbModel = mapper.ContractToModel(todoItem); - db.AddOrUpdate(dbModel); + if(todoItem.Id.IsDefault()) + { + db.TodoItems.Add(dbModel); + } + else + { + db.TodoItems.Attach(dbModel); + db.Entry(dbModel).State = EntityState.Modified; + } db.SaveChanges(); TodoItem savedTodoItem = mapper.ModelToContract(dbModel); diff --git a/Accessors.DatabaseAccessors/TodoListAccessor.cs b/Accessors.DatabaseAccessors/TodoListAccessor.cs index ae68c7f..7d351aa 100644 --- a/Accessors.DatabaseAccessors/TodoListAccessor.cs +++ b/Accessors.DatabaseAccessors/TodoListAccessor.cs @@ -71,7 +71,15 @@ public SaveResult SaveTodoList(TodoList todoList) { TodoListDTO dbModel = mapper.ContractToModel(todoList); - db.AddOrUpdate(dbModel); + if (todoList.Id.IsDefault()) + { + db.TodoLists.Add(dbModel); + } + else + { + db.TodoLists.Attach(dbModel); + db.Entry(dbModel).State = EntityState.Modified; + } db.SaveChanges(); TodoList savedTodoItem = mapper.ModelToContract(dbModel); diff --git a/Shared.DatabaseContext/TodoContext.cs b/Shared.DatabaseContext/TodoContext.cs index 17afe8b..90e603f 100644 --- a/Shared.DatabaseContext/TodoContext.cs +++ b/Shared.DatabaseContext/TodoContext.cs @@ -49,28 +49,5 @@ public override int SaveChanges() // throw new DbEntityValidationException(exceptionMessage, ex.EntityValidationErrors); // } } - - /// - /// If the id is default adds a new entity - /// If the id is anything else, attaches and marks as modified - /// Does not commit changes. You must call db.SaveChanges - /// - /// - /// - /// - public T AddOrUpdate(T entity) where T : class, DatabaseObjectBase - { - if (Shared.DataContracts.Id.Default() == entity.Id) - { - this.Set().Add(entity); - } - else - { - this.Set().Attach(entity); - this.Entry(entity).State = EntityState.Modified; - } - - return entity; - } } } diff --git a/Tests.AccessorTests/TodoItemAccessorTests.cs b/Tests.AccessorTests/TodoItemAccessorTests.cs index 0ad08a3..8d30f9f 100644 --- a/Tests.AccessorTests/TodoItemAccessorTests.cs +++ b/Tests.AccessorTests/TodoItemAccessorTests.cs @@ -115,7 +115,13 @@ public void SaveTodoItem_Update() public void SaveTodoItem_Create() { // arrange - TodoItem expectedTodoItem = dataPrep.TodoItems.Create(isPersisted: false); + TodoList parentList = dataPrep.TodoLists.Create(); + TodoItem expectedTodoItem = new TodoItem() + { + TodoListId = parentList.Id, + Description = Guid.NewGuid().ToString(), + IsComplete = false, + }; // act SaveResult saveResult = accessor.SaveTodoItem(expectedTodoItem); diff --git a/Tests.AccessorTests/TodoListAccessorTests.cs b/Tests.AccessorTests/TodoListAccessorTests.cs index 30a8e29..c769584 100644 --- a/Tests.AccessorTests/TodoListAccessorTests.cs +++ b/Tests.AccessorTests/TodoListAccessorTests.cs @@ -56,7 +56,11 @@ public void SaveTodoList_Update() public void SaveTodoList_Create() { User user = dataPrep.Users.Create(); - TodoList newTodoList = dataPrep.TodoLists.Create(isPersisted: false); + TodoList newTodoList = new TodoList() + { + UserId = user.Id, + Title = Guid.NewGuid().ToString(), + }; SaveResult saveResult = accessor.SaveTodoList(newTodoList); TodoList actualTodoList = saveResult.Result; diff --git a/Tests.DataPrep/DataPrep.cs b/Tests.DataPrep/DataPrep.cs index 2391f97..c1551ee 100644 --- a/Tests.DataPrep/DataPrep.cs +++ b/Tests.DataPrep/DataPrep.cs @@ -1,4 +1,10 @@ - +using Bogus; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + namespace Tests.DataPrep { public class TodoDataPrep diff --git a/Tests.DataPrep/TodoItemPrep.cs b/Tests.DataPrep/TodoItemPrep.cs index 0b1ad9c..74e9359 100644 --- a/Tests.DataPrep/TodoItemPrep.cs +++ b/Tests.DataPrep/TodoItemPrep.cs @@ -8,16 +8,20 @@ namespace Tests.DataPrep { - public class TodoItemPrep : TypePrepBase + public class TodoItemPrep { + ITestDataAccessor dataAccessor; TodoListPrep todoListPrep; - public TodoItemPrep(ITestDataAccessor dataAccessor, TodoListPrep todoListPrep) : base (dataAccessor) + Bogus.Faker random = new Bogus.Faker(); + + public TodoItemPrep(ITestDataAccessor dataAccessor, TodoListPrep todoListPrep) { + this.dataAccessor = dataAccessor; this.todoListPrep = todoListPrep; } - public TodoItem Create(TodoList todoList = null, bool? isComplete = null, bool isPersisted = true) + public TodoItem Create(TodoList todoList = null, bool? isComplete = null) { TodoList sanitizedTodoList = todoList ?? todoListPrep.Create(); @@ -28,17 +32,23 @@ public TodoItem Create(TodoList todoList = null, bool? isComplete = null, bool i IsComplete = isComplete ?? random.PickRandom(true, false), }; - if (isPersisted) - { - TodoItem savedItem = Create(todoItem); - return savedItem; - } - else - { - return todoItem; - } + TodoItem savedItem = Create(todoItem); + + return savedItem; } + public TodoItem Create(TodoItem todoItem, bool isActive = true) + { + TodoItem_Mapper mapper = new TodoItem_Mapper(); + TodoItemDTO model = mapper.ContractToModel(todoItem); + // handle active state here so I can create inactive items, but leave active flags off of data contracts + model.IsActive = isActive; + + TodoItemDTO savedModel = dataAccessor.Create(model); + TodoItem savedContract = mapper.ModelToContract(savedModel); + + return savedContract; + } public IEnumerable CreateManyForList(int count, TodoList todoList) { diff --git a/Tests.DataPrep/TodoListPrep.cs b/Tests.DataPrep/TodoListPrep.cs index b5ee99b..2faa799 100644 --- a/Tests.DataPrep/TodoListPrep.cs +++ b/Tests.DataPrep/TodoListPrep.cs @@ -8,15 +8,19 @@ namespace Tests.DataPrep { - public class TodoListPrep : TypePrepBase + public class TodoListPrep { + Bogus.Faker random = new Bogus.Faker(); + + ITestDataAccessor dataAccessor; UserPrep userPrep; - public TodoListPrep(ITestDataAccessor dataAccessor, UserPrep userPrep) : base(dataAccessor) + public TodoListPrep(ITestDataAccessor dataAccessor, UserPrep userPrep) { + this.dataAccessor = dataAccessor; this.userPrep = userPrep; } - public TodoList Create(User user = null, bool isPersisted = true) + public TodoList Create(User user = null) { User sanitizedUser = user ?? userPrep.Create(); TodoList todoList = new TodoList() @@ -25,16 +29,21 @@ public TodoList Create(User user = null, bool isPersisted = true) Title = random.Lorem.Sentence(), }; - if (isPersisted) - { - TodoList savedList = base.Create(todoList); - return savedList; - } - else - { - return todoList; - } + TodoList savedList = Create(todoList); + + return savedList; + } + + public TodoList Create(TodoList todoList, bool isActive = true) + { + TodoList_Mapper mapper = new TodoList_Mapper(); + TodoListDTO model = mapper.ContractToModel(todoList); + model.IsActive = isActive; + + TodoListDTO savedModel = dataAccessor.Create(model); + TodoList savedContract = mapper.ModelToContract(savedModel); + return savedContract; } public IEnumerable CreateManyForUser(int count, User user) diff --git a/Tests.DataPrep/TypePrepBase.cs b/Tests.DataPrep/TypePrepBase.cs deleted file mode 100644 index 7aecec0..0000000 --- a/Tests.DataPrep/TypePrepBase.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Bogus; -using Shared.DatabaseContext.DBOs; -using System; -using System.Collections.Generic; -using System.Text; - -namespace Tests.DataPrep -{ - public class TypePrepBase where T : class where PersistedType : class, DatabaseObjectBase - { - protected ITestDataAccessor dataAccessor; - protected Faker random = new Faker(); - - protected MapperBase mapper = new MapperBase(); - - public TypePrepBase(ITestDataAccessor dataAccessor) - { - this.dataAccessor = dataAccessor; - } - - public virtual T Create() - { - Faker faker = new Faker(); - T saved = Create(faker.Generate()); - return saved; - } - - public virtual T Create(T existing, bool isActive = true) - { - PersistedType model = mapper.ContractToModel(existing); - // handle active state here so I can create inactive items, but leave active flags off of data contracts - model.IsActive = isActive; - - PersistedType savedModel = dataAccessor.Create(model); - T savedContract = mapper.ModelToContract(savedModel); - - return savedContract; - } - - } -} diff --git a/Tests.DataPrep/UserPrep.cs b/Tests.DataPrep/UserPrep.cs index a15c16b..9beefd6 100644 --- a/Tests.DataPrep/UserPrep.cs +++ b/Tests.DataPrep/UserPrep.cs @@ -9,10 +9,39 @@ namespace Tests.DataPrep { - public class UserPrep : TypePrepBase + public class UserPrep { - public UserPrep(ITestDataAccessor dataAccessor) : base(dataAccessor) + Bogus.Faker random = new Bogus.Faker(); + + ITestDataAccessor dataAccessor; + public UserPrep(ITestDataAccessor dataAccessor) + { + this.dataAccessor = dataAccessor; + } + + public User Create() { + User user = new User() + { + Name = random.Name.FullName(), + }; + User savedUser = Create(user); + + return savedUser; } + + public User Create(User user, bool isActive = true) + { + // NOTE/TODO: feels like there should be a good way to encapsulate this so most data prep classes don't have to write it + User_Mapper mapper = new User_Mapper(); + UserDTO model = mapper.ContractToModel(user); + model.IsActive = isActive; + + UserDTO savedModel = dataAccessor.Create(model); + User savedContract = mapper.ModelToContract(savedModel); + + return savedContract; + } + } } diff --git a/Tests.TestBases/ManagerTestBase.cs b/Tests.TestBases/ManagerTestBase.cs index 92836f4..255a853 100644 --- a/Tests.TestBases/ManagerTestBase.cs +++ b/Tests.TestBases/ManagerTestBase.cs @@ -14,7 +14,8 @@ public abstract class ManagerTestBase [SetUp] public virtual void TestInitialize() { - dataPrep.EnsureDatastore(); + TodoContext database = new TodoContext(); + database.Database.EnsureCreated(); // transactionScope causes db changes to be rolled back at end of test _transactionScope = new TransactionScope(); diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 21a0eee..0bc81a1 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -13,18 +13,36 @@ variables: buildConfiguration: 'Release' steps: +- script: | + git fetch --all + git branch -D master + git rev-parse origin/master + displayName: Branch shenannigans for sonar + #https://community.sonarsource.com/t/pr-analysis-shows-no-code-on-one-specific-long-lived-branch/4172/6 - script: dotnet restore displayName: Restore nuget -- script: dotnet build --configuration $(buildConfiguration) - displayName: 'dotnet build $(buildConfiguration)' +- task: SonarCloudPrepare@1 + displayName: Prepare Sonar + inputs: + SonarCloud: '088aa95d-b2a2-4d2e-907c-ac5b8549dd80' + organization: 'farlee2121-github' + projectKey: 'farlee2121_TestingPatterns' + projectName: 'TestingPatterns' +- task: VSBuild@1 + displayName: 'Build solution **/*.sln' + inputs: + solution: '**/*.sln' + msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:PackageLocation="$(build.artifactstagingdirectory)\\"' + configuration: '$(BuildConfiguration)' - task: DotNetCoreCLI@2 inputs: command: test projects: '**/Tests.*/*.csproj' - arguments: '--configuration $(buildConfiguration) --collect "Code coverage"' + arguments: '--configuration $(buildConfiguration) --no-build --collect "Code coverage"' displayName: Unit Tests -- task: codecoveragecomparerbt@1 - displayName: - inputs: - codecoveragemeasurementmethod: 'Lines' +- task: SonarSource.sonarcloud.ce096e50-6155-4de8-8800-4221aaeed4a1.SonarCloudAnalyze@1 + displayName: Run Sonar +- task: SonarCloudPublish@1 + +