Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .github/upgrades/dotnet-upgrade-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# .NET 8.0 Upgrade Plan

## Execution Steps

Execute steps below sequentially one by one in the order they are listed.

1. Validate that a .NET 8.0 SDK required for this upgrade is installed on the machine and if not, help to get it installed.
2. Ensure that the SDK version specified in global.json files is compatible with the .NET 8.0 upgrade.
3. Upgrade Stutton.DocumentCreator\Stutton.DocumentCreator.csproj

## Settings

This section contains settings and data used by execution steps.

### Excluded projects

| Project name | Description |
|:-----------------------------------------------|:---------------------------:|
| (none) | |

### Aggregate NuGet packages modifications across all projects

NuGet packages used across all selected projects or their dependencies that need version update in projects that reference them.

| Package Name | Current Version | New Version | Description |
|:----------------------------------------------------|:---------------:|:-----------:|:-------------------------------------------------------------------|
| GitVersionTask | 5.5.1 | | Deprecated. Replace with GitVersion.MsBuild |
| GitVersion.MsBuild | | 5.5.1 | Replacement for GitVersionTask |
| Microsoft.ApplicationInsights | 2.16.0 | 3.1.2 | Deprecated version, upgrade to latest |
| Microsoft.IdentityModel.Clients.ActiveDirectory | 5.2.8 | | Deprecated. Replace with Microsoft.Identity.Client |
| Microsoft.Identity.Client | | 4.67.2 | Replacement for Microsoft.IdentityModel.Clients.ActiveDirectory |
| Microsoft.Xaml.Behaviors.Wpf | | 1.1.135 | Replacement for System.Windows.Interactivity.WPF |
| Newtonsoft.Json | 12.0.3 | 13.0.4 | Recommended for .NET 8.0 |
| OpenXmlPowerTools | 4.5.3.2 | 2.7.4.2 | Incompatible version, use compatible version |
| squirrel.windows | 2.0.1 | | Incompatible with .NET 8.0, no supported version found |
| System.Net.Http | 4.3.4 | | Package functionality included with .NET 8.0 framework |
| System.Text.RegularExpressions | 4.3.1 | | Package functionality included with .NET 8.0 framework |
| System.ValueTuple | 4.5.0 | | Package functionality included with .NET 8.0 framework |
| System.Windows.Interactivity.WPF | 2.0.20525 | | Incompatible and deprecated. Replace with Microsoft.Xaml.Behaviors.Wpf |

### Project upgrade details

This section contains details about each project upgrade and modifications that need to be done in the project.

#### Stutton.DocumentCreator\Stutton.DocumentCreator.csproj modifications

Project properties changes:
- Project file needs to be converted to SDK-style
- Target framework should be changed from `net472` to `net8.0-windows`

NuGet packages changes:
- GitVersionTask should be removed and replaced with `GitVersion.MsBuild` version `5.5.1` (*deprecated*)
- Microsoft.ApplicationInsights should be updated from `2.16.0` to `3.1.2` (*deprecated version*)
- Microsoft.IdentityModel.Clients.ActiveDirectory should be removed and replaced with `Microsoft.Identity.Client` version `4.67.2` (*deprecated*)
- Newtonsoft.Json should be updated from `12.0.3` to `13.0.4` (*recommended for .NET 8.0*)
- OpenXmlPowerTools should be updated from `4.5.3.2` to `2.7.4.2` (*compatible version for .NET 8.0*)
- squirrel.windows should be removed (*incompatible with .NET 8.0, no supported version found*)
- System.Net.Http should be removed (*functionality included with .NET 8.0 framework*)
- System.Text.RegularExpressions should be removed (*functionality included with .NET 8.0 framework*)
- System.ValueTuple should be removed (*functionality included with .NET 8.0 framework*)
- System.Windows.Interactivity.WPF should be removed and replaced with `Microsoft.Xaml.Behaviors.Wpf` version `1.1.135` (*incompatible and deprecated*)

