diff --git a/.azure-pipelines-canary.yml b/.azure-pipelines-canary.yml index fcd8692b5..a0cfc9ad5 100644 --- a/.azure-pipelines-canary.yml +++ b/.azure-pipelines-canary.yml @@ -27,18 +27,19 @@ stages: condition: and(succeeded(), eq(variables['IsCanary'], 'true')) dependsOn: Build_Canary - jobs: - - template: build/stage-release-appcenter.yml - parameters: - applicationEnvironment: Staging - deploymentEnvironment: AppCenter - appCenterWindowsSlug: $(AppCenterWindowsSlug_Canary) - appCenteriOSSlug: $(AppCenteriOSSlug_Canary) - appCenterAndroidSlug: $(AppCenterAndroidSlug_Canary) - androidKeyStoreFile: $(InternalKeystore) - androidVariableGroup: 'ApplicationTemplate.Distribution.Internal.Android' - appCenterServiceConnectionName: $(AppCenterCanaryServiceConnection) - appCenterDistributionGroup: $(AppCenterCanaryDistributionGroup) + # TODO create firebase project for the canary app + # jobs: + # - template: build/stage-release-appcenter.yml + # parameters: + # applicationEnvironment: Staging + # deploymentEnvironment: AppCenter + # appCenterWindowsSlug: $(AppCenterWindowsSlug_Canary) + # appCenteriOSSlug: $(AppCenteriOSSlug_Canary) + # appCenterAndroidSlug: $(AppCenterAndroidSlug_Canary) + # androidKeyStoreFile: $(InternalKeystore) + # androidVariableGroup: 'ApplicationTemplate.Distribution.Internal.Android' + # appCenterServiceConnectionName: $(AppCenterCanaryServiceConnection) + # appCenterDistributionGroup: $(AppCenterCanaryDistributionGroup) - template: build/stage-release-appstore.yml parameters: diff --git a/.azure-pipelines.yml b/.azure-pipelines.yml index 2a4689ac9..6d4b7e088 100644 --- a/.azure-pipelines.yml +++ b/.azure-pipelines.yml @@ -23,6 +23,10 @@ trigger: - main variables: +#-if false +- name: IsReleaseBranch + value: $[eq(variables['Build.SourceBranch'], 'refs/heads/main')] +#-endif - template: build/variables.yml stages: @@ -49,7 +53,7 @@ stages: iosVariableGroup: 'ApplicationTemplate.Distribution.Internal.iOS' - stage: Publish_Template_Package - condition: and(succeeded(), eq(variables['IsLightBuild'], 'false')) + condition: and(succeeded(), eq(variables['IsLightBuild'], 'false'), eq(variables['IsReleaseBranch'], 'true')) dependsOn: - Build_Staging_GeneratedApp - Build_Staging @@ -72,21 +76,16 @@ stages: iosVariableGroup: 'ApplicationTemplate.Distribution.Internal.iOS' BannerVersionNameText: "STAGING" -- stage: AppCenter_TestFlight_Staging +- stage: Firebase_TestFlight_Staging condition: and(succeeded(), eq(variables['IsLightBuild'], 'false')) dependsOn: Build_Staging jobs: - - template: build/stage-release-appcenter.yml + - template: build/stage-release-firebase.yml parameters: applicationEnvironment: Staging deploymentEnvironment: AppCenter - appCenterWindowsSlug: $(AppCenterWindowsSlug) - appCenteriOSSlug: $(AppCenteriOSSlug) - appCenterAndroidSlug: $(AppCenterAndroidSlug) - androidKeyStoreFile: $(InternalKeystore) androidVariableGroup: 'ApplicationTemplate.Distribution.Internal.Android' - appCenterServiceConnectionName: $(AppCenterServiceConnection) - appCenterDistributionGroup: $(AppCenterDistributionGroup) + iosVariableGroup: 'ApplicationTemplate.Distribution.Internal.Ios' - template: build/stage-release-appstore.yml parameters: @@ -106,22 +105,6 @@ stages: iosCertificateFile: $(AppStoreCertificate) iosVariableGroup: 'ApplicationTemplate.Distribution.AppStore' -- stage: AppCenter_Production - condition: and(succeeded(), eq(variables['IsLightBuild'], 'false')) - dependsOn: Build_Production - jobs: - - template: build/stage-release-appcenter.yml - parameters: - applicationEnvironment: Production - deploymentEnvironment: 'AppCenter Prod' - appCenterWindowsSlug: $(AppCenterWindowsSlug_Production) - appCenteriOSSlug: $(AppCenteriOSSlug_Production) - appCenterAndroidSlug: $(AppCenterAndroidSlug_Production) - androidKeyStoreFile: $(GooglePlayKeystore) - androidVariableGroup: 'ApplicationTemplate.Distribution.GooglePlay' - appCenterServiceConnectionName: $(AppCenterServiceConnection) - appCenterDistributionGroup: $(AppCenterDistributionGroup) - - stage: AppStore condition: and(succeeded(), eq(variables['IsLightBuild'], 'false')) dependsOn: Build_Production diff --git a/.editorconfig b/.editorconfig index bb2905e0e..1ae715381 100644 --- a/.editorconfig +++ b/.editorconfig @@ -391,6 +391,8 @@ dotnet_diagnostic.SA1210.severity = suggestion dotnet_diagnostic.CA1308.severity = none # CS1587: XML comment is not placed on a valid language element dotnet_diagnostic.CS1587.severity = none +# CA1859: Use concrete types when possible for improved performance +dotnet_diagnostic.CA1859.severity = none # VSTHRD110: This one is bugged: https://github.com/microsoft/vs-threading/issues/899 dotnet_diagnostic.VSTHRD110.severity = none diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index fffbf5274..aa24fcf81 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -32,7 +32,7 @@ GitHub Issue: # ### Always applicable No matter your changes, these checks always apply. - [ ] Your conventional commits are aligned with the **Impact on version** section. -- [ ] Updated [CHANGELOG.md](../CHANGELOG.md). +- [ ] Updated [CHANGELOG.md](../blob/main/CHANGELOG.md). - Use the latest Major.Minor.X header if you do a **Patch** change. - Create a new Major.Minor.X header if you do a **Minor** or **Major** change. - If you create a new header, it aligns with the **Impact on version** section and matches what is generated in the build pipeline. diff --git a/APP_README.md b/APP_README.md index 0531c9f31..b05f9313d 100644 --- a/APP_README.md +++ b/APP_README.md @@ -20,12 +20,13 @@ This repository was generated using the **nventive Mobile Template**. ### Local Development Requirements All development is expected to be done from Visual Studio in a Windows environment. -- .Net 7 +- .NET 8 - Visual Studio 2022 (17.4 and above) - We recommend validating your components using this [Uno guide](https://platform.uno/docs/articles/get-started-vs-2022.html). - For mobile development, MAUI workloads are required. - You can install them using [`uno-check`](https://platform.uno/docs/articles/external/uno.check/doc/using-uno-check.html). - For local iOS compilation and debugging, you need access to Mac with Xcode 14.2 (more recent versions may work too). + > 💡 You'll need an [Apple provisioning profile](https://developer.apple.com/help/account/manage-profiles/create-a-development-provisioning-profile/) to start your application. ### Pipelines Requirements The pipelines (for continuous integration, testing, and delivery) of this project are made for [Azure Pipelines](https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/what-is-azure-pipelines?view=azure-devops). @@ -80,10 +81,10 @@ TODO: Fill the following table with your own pipelines. | Link | Code Entry Point | Goal | Triggers | |-|-|-|-| | [Name of Main Pipeline](link-to-pipeline)| [`.azure-pipelines.yml`](.azure-pipelines.yml)| Build validation during pull request.| Pull requests. -| [Name of Main Pipeline](link-to-pipeline)| [`.azure-pipelines.yml`](.azure-pipelines.yml)| Build and deploy the application to AppCenter, TestFlight, and GooglePlay. | Changes on the `main` branch.
Manual trigger. +| [Name of Main Pipeline](link-to-pipeline)| [`.azure-pipelines.yml`](.azure-pipelines.yml)| Build and deploy the application to Firebase, TestFlight, and GooglePlay. | Changes on the `main` branch.
Manual trigger. | [Name of API Integration Tests Pipeline](link-to-pipeline)| [`.azure-pipelines-api-integration-tests.yml`](.azure-pipelines.yml)| Run all tests, including APIs integration tests. | Daily cron job.
Manual trigger. | [Name of Canary Merge Pipeline](link-to-pipeline)| [`build/canary-merge.yml`](.azure-pipelines.yml)| Creation of canary branches (`canary/build/*`). | Daily cron job. -| [Name of Canary Pipeline](link-to-pipeline)| [`.azure-pipelines-canary.yml`](.azure-pipelines.yml)| Build and deploy canary versions of the app to AppCenter and TestFlight. | Upon creation of branches with the `canary/build/*` pattern. +| [Name of Canary Pipeline](link-to-pipeline)| [`.azure-pipelines-canary.yml`](.azure-pipelines.yml)| Build and deploy canary versions of the app to Firebase and TestFlight. | Upon creation of branches with the `canary/build/*` pattern. ## Additional Information diff --git a/CHANGELOG.md b/CHANGELOG.md index 444792b6f..6122f305d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) Prefix your items with `(Template)` if the change is about the template and not the resulting application. +## 4.0.x +- Using Firebase app distribution instead of AppCenter for internal distribution. + +## 3.5.X +- Bump Uno packages to 5.2.121 to fix a crash on iOS. +- Ensure NV.Template.Mobile nuget is only deployed from the main branch. + ## 3.4.X - Added a kill switch feature to the app. - Bump Uno.WinUI, Uno.WinUI.DevServer, Uno.WinUI.Lottie and Uno.UI.Adapter.Microsoft.Extensions.Logging to 5.0.159 to fix backNavigation/CloseModal crash. diff --git a/Directory.Build.props b/Directory.Build.props index f95d199de..e81b7d0ce 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,13 +4,13 @@ all runtime; build; native; contentfiles; analyzers - + all runtime; build; native; contentfiles; analyzers - + diff --git a/README.md b/README.md index 61a3e86fc..033f0d760 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Uno Platform Application Template +# Uno Platform Application Template [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg?style=flat-square)](LICENSE) ![Version](https://img.shields.io/nuget/v/NV.Templates.Mobile?style=flat-square) ![Downloads](https://img.shields.io/nuget/dt/NV.Templates.Mobile?style=flat-square) @@ -21,7 +21,7 @@ From left to right: WinUI, iOS, and Android. ## Requirements -Visual Studio 2022 with .Net 7 are required. +Visual Studio 2022 with .NET 8 are required. This template largely relies on Uno Platform, if you want to make sure you got everything installed correctly on your machine, we encourage you to use `uno-check`, the documentation is available [here](https://platform.uno/docs/articles/uno-check.html) diff --git a/build/stage-release-appcenter.yml b/build/stage-release-appcenter.yml deleted file mode 100644 index 694963230..000000000 --- a/build/stage-release-appcenter.yml +++ /dev/null @@ -1,110 +0,0 @@ -parameters: - applicationEnvironment: '' # e.g. "Staging", "Production" - deploymentEnvironment: '' # e.g. "GooglePlay", "AppStore", "AppCenter" - appCenterWindowsSlug: '' - appCenteriOSSlug: '' - appCenterAndroidSlug: '' - androidKeyStoreFile: '' - androidVariableGroup: '' - appCenterServiceConnectionName: '' - appCenterDistributionGroup: '' - -jobs: -- deployment: AppCenter_Android - pool: - vmImage: $(windowsHostedAgentImage) - variables: - - name: artifactName - value: $(AndroidArtifactName)_${{ parameters.applicationEnvironment }} - - name: releaseNotesArtifactName - value: ReleaseNotes_${{ parameters.applicationEnvironment }} - - group: ${{ parameters.androidVariableGroup }} - environment: ${{ parameters.deploymentEnvironment }} - strategy: - runOnce: - deploy: - steps: - - download: current - displayName: "Download Artifact" - artifact: $(artifactName) - - - download: current - displayName: "Download Release Notes" - artifact: $(releaseNotesArtifactName) - - - task: AppCenterDistribute@3 - displayName: Deploy Android to AppCenter - inputs: - serverEndpoint: ${{ parameters.appCenterServiceConnectionName }} - distributionGroupId: ${{ parameters.appCenterDistributionGroup }} - appSlug: ${{ parameters.appCenterAndroidSlug }} - appFile: '$(Pipeline.Workspace)/$(artifactName)/*Signed.aab' - releaseNotesOption: file - releaseNotesFile: "$(Pipeline.Workspace)/$(releaseNotesArtifactName)/ReleaseNotes-Excerpt.md" - isSilent: true - - - task: DeleteFiles@1 - displayName: "Remove Downloaded Artifacts (Build)" - condition: always() - inputs: - SourceFolder: $(Pipeline.Workspace)/$(artifactName) - RemoveSourceFolder: true - Contents: '**' - - - task: DeleteFiles@1 - displayName: "Remove Downloaded Artifacts (Release Notes)" - condition: always() - inputs: - SourceFolder: $(Pipeline.Workspace)/$(releaseNotesArtifactName) - RemoveSourceFolder: true - Contents: '**' - -- deployment: AppCenter_iOS - pool: - vmImage: $(macOSHostedAgentImage) - variables: - - name: artifactName - value: $(iOSArtifactName)_${{ parameters.applicationEnvironment }} - - name: releaseNotesArtifactName - value: ReleaseNotes_${{ parameters.applicationEnvironment }} - environment: ${{ parameters.deploymentEnvironment }} - strategy: - runOnce: - deploy: - steps: - - download: current - displayName: "Download Artifact" - artifact: $(artifactName) - - - download: current - displayName: "Download Release Notes" - artifact: $(releaseNotesArtifactName) - - - task: AppCenterDistribute@3 - displayName: Deploy iOS to AppCenter - inputs: - serverEndpoint: ${{ parameters.appCenterServiceConnectionName }} - appSlug: ${{ parameters.appCenteriOSSlug }} - appFile: $(Pipeline.Workspace)/$(artifactName)/*.ipa - symbolsDsymFiles: $(Pipeline.Workspace)/$(artifactName)/*.dSYM - symbolsIncludeParentDirectory: true - distributionGroupId: ${{ parameters.appCenterDistributionGroup }} - releaseNotesOption: file - releaseNotesFile: "$(Pipeline.Workspace)/$(releaseNotesArtifactName)/ReleaseNotes-Excerpt.md" - isSilent: true - - - task: DeleteFiles@1 - displayName: "Remove Downloaded Artifacts (Build)" - condition: always() - inputs: - SourceFolder: $(Pipeline.Workspace)/$(artifactName) - RemoveSourceFolder: true - Contents: '**' - - - task: DeleteFiles@1 - displayName: "Remove Downloaded Artifacts (Release Notes)" - condition: always() - inputs: - SourceFolder: $(Pipeline.Workspace)/$(releaseNotesArtifactName) - RemoveSourceFolder: true - Contents: '**' diff --git a/build/stage-release-appstore.yml b/build/stage-release-appstore.yml index d6c91a2bd..16348d0ac 100644 --- a/build/stage-release-appstore.yml +++ b/build/stage-release-appstore.yml @@ -1,6 +1,6 @@ parameters: applicationEnvironment: '' # e.g. "Staging", "Production" - deploymentEnvironment: '' # e.g. "GooglePlay", "AppStore", "AppCenter" + deploymentEnvironment: '' # e.g. "GooglePlay", "AppStore", "Firebase" jobs: - deployment: AppStore_iOS_${{ parameters.deploymentEnvironment}} diff --git a/build/stage-release-firebase.yml b/build/stage-release-firebase.yml new file mode 100644 index 000000000..31b776df7 --- /dev/null +++ b/build/stage-release-firebase.yml @@ -0,0 +1,42 @@ +parameters: + applicationEnvironment: '' # e.g. "Staging", "Production" + deploymentEnvironment: '' # e.g. "GooglePlay", "AppStore", "Firebase" + androidVariableGroup: '' + iosVariableGroup: '' + +jobs: +- deployment: Firebase_Android + pool: + vmImage: $(linuxHostedAgentImage) + variables: + - name: artifactName + value: $(AndroidArtifactName)_${{ parameters.applicationEnvironment }} + - name: releaseNotesArtifactName + value: ReleaseNotes_${{ parameters.applicationEnvironment }} + - group: ${{ parameters.androidVariableGroup }} + environment: ${{ parameters.deploymentEnvironment }} + strategy: + runOnce: + deploy: + steps: + - template: ./templates/firebase-deploy.yml + parameters: + fileName: '$(ApplicationIdentifier)-Signed.apk' + +- deployment: Firebase_iOS + pool: + vmImage: $(linuxHostedAgentImage) + variables: + - name: artifactName + value: $(iOSArtifactName)_${{ parameters.applicationEnvironment }} + - name: releaseNotesArtifactName + value: ReleaseNotes_${{ parameters.applicationEnvironment }} + - group: ${{ parameters.iosVariableGroup }} + environment: ${{ parameters.deploymentEnvironment }} + strategy: + runOnce: + deploy: + steps: + - template: ./templates/firebase-deploy.yml + parameters: + fileName: '$(SolutionName).Mobile.ipa' \ No newline at end of file diff --git a/build/stage-release-googleplay.yml b/build/stage-release-googleplay.yml index 2f8fd4006..38e22f255 100644 --- a/build/stage-release-googleplay.yml +++ b/build/stage-release-googleplay.yml @@ -1,6 +1,6 @@ parameters: applicationEnvironment: '' # e.g. "Staging", "Production" - deploymentEnvironment: '' # e.g. "GooglePlay", "AppStore", "AppCenter" + deploymentEnvironment: '' # e.g. "GooglePlay", "AppStore", "Firebase" jobs: - deployment: GooglePlay_Android diff --git a/build/templates/firebase-deploy.yml b/build/templates/firebase-deploy.yml new file mode 100644 index 000000000..9fe6132a5 --- /dev/null +++ b/build/templates/firebase-deploy.yml @@ -0,0 +1,50 @@ +parameters: + fileName: '' + +steps: +# Install Firebase tools +- script: 'npm install -g firebase-tools' + displayName: 'Install Firebase Tools' + +- download: current + displayName: "Download Artifact" + artifact: $(artifactName) + +- download: current + displayName: "Download Release Notes" + artifact: $(releaseNotesArtifactName) + +- task: DownloadSecureFile@1 + inputs: + secureFile: $(FirebaseDistributionServiceConnectionFile) + name: DistributionServiceConnection + displayName: "Download Firebase Service connection" + +- script: | + firebase appdistribution:distribute '$(Pipeline.Workspace)/$(artifactName)/${{ parameters.fileName }}' \ + --app '$(FirebaseAppId)' \ + --release-notes-file '$(Pipeline.Workspace)/$(releaseNotesArtifactName)/ReleaseNotes-Excerpt.md' \ + --groups "nventive" + env: + GOOGLE_APPLICATION_CREDENTIALS: $(DistributionServiceConnection.secureFilePath) + displayName: 'Deploy to Firebase' + +- task: DeleteFiles@1 + displayName: "Remove Downloaded Artifacts (Build)" + condition: always() + inputs: + SourceFolder: $(Pipeline.Workspace)/$(artifactName) + RemoveSourceFolder: true + Contents: '**' + +- task: DeleteFiles@1 + displayName: "Remove Downloaded Artifacts (Release Notes)" + condition: always() + inputs: + SourceFolder: $(Pipeline.Workspace)/$(releaseNotesArtifactName) + RemoveSourceFolder: true + Contents: '**' + +- task: PostBuildCleanup@3 + displayName: 'Post-Build Cleanup: Cleanup files to keep build server clean!' + condition: always() \ No newline at end of file diff --git a/build/variables.yml b/build/variables.yml index b71d01b40..1e9963874 100644 --- a/build/variables.yml +++ b/build/variables.yml @@ -3,7 +3,7 @@ # Make sure you have the following variable groups in your Azure pipeline library: # # ApplicationTemplate.Distribution.Internal.Android - # ApplicationIdentifier: This is the internal application id of the app for AppCenter releases. Note that this variable is used by Nimue to automatically change the package name. + # ApplicationIdentifier: This is the internal application id of the app for Firebase releases. Note that this variable is used by Nimue to automatically change the package name. # AndroidSigningKeyAlias: This is the keystore alias. # AndroidSigningKeyPass: This is the keystore keypass (secured). # AndroidSigningStorePass: This is the keystore storepass (secured). @@ -15,7 +15,7 @@ # AndroidSigningStorePass: This is the keystore storepass (secured). # # ApplicationTemplate.Distribution.Internal.iOS - # ApplicationIdentifier: This is the internal application id of the app for AppCenter releases. Note that this variable is used by Nimue to automatically change the bundle id. + # ApplicationIdentifier: This is the internal application id of the app for Firebase releases. Note that this variable is used by Nimue to automatically change the bundle id. # AppleCertificatePassword: The certificate password (secured). # # ApplicationTemplate.Distribution.AppStore @@ -31,7 +31,7 @@ InternalKeystore: com.nventive.internal.applicationtemplate.jks # This is the internal keystore used for internal builds. GooglePlayKeystore: com.nventive.applicationtemplate.jks # This is the official keystore used for Google Play. # iOS - InternalProvisioningProfile: com.nventive.applicationtemplate.mobileprovision # This is the internal provisioning profile for internal builds. + InternalProvisioningProfile: com.nventive.applicationtemplate-adhoc.mobileprovision # This is the internal provisioning profile for internal builds. InternalCertificate: nventive.p12 # This is the certificate from the nventive Apple account used to sign internal builds. AppStoreProvisioningProfile: com.nventive.applicationtemplate.mobileprovision # This is the client provisioning profile for the AppStore (Production distribution). AppStoreCertificate: nventive.p12 # This is the client production certificate used to sign AppStore builds. @@ -39,26 +39,10 @@ # Prerequisites - Service connections # Make sure you have the following service connections in your Azure pipeline library. GooglePlayServiceConnection: GooglePlay-nventive-ApplicationTemplate - AppCenterServiceConnection: AppCenter-nventive-framework AppStoreServiceConnection: AppStore-nventive - AppCenterCanaryServiceConnection: AppCenter-nventive-framework - # AppCenter slugs - # The "app slug" corresponds to the identifiers of the app in AppCenter; to find it, navigate to the app in a browser and; - # the URL should look like this: https://appcenter.ms/orgs/{orgId}/apps/{appId}; the slug is simply: "{orgId}/{appId}". - AppCenterAndroidSlug: 'nventive-framework/Application-Template-1' - AppCenteriOSSlug: 'nventive-framework/Application-Template' - AppCenterWindowsSlug: 'nventive-framework/Application-Template-2' - AppCenterAndroidSlug_Production: 'nventive-framework/ApplicationTemplate-Production-1' - AppCenteriOSSlug_Production: 'nventive-framework/ApplicationTemplate-Production' - AppCenterWindowsSlug_Production: 'nventive-framework/ApplicationTemplate-Production-2' - AppCenterAndroidSlug_Canary: 'nventive-framework/Application-Template-Canary-1' - AppCenteriOSSlug_Canary: 'nventive-framework/Application-Template-Canary' - AppCenterWindowsSlug_Canary: 'nventive-framework/Application-Template-Canary-2' - - # AppCenter Distribution Groups - AppCenterDistributionGroup: '00000000-0000-0000-0000-000000000000' - AppCenterCanaryDistributionGroup: '00000000-0000-0000-0000-000000000000' + # Firebase distribution + FirebaseDistributionServiceConnectionFile: 'firebase_unoapptemplate_dist.json' # Azure subscription # AzureSubscriptionName: @@ -74,6 +58,7 @@ # Virtual machine images windowsHostedAgentImage: 'windows-2022' macOSHostedAgentImage: 'macOS-13' + linuxHostedAgentImage: 'ubuntu-22.04' # SDK versions DotNetVersion: '7.0.400' diff --git a/doc/AzurePipelines.md b/doc/AzurePipelines.md index 356efde3a..3273c1983 100644 --- a/doc/AzurePipelines.md +++ b/doc/AzurePipelines.md @@ -18,7 +18,7 @@ These pipelines rely on a few variable groups and secrets in order to fully work At high level, the CI/CD pipelines do the following: - **Build** the app in **staging**. - - **Deploy** the staging app (to AppCenter and/or TestFlight and GooglePlay). + - **Deploy** the staging app (to Firebase and/or TestFlight and GooglePlay). - **Build** the app in **production**. - **Deploy** the production app (to TestFlight and GooglePlay). @@ -68,8 +68,8 @@ This is where the exact build steps are defined. These vary depending on the pla The release stages are even more straigtforward than the build ones. One thing to note is that, for the same reason as it is done at the end of the build steps, a clean-up step is included in every stage. -### AppCenter Release Stage ([stage-release-appcenter.yml](../build/stage-release-appcenter.yml)) -This stage is in charge of pushing the application to AppCenter. It's divided into 2 jobs, one for each platform. +### Firebase Release Stage ([stage-release-firebase.yml](../build/stage-release-firebase.yml)) +This stage is in charge of pushing the application to Firebase. It's divided into 2 jobs, one for each platform. ### Apple AppStore Release Stage ([stage-release-appstore.yml](../build/stage-release-appstore.yml)) This stage is in charge of pushing the iOS version to the Apple AppStore. Given that the build stage signs the application, this is as simple as using the proper task and pushing the **IPA** file. This should only be run for configurations that properly sign the application. diff --git a/src/ApplicationTemplate.sln b/src/ApplicationTemplate.sln index fcc4a9db8..3e6323924 100644 --- a/src/ApplicationTemplate.sln +++ b/src/ApplicationTemplate.sln @@ -64,7 +64,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{6E8378CB ..\build\canary-merge.yml = ..\build\canary-merge.yml ..\build\gitversion-config.yml = ..\build\gitversion-config.yml ..\build\stage-build.yml = ..\build\stage-build.yml - ..\build\stage-release-appcenter.yml = ..\build\stage-release-appcenter.yml + ..\build\stage-release-firebase.yml = ..\build\stage-release-firebase.yml ..\build\stage-release-appstore.yml = ..\build\stage-release-appstore.yml ..\build\stage-release-googleplay.yml = ..\build\stage-release-googleplay.yml ..\build\steps-build-android.yml = ..\build\steps-build-android.yml diff --git a/src/app/ApplicationTemplate.Access/ApiClients/Authentication/AuthenticationRepositoryMock.cs b/src/app/ApplicationTemplate.Access/ApiClients/Authentication/AuthenticationRepositoryMock.cs index c434bf974..1f5c8da43 100644 --- a/src/app/ApplicationTemplate.Access/ApiClients/Authentication/AuthenticationRepositoryMock.cs +++ b/src/app/ApplicationTemplate.Access/ApiClients/Authentication/AuthenticationRepositoryMock.cs @@ -1,15 +1,13 @@ using System; -using System.Collections.Generic; using System.Globalization; using System.IO; -using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace ApplicationTemplate.DataAccess; -public class AuthenticationRepositoryMock : IAuthenticationRepository +public sealed class AuthenticationRepositoryMock : IAuthenticationRepository { private readonly JsonSerializerOptions _serializerOptions; @@ -43,10 +41,7 @@ public async Task Login(CancellationToken ct, string email, public async Task RefreshToken(CancellationToken ct, AuthenticationData unauthorizedToken) { - if (unauthorizedToken is null) - { - throw new ArgumentNullException(nameof(unauthorizedToken)); - } + ArgumentNullException.ThrowIfNull(unauthorizedToken); // We add a delay to simulate a long API call await Task.Delay(TimeSpan.FromSeconds(2)); diff --git a/src/app/ApplicationTemplate.Access/ApplicationTemplate.Access.csproj b/src/app/ApplicationTemplate.Access/ApplicationTemplate.Access.csproj index 879c2b847..04bd5ee41 100644 --- a/src/app/ApplicationTemplate.Access/ApplicationTemplate.Access.csproj +++ b/src/app/ApplicationTemplate.Access/ApplicationTemplate.Access.csproj @@ -24,7 +24,7 @@ - + diff --git a/src/app/ApplicationTemplate.Access/Framework/Serialization/JsonSerializerToResponseContentSererializerAdapter.cs b/src/app/ApplicationTemplate.Access/Framework/Serialization/JsonSerializerToResponseContentSererializerAdapter.cs index 7c0a9c4e7..c510fe833 100644 --- a/src/app/ApplicationTemplate.Access/Framework/Serialization/JsonSerializerToResponseContentSererializerAdapter.cs +++ b/src/app/ApplicationTemplate.Access/Framework/Serialization/JsonSerializerToResponseContentSererializerAdapter.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Net.Http; -using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -9,7 +7,7 @@ namespace ApplicationTemplate; -public class JsonSerializerToResponseContentSererializerAdapter : IResponseContentDeserializer +public sealed class JsonSerializerToResponseContentSererializerAdapter : IResponseContentDeserializer { private readonly JsonSerializerOptions _serializerOptions; @@ -20,10 +18,7 @@ public JsonSerializerToResponseContentSererializerAdapter(JsonSerializerOptions public async Task Deserialize(CancellationToken ct, HttpContent content) { - if (content is null) - { - throw new ArgumentNullException(nameof(content)); - } + ArgumentNullException.ThrowIfNull(content); using (var stream = await content.ReadAsStreamAsync()) { diff --git a/src/app/ApplicationTemplate.Access/Framework/Serialization/JwtData.cs b/src/app/ApplicationTemplate.Access/Framework/Serialization/JwtData.cs index ab9e0de02..489e5e4e1 100644 --- a/src/app/ApplicationTemplate.Access/Framework/Serialization/JwtData.cs +++ b/src/app/ApplicationTemplate.Access/Framework/Serialization/JwtData.cs @@ -30,7 +30,7 @@ public JwtData(string token, JsonSerializerOptions jsonSerializerOptions = null) _jsonSerializerOptions = jsonSerializerOptions; Token = token; - var parts = token?.Split(new[] { '.' }); + var parts = token?.Split(['.']); RawHeader = parts?.Length > 0 ? Base64DecodeToString(parts[0]) : null; RawPayload = parts?.Length > 1 ? Base64DecodeToString(parts[1]) : null; Signature = parts?.Length > 2 ? Base64Decode(parts[2]) : null; diff --git a/src/app/ApplicationTemplate.Business/DadJokes/DadJokesQuote.cs b/src/app/ApplicationTemplate.Business/DadJokes/DadJokesQuote.cs index e375c2d67..907d40a9b 100644 --- a/src/app/ApplicationTemplate.Business/DadJokes/DadJokesQuote.cs +++ b/src/app/ApplicationTemplate.Business/DadJokes/DadJokesQuote.cs @@ -14,10 +14,7 @@ public DadJokesQuote(DadJokeContentData data, bool isFavorite) return; } - if (data is null) - { - throw new ArgumentNullException(nameof(data)); - } + ArgumentNullException.ThrowIfNull(data); Id = data.Id; Selftext = data.Selftext; diff --git a/src/app/ApplicationTemplate.Business/DadJokes/DadJokesService.cs b/src/app/ApplicationTemplate.Business/DadJokes/DadJokesService.cs index 76f0acb7e..f9f79ddd1 100644 --- a/src/app/ApplicationTemplate.Business/DadJokes/DadJokesService.cs +++ b/src/app/ApplicationTemplate.Business/DadJokes/DadJokesService.cs @@ -63,10 +63,7 @@ public async Task> GetFavorites(CancellationToken public async Task SetIsFavorite(CancellationToken ct, DadJokesQuote quote, bool isFavorite) { - if (quote is null) - { - throw new ArgumentNullException(nameof(quote)); - } + ArgumentNullException.ThrowIfNull(quote); var settings = await _applicationSettingsRepository.GetCurrent(ct); diff --git a/src/app/ApplicationTemplate.Business/KillSwitch/KillSwitchService.cs b/src/app/ApplicationTemplate.Business/KillSwitch/KillSwitchService.cs index 89d40584b..e1de08efd 100644 --- a/src/app/ApplicationTemplate.Business/KillSwitch/KillSwitchService.cs +++ b/src/app/ApplicationTemplate.Business/KillSwitch/KillSwitchService.cs @@ -26,5 +26,5 @@ public KillSwitchService(IKillSwitchRepository killSwitchRepository, ILogger public IObservable ObserveKillSwitchActivation() => _killSwitchRepository.ObserveKillSwitchActivation() - .Do(isActive => _logger.LogInformation("Kill switch is now {isActive}.", isActive)); + .Do(isActive => _logger.LogInformation("Kill switch is now {IsActive}.", isActive)); } diff --git a/src/app/ApplicationTemplate.Mobile/ApplicationTemplate.Mobile.csproj b/src/app/ApplicationTemplate.Mobile/ApplicationTemplate.Mobile.csproj index 520586f99..6678ad4b6 100644 --- a/src/app/ApplicationTemplate.Mobile/ApplicationTemplate.Mobile.csproj +++ b/src/app/ApplicationTemplate.Mobile/ApplicationTemplate.Mobile.csproj @@ -1,4 +1,4 @@ - + 12.0 net8.0-android;net8.0-ios @@ -18,8 +18,8 @@ - - + + @@ -28,14 +28,14 @@ - + - - - - - + + + + + @@ -100,7 +100,7 @@ - + @@ -156,6 +156,7 @@ + manual True $(MtouchExtraArgs) --setenv=MONO_LOG_LEVEL=debug --setenv=MONO_LOG_MASK=gc False @@ -199,7 +200,7 @@ - + diff --git a/src/app/ApplicationTemplate.Mobile/iOS/LinkerExclusions.xml b/src/app/ApplicationTemplate.Mobile/iOS/LinkerExclusions.xml index fd329b96c..8edb450a0 100644 --- a/src/app/ApplicationTemplate.Mobile/iOS/LinkerExclusions.xml +++ b/src/app/ApplicationTemplate.Mobile/iOS/LinkerExclusions.xml @@ -6,7 +6,7 @@ - + diff --git a/src/app/ApplicationTemplate.Presentation/ApplicationTemplate.Presentation.csproj b/src/app/ApplicationTemplate.Presentation/ApplicationTemplate.Presentation.csproj index 314bb5a2d..ea9273bbb 100644 --- a/src/app/ApplicationTemplate.Presentation/ApplicationTemplate.Presentation.csproj +++ b/src/app/ApplicationTemplate.Presentation/ApplicationTemplate.Presentation.csproj @@ -16,7 +16,7 @@ - + @@ -26,15 +26,15 @@ - - - - + + + + - - + + @@ -43,5 +43,4 @@ - diff --git a/src/app/ApplicationTemplate.Presentation/Configuration/ConfigurationConfiguration.cs b/src/app/ApplicationTemplate.Presentation/Configuration/ConfigurationConfiguration.cs index 239ea298a..36c244517 100644 --- a/src/app/ApplicationTemplate.Presentation/Configuration/ConfigurationConfiguration.cs +++ b/src/app/ApplicationTemplate.Presentation/Configuration/ConfigurationConfiguration.cs @@ -29,10 +29,7 @@ public static class ConfigurationConfiguration /// The environment manager. public static IHostBuilder AddConfiguration(this IHostBuilder hostBuilder, string folderPath, IEnvironmentManager environmentManager) { - if (hostBuilder is null) - { - throw new ArgumentNullException(nameof(hostBuilder)); - } + ArgumentNullException.ThrowIfNull(hostBuilder); return hostBuilder .AddConfiguration(environmentManager) @@ -124,10 +121,7 @@ private static IConfigurationBuilder AddUserOverrideConfiguration(this IConfigur /// The environment manager. private static IHostBuilder AddConfiguration(this IHostBuilder hostBuilder, IEnvironmentManager environmentManager) { - if (hostBuilder is null) - { - throw new ArgumentNullException(nameof(hostBuilder)); - } + ArgumentNullException.ThrowIfNull(hostBuilder); return hostBuilder.ConfigureServices((hostBuilderContext, serviceCollection) => serviceCollection .AddSingleton(serviceProvider => hostBuilderContext.Configuration) @@ -136,7 +130,7 @@ private static IHostBuilder AddConfiguration(this IHostBuilder hostBuilder, IEnv ); } - public class AppSettingsFile + public sealed class AppSettingsFile { private static AppSettingsFile[] _appSettingsFiles; diff --git a/src/app/ApplicationTemplate.Presentation/Configuration/EnvironmentManager.cs b/src/app/ApplicationTemplate.Presentation/Configuration/EnvironmentManager.cs index 79b693df0..24de1263b 100644 --- a/src/app/ApplicationTemplate.Presentation/Configuration/EnvironmentManager.cs +++ b/src/app/ApplicationTemplate.Presentation/Configuration/EnvironmentManager.cs @@ -1,8 +1,6 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using static ApplicationTemplate.ConfigurationConfiguration; namespace ApplicationTemplate; @@ -10,7 +8,7 @@ namespace ApplicationTemplate; /// /// This implementation of uses local files to support the override features. /// -public class EnvironmentManager : IEnvironmentManager +public sealed class EnvironmentManager : IEnvironmentManager { //-:cnd:noEmit #if PRODUCTION @@ -58,10 +56,7 @@ public void ClearOverride() public void Override(string environment) { - if (environment == null) - { - throw new ArgumentNullException(nameof(environment)); - } + ArgumentNullException.ThrowIfNull(environment); environment = environment.ToUpperInvariant(); diff --git a/src/app/ApplicationTemplate.Presentation/Framework/DynamicData/IChangeSet.Extensions.cs b/src/app/ApplicationTemplate.Presentation/Framework/DynamicData/IChangeSet.Extensions.cs index c1a6aa987..dd5d85d84 100644 --- a/src/app/ApplicationTemplate.Presentation/Framework/DynamicData/IChangeSet.Extensions.cs +++ b/src/app/ApplicationTemplate.Presentation/Framework/DynamicData/IChangeSet.Extensions.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; namespace DynamicData; @@ -8,10 +7,7 @@ public static class ChangeSetExtensions { public static IEnumerable GetAddedItems(this IChangeSet changeSet) { - if (changeSet is null) - { - throw new ArgumentNullException(nameof(changeSet)); - } + ArgumentNullException.ThrowIfNull(changeSet); foreach (var change in changeSet) { @@ -38,10 +34,7 @@ public static IEnumerable GetAddedItems(this IChangeSet changeSet) public static IEnumerable GetRemovedItems(this IChangeSet changeSet) { - if (changeSet is null) - { - throw new ArgumentNullException(nameof(changeSet)); - } + ArgumentNullException.ThrowIfNull(changeSet); foreach (var change in changeSet) { diff --git a/src/app/ApplicationTemplate.Presentation/Framework/ViewModels/IViewModel.Extensions.PropertyFromDynamicProperty.cs b/src/app/ApplicationTemplate.Presentation/Framework/ViewModels/IViewModel.Extensions.PropertyFromDynamicProperty.cs index 55901a91c..9753fea4a 100644 --- a/src/app/ApplicationTemplate.Presentation/Framework/ViewModels/IViewModel.Extensions.PropertyFromDynamicProperty.cs +++ b/src/app/ApplicationTemplate.Presentation/Framework/ViewModels/IViewModel.Extensions.PropertyFromDynamicProperty.cs @@ -1,9 +1,5 @@ using System; -using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Text; -using Chinook.DynamicMvvm; -using Chinook.DynamicMvvm.Implementations; namespace Chinook.DynamicMvvm; @@ -20,10 +16,7 @@ public static class ChinookViewModelExtensionsForPropertiesFromDynamicProperty /// The property's value. public static T GetFromDynamicProperty(this IViewModel viewModel, IDynamicProperty source, [CallerMemberName] string name = null) { - if (source is null) - { - throw new ArgumentNullException(nameof(source)); - } + ArgumentNullException.ThrowIfNull(source); return viewModel.Get(viewModel.GetOrCreateDynamicProperty(name, n => new ValueChangedOnBackgroundTaskDynamicPropertyFromDynamicProperty(name, source, viewModel, source.Value))); } @@ -41,10 +34,7 @@ public static T GetFromDynamicProperty(this IViewModel viewModel, IDynamicPro /// The property's value. public static TResult GetFromDynamicProperty(this IViewModel viewModel, IDynamicProperty source, Func selector, [CallerMemberName] string name = null) { - if (source is null) - { - throw new ArgumentNullException(nameof(source)); - } + ArgumentNullException.ThrowIfNull(source); return viewModel.Get(viewModel.GetOrCreateDynamicProperty(name, n => new ValueChangedOnBackgroundTaskDynamicPropertyFromDynamicProperty(name, source, viewModel, selector, selector(source.Value)))); diff --git a/src/app/ApplicationTemplate.Presentation/Framework/ViewModels/ValueChangedOnBackgroundTaskDynamicPropertyFromDynamicProperty.cs b/src/app/ApplicationTemplate.Presentation/Framework/ViewModels/ValueChangedOnBackgroundTaskDynamicPropertyFromDynamicProperty.cs index 07124fef0..3b71eff63 100644 --- a/src/app/ApplicationTemplate.Presentation/Framework/ViewModels/ValueChangedOnBackgroundTaskDynamicPropertyFromDynamicProperty.cs +++ b/src/app/ApplicationTemplate.Presentation/Framework/ViewModels/ValueChangedOnBackgroundTaskDynamicPropertyFromDynamicProperty.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using Chinook.DynamicMvvm.Implementations; namespace Chinook.DynamicMvvm; @@ -35,10 +33,7 @@ public override object Value get => base.Value; set { - if (_isDisposed) - { - throw new ObjectDisposedException(Name); - } + ObjectDisposedException.ThrowIf(_isDisposed, this); if (!Equals(value, base.Value)) { diff --git a/src/app/ApplicationTemplate.Presentation/ViewModels/DadJokes/DadJokesPageViewModel.cs b/src/app/ApplicationTemplate.Presentation/ViewModels/DadJokes/DadJokesPageViewModel.cs index b17138e27..8d705115f 100644 --- a/src/app/ApplicationTemplate.Presentation/ViewModels/DadJokes/DadJokesPageViewModel.cs +++ b/src/app/ApplicationTemplate.Presentation/ViewModels/DadJokes/DadJokesPageViewModel.cs @@ -14,7 +14,7 @@ namespace ApplicationTemplate.Presentation; -public partial class DadJokesPageViewModel : ViewModel +public sealed partial class DadJokesPageViewModel : ViewModel { [Inject] private IDadJokesService _dadJokesService; [Inject] private ISectionsNavigator _sectionsNavigator; @@ -67,7 +67,7 @@ private async Task SetupFavoritesUpdate(CancellationToken ct) void UpdateItemViewModels(IChangeSet changeSet) { var quotesVMs = Jokes.State.Data; - if (quotesVMs != null && quotesVMs.Any()) + if (quotesVMs != null && quotesVMs.Length != 0) { var addedItems = changeSet.GetAddedItems(); var removedItems = changeSet.GetRemovedItems(); diff --git a/src/app/ApplicationTemplate.Presentation/ViewModels/Diagnostics/DiagnosticsCountersService.cs b/src/app/ApplicationTemplate.Presentation/ViewModels/Diagnostics/DiagnosticsCountersService.cs index 2c0afd4c0..d2e44aafa 100644 --- a/src/app/ApplicationTemplate.Presentation/ViewModels/Diagnostics/DiagnosticsCountersService.cs +++ b/src/app/ApplicationTemplate.Presentation/ViewModels/Diagnostics/DiagnosticsCountersService.cs @@ -4,7 +4,7 @@ namespace ApplicationTemplate.Presentation; -public class DiagnosticsCountersService +public sealed class DiagnosticsCountersService { public event EventHandler CountersChanged; @@ -92,7 +92,7 @@ private void OnCommandNotification(KeyValuePair notification) } } -public partial class CountersData +public sealed partial class CountersData { public CountersData() { @@ -100,10 +100,7 @@ public CountersData() public CountersData(CountersData source) { - if (source is null) - { - throw new ArgumentNullException(nameof(source)); - } + ArgumentNullException.ThrowIfNull(source); CreatedViewModels = source.CreatedViewModels; DisposedViewModels = source.DisposedViewModels; diff --git a/src/app/ApplicationTemplate.Presentation/ViewModels/Diagnostics/HttpDebugger/HttpDebuggerViewModel.cs b/src/app/ApplicationTemplate.Presentation/ViewModels/Diagnostics/HttpDebugger/HttpDebuggerViewModel.cs index 49f2f3c7b..9725dc815 100644 --- a/src/app/ApplicationTemplate.Presentation/ViewModels/Diagnostics/HttpDebugger/HttpDebuggerViewModel.cs +++ b/src/app/ApplicationTemplate.Presentation/ViewModels/Diagnostics/HttpDebugger/HttpDebuggerViewModel.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.Reactive.Concurrency; @@ -9,7 +8,6 @@ using System.Text.Json; using ApplicationTemplate.DataAccess; using Chinook.DynamicMvvm; -using Chinook.DynamicMvvm.Deactivation; using DynamicData; using Uno; using Uno.Extensions; @@ -18,6 +16,8 @@ namespace ApplicationTemplate.Presentation; public sealed partial class HttpDebuggerViewModel : TabViewModel { + private static readonly JsonSerializerOptions _jsonSerializerOptions = new JsonSerializerOptions { WriteIndented = true }; + [Inject] private IHttpDebuggerService _httpDebuggerService; public HttpDebuggerViewModel() @@ -261,7 +261,7 @@ private static string TryFormatContent(string content, bool indentJson) var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(content)); if (JsonDocument.TryParseValue(ref reader, out var jsonDoc)) { - return JsonSerializer.Serialize(jsonDoc, new JsonSerializerOptions { WriteIndented = true }); + return JsonSerializer.Serialize(jsonDoc, _jsonSerializerOptions); } } diff --git a/src/app/ApplicationTemplate.Shared.Views/Content/DadJokes/DadJokesPage.xaml b/src/app/ApplicationTemplate.Shared.Views/Content/DadJokes/DadJokesPage.xaml index 9272332cb..76561d6a8 100644 --- a/src/app/ApplicationTemplate.Shared.Views/Content/DadJokes/DadJokesPage.xaml +++ b/src/app/ApplicationTemplate.Shared.Views/Content/DadJokes/DadJokesPage.xaml @@ -1,10 +1,10 @@ - @@ -103,18 +103,16 @@ - - - + - + diff --git a/src/app/ApplicationTemplate.Tests.Functional/ApplicationTemplate.Tests.Functional.csproj b/src/app/ApplicationTemplate.Tests.Functional/ApplicationTemplate.Tests.Functional.csproj index a11e79160..03082a82d 100644 --- a/src/app/ApplicationTemplate.Tests.Functional/ApplicationTemplate.Tests.Functional.csproj +++ b/src/app/ApplicationTemplate.Tests.Functional/ApplicationTemplate.Tests.Functional.csproj @@ -11,7 +11,7 @@ - + @@ -25,11 +25,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -38,5 +38,4 @@ - diff --git a/src/app/ApplicationTemplate.Tests.Functional/FunctionalTestBase.cs b/src/app/ApplicationTemplate.Tests.Functional/FunctionalTestBase.cs index 7edb9625b..062bbba10 100644 --- a/src/app/ApplicationTemplate.Tests.Functional/FunctionalTestBase.cs +++ b/src/app/ApplicationTemplate.Tests.Functional/FunctionalTestBase.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Globalization; using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Threading; @@ -180,7 +181,7 @@ private void ConfigureLogging(HostBuilderContext hostBuilderContext, ILoggingBui var serilogConfiguration = new LoggerConfiguration() .ReadFrom.Configuration(hostBuilderContext.Configuration) .Enrich.With(new ThreadIdEnricher()) - .WriteTo.TestOutput(_output, outputTemplate: "{Timestamp:HH:mm:ss.fff} Thread:{ThreadId} {Level:u1}/{SourceContext}: {Message:lj} {Exception}{NewLine}"); + .WriteTo.TestOutput(_output, outputTemplate: "{Timestamp:HH:mm:ss.fff} Thread:{ThreadId} {Level:u1}/{SourceContext}: {Message:lj} {Exception}{NewLine}", formatProvider: CultureInfo.InvariantCulture); var logger = serilogConfiguration.CreateLogger(); loggingBuilder.AddSerilog(logger); @@ -209,7 +210,6 @@ protected virtual IServiceCollection ReplaceWithMock(IServiceCollectio return services.Replace(ServiceDescriptor.Singleton(mockedService)); } - /// /// Gets the application settings to use for this test. /// diff --git a/src/app/ApplicationTemplate.Tests.Unit/ApplicationTemplate.Tests.Unit.csproj b/src/app/ApplicationTemplate.Tests.Unit/ApplicationTemplate.Tests.Unit.csproj index d27f9a97c..e52dbe62c 100644 --- a/src/app/ApplicationTemplate.Tests.Unit/ApplicationTemplate.Tests.Unit.csproj +++ b/src/app/ApplicationTemplate.Tests.Unit/ApplicationTemplate.Tests.Unit.csproj @@ -10,7 +10,7 @@ - + @@ -24,11 +24,11 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/app/ApplicationTemplate.Windows/ApplicationTemplate.Windows.csproj b/src/app/ApplicationTemplate.Windows/ApplicationTemplate.Windows.csproj index 9fa56137c..474dcd92b 100644 --- a/src/app/ApplicationTemplate.Windows/ApplicationTemplate.Windows.csproj +++ b/src/app/ApplicationTemplate.Windows/ApplicationTemplate.Windows.csproj @@ -70,8 +70,8 @@ - - + + @@ -86,8 +86,8 @@ - - + + @@ -128,5 +128,5 @@ - +