Skip to content
This repository was archived by the owner on Jun 5, 2025. It is now read-only.
415 changes: 365 additions & 50 deletions src/AzureExtension/DataManager/AzureDataManager.cs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/AzureExtension/DataManager/AzureDataManagerCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ private dynamic CreateUpdateEventContext(int errors, int accountsUpdated, int ac
private void UpdateOrganization(Account account, DeveloperId.DeveloperId developerId, VssConnection connection, CancellationToken cancellationToken)
{
// Update account identity information:
var identity = Identity.GetOrCreateIdentity(DataStore, connection.AuthorizedIdentity, connection, true);
var identity = Identity.GetOrCreateIdentity(DataStore, connection.AuthorizedIdentity, connection, developerId.LoginId);

_log.Verbose($"Updating organization: {account.AccountName}");
var organization = Organization.GetOrCreate(DataStore, account.AccountUri);
Expand Down
65 changes: 65 additions & 0 deletions src/AzureExtension/DataManager/AzureDataManagerUpdate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using DevHomeAzureExtension.DataManager;
using DevHomeAzureExtension.DataModel;
using Microsoft.TeamFoundation.Policy.WebApi;
using Serilog;

namespace DevHomeAzureExtension;

public partial class AzureDataManager
Comment thread
dkbennett marked this conversation as resolved.
{
// This is how frequently the DataStore update occurs.
private static readonly TimeSpan _updateInterval = TimeSpan.FromMinutes(5);
private static DateTime _lastUpdateTime = DateTime.MinValue;

public static async Task Update()
{
// Only update per the update interval.
// This is intended to be dynamic in the future.
if (DateTime.UtcNow - _lastUpdateTime < _updateInterval)
{
return;
}

try
{
await UpdateDeveloperPullRequests();
}
catch (Exception ex)
{
Log.Error(ex, "Update failed unexpectedly.");
}

_lastUpdateTime = DateTime.UtcNow;
}

public static async Task UpdateDeveloperPullRequests()
{
var log = Log.ForContext("SourceContext", $"UpdateDeveloperPullRequests");
log.Debug($"Executing UpdateDeveloperPullRequests");

var cacheManager = CacheManager.GetInstance();
if (cacheManager.UpdateInProgress)
{
log.Information("Cache is being updated, skipping Developer Pull Request Update");
return;
}

var identifier = Guid.NewGuid();
using var dataManager = CreateInstance(identifier.ToString()) ?? throw new DataStoreInaccessibleException();
await dataManager.UpdatePullRequestsForLoggedInDeveloperIdsAsync(null, identifier);

// Show any new notifications that were created from the pull request update.
var notifications = dataManager.GetNotifications();
foreach (var notification in notifications)
{
// Show notifications for failed checkruns for Developer users.
if (notification.Type == NotificationType.PullRequestRejected || notification.Type == NotificationType.PullRequestApproved)
{
notification.ShowToast();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public enum DataManagerUpdateKind
Error,
Cache,
Cancel,
Developer,
}

public class DataManagerUpdateEventArgs : EventArgs
Expand Down
10 changes: 10 additions & 0 deletions src/AzureExtension/DataManager/IAzureDataManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,22 @@ public interface IAzureDataManager : IDisposable

Task UpdateDataForPullRequestsAsync(AzureUri repositoryUri, string developerLogin, PullRequestView view, RequestOptions? options = null, Guid? requestor = null);

Task UpdatePullRequestsForLoggedInDeveloperIdsAsync(RequestOptions? options = null, Guid? requestor = null);

Query? GetQuery(string queryId, string developerId);

Query? GetQuery(AzureUri queryUri, string developerId);

Identity GetIdentity(long id);

WorkItemType GetWorkItemType(long id);

IEnumerable<Notification> GetNotifications(DateTime? since = null, bool includeToasted = false);

IEnumerable<Repository> GetRepositories();

IEnumerable<Repository> GetDeveloperRepositories();

// Repository name may not be unique across projects, and projects may not be unique across
// organizations, so we need all three to identify the repository.
PullRequests? GetPullRequests(string organization, string project, string repositoryName, string developerId, PullRequestView view);
Expand Down
47 changes: 43 additions & 4 deletions src/AzureExtension/DataModel/AzureDataStoreSchema.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public AzureDataStoreSchema()
}

// Update this anytime incompatible changes happen with a released version.
private const long SchemaVersionValue = 0x0006;
private const long SchemaVersionValue = 0x0007;

private const string Metadata =
@"CREATE TABLE Metadata (" +
Expand All @@ -30,7 +30,7 @@ public AzureDataStoreSchema()
"Name TEXT NOT NULL COLLATE NOCASE," +
"InternalId TEXT NOT NULL," +
"Avatar TEXT NOT NULL COLLATE NOCASE," +
"IsDeveloper INTEGER NOT NULL," +
"DeveloperLoginId TEXT," +
"TimeUpdated INTEGER NOT NULL" +
");" +

Expand Down Expand Up @@ -89,6 +89,10 @@ public AzureDataStoreSchema()
"TimeUpdated INTEGER NOT NULL" +
");" +

// While Name and ProjectId should be unique, it is possible renaming occurs and
// we might have a collision if we have cached a repository prior to rename and
// then encounter a different repository with that name. Therefore we will not
// create a unique index on ProjectId and Name.
// Repository InternalId is a Guid, so by definition is unique.
"CREATE UNIQUE INDEX IDX_Repository_InternalId ON Repository (InternalId);";

Expand Down Expand Up @@ -138,7 +142,7 @@ public AzureDataStoreSchema()
private const string PullRequests =
@"CREATE TABLE PullRequests (" +
"Id INTEGER PRIMARY KEY NOT NULL," +
"RepositoryName TEXT NOT NULL COLLATE NOCASE," +
"RepositoryId INTEGER NOT NULL," +
"DeveloperLogin TEXT NOT NULL COLLATE NOCASE," +
"Results TEXT NOT NULL," +
"ProjectId INTEGER NOT NULL," +
Expand All @@ -148,7 +152,40 @@ public AzureDataStoreSchema()

// Developer Pull requests are unique on Org / Project / Repository and
// the developer login, and the view.
"CREATE UNIQUE INDEX IDX_PullRequests_ProjectIdRepositoryNameDeveloperLoginViewId ON PullRequests (ProjectId, RepositoryName, DeveloperLogin, ViewId);";
"CREATE UNIQUE INDEX IDX_PullRequests_ProjectIdRepositoryIdDeveloperLoginViewId ON PullRequests (ProjectId, RepositoryId, DeveloperLogin, ViewId);";

// PullRequsetPolicyStatus is a snapshot of a developer's Pull Requests.
private const string PullRequestPolicyStatus =
@"CREATE TABLE PullRequestPolicyStatus (" +
"Id INTEGER PRIMARY KEY NOT NULL," +
"ArtifactId TEXT NULL COLLATE NOCASE," +
"ProjectId INTEGER NOT NULL," +
"RepositoryId INTEGER NOT NULL," +
"PullRequestId INTEGER NOT NULL," +
"Title TEXT NULL COLLATE NOCASE," +
"PolicyStatusId INTEGER NOT NULL," +
"PolicyStatusReason TEXT NULL COLLATE NOCASE," +
"PullRequestStatusId INTEGER NOT NULL," +
"TargetBranchName TEXT NULL COLLATE NOCASE," +
"HtmlUrl TEXT NULL COLLATE NOCASE," +
"TimeUpdated INTEGER NOT NULL," +
"TimeCreated INTEGER NOT NULL" +
");";

private const string Notification =
@"CREATE TABLE Notification (" +
"Id INTEGER PRIMARY KEY NOT NULL," +
"TypeId INTEGER NOT NULL," +
"ProjectId INTEGER NOT NULL," +
"RepositoryId INTEGER NOT NULL," +
"Title TEXT NOT NULL COLLATE NOCASE," +
"Description TEXT NOT NULL COLLATE NOCASE," +
"Identifier TEXT NULL COLLATE NOCASE," +
"Result TEXT NULL COLLATE NOCASE," +
"HtmlUrl TEXT NULL COLLATE NOCASE," +
"ToastState INTEGER NOT NULL," +
"TimeCreated INTEGER NOT NULL" +
");";

// All Sqls together.
private static readonly List<string> _schemaSqlsValue =
Expand All @@ -163,5 +200,7 @@ public AzureDataStoreSchema()
Query,
WorkItemType,
PullRequests,
PullRequestPolicyStatus,
Notification,
];
}
42 changes: 30 additions & 12 deletions src/AzureExtension/DataModel/DataObjects/Identity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Text.Json.Serialization;
using Dapper;
using Dapper.Contrib.Extensions;
using DevHomeAzureExtension.DeveloperId;
using DevHomeAzureExtension.Helpers;
using Microsoft.VisualStudio.Services.Profile;
using Microsoft.VisualStudio.Services.Profile.Client;
Expand All @@ -13,6 +14,7 @@

namespace DevHomeAzureExtension.DataModel;

// This represents an Azure DevOps Identity or IdentityRef.
[Table("Identity")]
public class Identity
{
Expand Down Expand Up @@ -42,9 +44,8 @@ public class Identity

public string Avatar { get; set; } = string.Empty;

// Represents whether this identity is associated with a DeveloperId that is logged in.
// This is the backing database column for the IsLoggedInDeveloper property.
public long IsDeveloper { get; set; } = DataStore.NoForeignKey;
// The DeveloperLoginId associated with this identity, if one exists.
public string? DeveloperLoginId { get; set; }

[JsonIgnore]
public long TimeUpdated { get; set; } = DataStore.NoForeignKey;
Expand All @@ -57,7 +58,23 @@ public class Identity
[Write(false)]
[Computed]
[JsonIgnore]
public bool IsLoggedInDeveloper => IsDeveloper != 0L;
public bool IsLoggedInDeveloper => !string.IsNullOrEmpty(DeveloperLoginId);

[Write(false)]
[Computed]
public DeveloperId.DeveloperId? DeveloperId
{
get
{
if (!IsLoggedInDeveloper)
{
return null;
}

var devIdProvider = DeveloperIdProvider.GetInstance();
return devIdProvider.GetDeveloperIdFromAccountIdentifier(DeveloperLoginId!);
}
}

public string ToJson() => JsonSerializer.Serialize(this);

Expand Down Expand Up @@ -145,19 +162,18 @@ private static Identity CreateFromIdentity(Microsoft.VisualStudio.Services.Ident
};
}

public static Identity AddOrUpdateIdentity(DataStore dataStore, Identity identity, bool isDeveloper = false)
public static Identity AddOrUpdateIdentity(DataStore dataStore, Identity identity, string? developerLoginId = null)
{
// Check for existing Identity data.
var existingIdentity = GetByInternalId(dataStore, identity.InternalId);
if (existingIdentity is not null)
{
identity.Id = existingIdentity.Id;

// If this is a developer, set to developer, but do not set to false.
// We presume not a developer unless it is explicitly set.
if (isDeveloper)
if (!string.IsNullOrEmpty(developerLoginId))
{
identity.IsDeveloper = 1;
identity.DeveloperLoginId = developerLoginId;
}

dataStore.Connection!.Update(identity);
Expand Down Expand Up @@ -192,7 +208,7 @@ public static Identity Get(DataStore? dataStore, long id)
}

// Creation from an Azure IdentityRef object.
public static Identity GetOrCreateIdentity(DataStore dataStore, IdentityRef? identityRef, VssConnection connection, bool isDeveloper = false)
public static Identity GetOrCreateIdentity(DataStore dataStore, IdentityRef? identityRef, VssConnection connection, string developerLoginId = "")
Comment thread
dkbennett marked this conversation as resolved.
{
ArgumentNullException.ThrowIfNull(identityRef);

Expand All @@ -210,18 +226,19 @@ public static Identity GetOrCreateIdentity(DataStore dataStore, IdentityRef? ide
// We don't want to create an identity object and download a new avatar unless it needs to
// be updated. In the event of an empty avatar we will retry more frequently to update it,
// but not every time.
var isDeveloper = !string.IsNullOrEmpty(developerLoginId);
if (existing is null || (isDeveloper && !existing.IsLoggedInDeveloper) || ((DateTime.UtcNow - existing.UpdatedAt) > _updateThreshold)
|| (string.IsNullOrEmpty(existing.Avatar) && ((DateTime.UtcNow - existing.UpdatedAt) > _avatarRetryDelay)))
{
var newIdentity = CreateFromIdentityRef(identityRef, connection);
return AddOrUpdateIdentity(dataStore, newIdentity, isDeveloper);
return AddOrUpdateIdentity(dataStore, newIdentity, developerLoginId);
}

return existing;
}

// Creation from an Azure Identity object.
public static Identity GetOrCreateIdentity(DataStore dataStore, Microsoft.VisualStudio.Services.Identity.Identity? identity, VssConnection connection, bool isDeveloper = false)
public static Identity GetOrCreateIdentity(DataStore dataStore, Microsoft.VisualStudio.Services.Identity.Identity? identity, VssConnection connection, string developerLoginId = "")
{
ArgumentNullException.ThrowIfNull(identity);

Expand All @@ -232,11 +249,12 @@ public static Identity GetOrCreateIdentity(DataStore dataStore, Microsoft.Visual
// We don't want to create an identity object and download a new avatar unlesss it needs to
// be updated. In the event of an empty avatar we will retry more frequently to update it,
// but not every time.
var isDeveloper = !string.IsNullOrEmpty(developerLoginId);
if (existing is null || (isDeveloper && !existing.IsLoggedInDeveloper) || ((DateTime.UtcNow - existing.UpdatedAt) > _updateThreshold)
|| (string.IsNullOrEmpty(existing.Avatar) && ((DateTime.UtcNow - existing.UpdatedAt) > _avatarRetryDelay)))
{
var newIdentity = CreateFromIdentity(identity, connection);
return AddOrUpdateIdentity(dataStore, newIdentity, isDeveloper);
return AddOrUpdateIdentity(dataStore, newIdentity, developerLoginId);
}

return existing;
Expand Down
Loading