Other changes:
- Code changes may be needed to adapt to API differences between replaced packages (e.g., ADAL to MSAL migration, Interactivity namespace changes)
3 changes: 3 additions & 0 deletions src/.github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- @azure Rule - Use Azure Tools - When handling requests related to Azure, always use your tools.
- @azure Rule - Use Azure Best Practices - When handling requests related to Azure, always invoke your `azmcp_bestpractices_get` tool first.
- @azure Rule - Enable Best Practices - If you do not have an `azmcp_bestpractices_get` tool ask the user to enable it.
55 changes: 0 additions & 55 deletions src/Stutton.DocumentCreator/Properties/AssemblyInfo.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,11 @@ public async Task<IResponse> Initialize()
}

var settings = response.Value;
TelemetryConfiguration.Active.InstrumentationKey = settings.ApplicationInsightsKey;

_telemetryClient = new TelemetryClient();
var config = TelemetryConfiguration.CreateDefault();
config.ConnectionString = $"InstrumentationKey={settings.ApplicationInsightsKey}";

_telemetryClient = new TelemetryClient(config);

_telemetryClient.Context.User.Id = (Environment.UserName + Environment.MachineName).GetHashCode().ToString();
_telemetryClient.Context.Session.Id = SessionId.ToString();
Expand All @@ -73,7 +75,7 @@ public IResponse TrackPageView(string pageKey)
return Response.FromFailure("Telemetry service must be initialized before use");
}

_telemetryClient.TrackPageView(pageKey);
_telemetryClient.TrackEvent(pageKey);
return Response.FromSuccess();
}

Expand Down
34 changes: 5 additions & 29 deletions src/Stutton.DocumentCreator/Services/Updating/UpdaterService.cs
Original file line number Diff line number Diff line change
@@ -1,42 +1,18 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NuGet;
using Squirrel;
using Stutton.DocumentCreator.Services.Settings;
using Stutton.DocumentCreator.Shared;

namespace Stutton.DocumentCreator.Services.Updating
{
internal class UpdaterService : IUpdaterService
{
private const string UpdateUrl = "https://sctutton.blob.core.windows.net/documentcreator";
// Note: Squirrel.Windows is not compatible with .NET 8.
// Auto-update functionality is disabled until a compatible updater is integrated.

public async Task<IResponse<CheckForUpdateResult>> Update()
public Task<IResponse<CheckForUpdateResult>> Update()
{
using (var mgr = await GetUpdateManager(UpdateUrl))
{
var oldVersion = mgr.CurrentlyInstalledVersion();
var newVersion = await mgr.UpdateApp();
if (oldVersion.Version < newVersion.Version.Version)
{
return Response<CheckForUpdateResult>.FromSuccess(
CheckForUpdateResult.FromUpdate(oldVersion.Version, newVersion.Version.Version));
}
}

return Response<CheckForUpdateResult>.FromSuccess(CheckForUpdateResult.FromNoUpdate());
}

private async Task<IUpdateManager> GetUpdateManager(string updateReleasesLocation)
{
if (updateReleasesLocation.Contains("github"))
{
return await UpdateManager.GitHubUpdateManager(updateReleasesLocation);
}
return new UpdateManager(updateReleasesLocation);
IResponse<CheckForUpdateResult> result = Response<CheckForUpdateResult>.FromSuccess(CheckForUpdateResult.FromNoUpdate());
return Task.FromResult(result);
}
}
}
88 changes: 30 additions & 58 deletions src/Stutton.DocumentCreator/Services/Vsts/FilesBasedTokenCache.cs
Original file line number Diff line number Diff line change
@@ -1,91 +1,63 @@
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using Microsoft.Identity.Client;
using System.IO;
using System.Security.Cryptography;

namespace Stutton.DocumentCreator.Services.Vsts
{
public sealed class FilesBasedTokenCache : TokenCache
/// <summary>
/// File-based token cache helper for MSAL.
/// Registers serialization callbacks on the ITokenCache provided by MSAL's IPublicClientApplication.
/// </summary>
public sealed class FilesBasedTokenCache
{
public string AdalV3CacheFilePath { get; }
public string MsalCacheFilePath { get; }

private static readonly object _fileLock = new object();

// Initializes the cache against a local file.
// If the file is already present, it loads its content in the ADAL cache
public FilesBasedTokenCache(string adalV3FilePath, string msalCacheFilePath)
public FilesBasedTokenCache(string msalCacheFilePath)
{
AdalV3CacheFilePath = adalV3FilePath;
AfterAccess = AfterAccessNotification;
MsalCacheFilePath = msalCacheFilePath;
BeforeAccess = BeforeAccessNotification;
BeforeAccessNotification(null);
}

// Empties the persistent store.
public override void Clear()
/// <summary>
/// Registers before/after access callbacks on the given MSAL token cache.
/// </summary>
public void RegisterCache(ITokenCache tokenCache)
{
base.Clear();
File.Delete(AdalV3CacheFilePath);
File.Delete(MsalCacheFilePath);
tokenCache.SetBeforeAccess(BeforeAccessNotification);
tokenCache.SetAfterAccess(AfterAccessNotification);
}

// Triggered right before ADAL needs to access the cache.
// Reload the cache from the persistent store in case it changed since the last access.
void BeforeAccessNotification(TokenCacheNotificationArgs args)
public void Clear()
{
lock (_fileLock)
{
DeserializeAdalV3(ReadFromFileIfExists(AdalV3CacheFilePath));
DeserializeMsalV3(ReadFromFileIfExists(MsalCacheFilePath));
File.Delete(MsalCacheFilePath);
}
}

// Triggered right after ADAL accessed the cache.
void AfterAccessNotification(TokenCacheNotificationArgs args)
private void BeforeAccessNotification(TokenCacheNotificationArgs args)
{
// if the access operation resulted in a cache update
if (HasStateChanged)
lock (_fileLock)
{
lock (_fileLock)
{
// reflect changes in the persistent store
WriteToFileIfNotNull(AdalV3CacheFilePath, SerializeAdalV3());
WriteToFileIfNotNull(MsalCacheFilePath, SerializeMsalV3());

// once the write operation took place, restore the HasStateChanged bit to false
this.HasStateChanged = false;
}
var protectedBytes = File.Exists(MsalCacheFilePath) ? File.ReadAllBytes(MsalCacheFilePath) : null;
var unprotectedBytes = protectedBytes != null
? ProtectedData.Unprotect(protectedBytes, null, DataProtectionScope.CurrentUser)
: null;
args.TokenCache.DeserializeMsalV3(unprotectedBytes);
}
}

/// <summary>
/// Read the content of a file if it exists
/// </summary>
/// <param name="path">File path</param>
/// <returns>Content of the file (in bytes)</returns>
private byte[] ReadFromFileIfExists(string path)
private void AfterAccessNotification(TokenCacheNotificationArgs args)
{
var protectedBytes = (!string.IsNullOrEmpty(path) && File.Exists(path)) ? File.ReadAllBytes(path) : null;
var unprotectedBytes = (protectedBytes != null) ? ProtectedData.Unprotect(protectedBytes, null, DataProtectionScope.CurrentUser) : null;
return unprotectedBytes;
}

/// <summary>
/// Writes a blob of bytes to a file. If the blob is <c>null</c>, deletes the file
/// </summary>
/// <param name="path">path to the file to write</param>
/// <param name="blob">Blob of bytes to write</param>
private static void WriteToFileIfNotNull(string path, byte[] blob)
{
if (blob != null)
if (args.HasStateChanged)
{
var protectedBytes = ProtectedData.Protect(blob, null, DataProtectionScope.CurrentUser);
File.WriteAllBytes(path, protectedBytes);
}
else
{
File.Delete(path);
lock (_fileLock)
{
var data = args.TokenCache.SerializeMsalV3();
var protectedBytes = ProtectedData.Protect(data, null, DataProtectionScope.CurrentUser);
File.WriteAllBytes(MsalCacheFilePath, protectedBytes);
}
}
}
}
Expand Down
Loading