diff --git a/src/dotnet/APIView/APIViewWeb/Controllers/CommentsController.cs b/src/dotnet/APIView/APIViewWeb/Controllers/CommentsController.cs index 7ba499dd5a7..1f6a2e43033 100644 --- a/src/dotnet/APIView/APIViewWeb/Controllers/CommentsController.cs +++ b/src/dotnet/APIView/APIViewWeb/Controllers/CommentsController.cs @@ -39,7 +39,6 @@ public async Task Add(string reviewId, string revisionId, string e foreach(string user in taggedUsers) { comment.TaggedUsers.Add(user); - await _notificationManager.NotifyUserOnCommentTag(user, comment); } await _commentsManager.AddCommentAsync(User, comment); diff --git a/src/dotnet/APIView/APIViewWeb/Managers/CommentsManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/CommentsManager.cs index f94a44e9a10..38970c695eb 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/CommentsManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/CommentsManager.cs @@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Authorization; using Newtonsoft.Json; using Microsoft.Extensions.Options; +using Microsoft.TeamFoundation.Common; namespace APIViewWeb.Managers { @@ -91,6 +92,7 @@ public async Task AddCommentAsync(ClaimsPrincipal user, CommentModel comment) await _commentsRepository.UpsertCommentAsync(comment); if (!comment.IsResolve) { + await _notificationManager.NotifyUserOnCommentTag(comment); await _notificationManager.NotifySubscribersOnComment(user, comment); } } @@ -103,18 +105,16 @@ public async Task UpdateCommentAsync(ClaimsPrincipal user, string comment.Comment = commentText; comment.Username = user.GetGitHubLogin(); - var newTaggedUsers = new HashSet(); foreach (var taggedUser in taggedUsers) { - if (!comment.TaggedUsers.Contains(taggedUser)) + if (!string.IsNullOrEmpty(taggedUser)) { - await _notificationManager.NotifyUserOnCommentTag(taggedUser, comment); + comment.TaggedUsers.Add(taggedUser); } - newTaggedUsers.Add(taggedUser); } - comment.TaggedUsers = newTaggedUsers; await _commentsRepository.UpsertCommentAsync(comment); + await _notificationManager.NotifyUserOnCommentTag(comment); await _notificationManager.NotifySubscribersOnComment(user, comment); return comment; } diff --git a/src/dotnet/APIView/APIViewWeb/Managers/INotificationManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/INotificationManager.cs index 330b7d77dee..840383805d6 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/INotificationManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/INotificationManager.cs @@ -8,7 +8,7 @@ namespace APIViewWeb.Managers public interface INotificationManager { public Task NotifySubscribersOnComment(ClaimsPrincipal user, CommentModel comment); - public Task NotifyUserOnCommentTag(string username, CommentModel comment); + public Task NotifyUserOnCommentTag(CommentModel comment); public Task NotifyApproversOfReview(ClaimsPrincipal user, string reviewId, HashSet reviewers); public Task NotifySubscribersOnNewRevisionAsync(ReviewRevisionModel revision, ClaimsPrincipal user); public Task ToggleSubscribedAsync(ClaimsPrincipal user, string reviewId); diff --git a/src/dotnet/APIView/APIViewWeb/Managers/NotificationManager.cs b/src/dotnet/APIView/APIViewWeb/Managers/NotificationManager.cs index 8f46f38d09e..19ec39eb21a 100644 --- a/src/dotnet/APIView/APIViewWeb/Managers/NotificationManager.cs +++ b/src/dotnet/APIView/APIViewWeb/Managers/NotificationManager.cs @@ -4,52 +4,61 @@ using APIView.Identity; using APIViewWeb.Models; using Microsoft.Extensions.Configuration; -using SendGrid; -using SendGrid.Helpers.Mail; using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Security.Claims; using System.Text; using System.Threading.Tasks; -using Microsoft.TeamFoundation.Common; using APIViewWeb.Repositories; +using Microsoft.ApplicationInsights.Extensibility; +using Microsoft.ApplicationInsights; +using System.Net.Http; +using System.Net.Http.Json; namespace APIViewWeb.Managers { public class NotificationManager : INotificationManager { - private readonly string _endpoint; + private readonly string _apiviewEndpoint; private readonly ICosmosReviewRepository _reviewRepository; private readonly ICosmosUserProfileRepository _userProfileRepository; - private readonly ISendGridClient _sendGridClient; + private readonly IConfiguration _configuration; + private readonly string _testEmailToAddress; + private readonly string _emailSenderServiceUrl; private const string ENDPOINT_SETTING = "Endpoint"; - private const string SENDGRID_KEY_SETTING = "SendGrid:Key"; - private const string FROM_ADDRESS = "apiview-noreply@microsoft.com"; - private const string REPLY_TO_HEADER = "In-Reply-To"; - private const string REFERENCES_HEADER = "References"; - public NotificationManager(IConfiguration configuration, ICosmosReviewRepository reviewRepository, - ICosmosUserProfileRepository userProfileRepository, ISendGridClient sendGridClient = null) + static TelemetryClient _telemetryClient = new(TelemetryConfiguration.CreateDefault()); + + public NotificationManager(IConfiguration configuration, + ICosmosReviewRepository reviewRepository, + ICosmosUserProfileRepository userProfileRepository) { - _sendGridClient = sendGridClient ?? new SendGridClient(configuration[SENDGRID_KEY_SETTING]); - _endpoint = configuration.GetValue(ENDPOINT_SETTING); + _apiviewEndpoint = configuration.GetValue(ENDPOINT_SETTING); _reviewRepository = reviewRepository; _userProfileRepository = userProfileRepository; + _configuration = configuration; + _testEmailToAddress = configuration["apiview-email-test-address"] ?? ""; + _emailSenderServiceUrl = configuration["azure-sdk-emailer-url"] ?? ""; } public async Task NotifySubscribersOnComment(ClaimsPrincipal user, CommentModel comment) { var review = await _reviewRepository.GetReviewAsync(comment.ReviewId); - await SendEmailsAsync(review, user, GetPlainTextContent(comment), GetHtmlContent(comment, review)); + await SendEmailsAsync(review, user, GetHtmlContent(comment, review), comment.TaggedUsers); } - public async Task NotifyUserOnCommentTag(string username, CommentModel comment) + public async Task NotifyUserOnCommentTag(CommentModel comment) { - var review = await _reviewRepository.GetReviewAsync(comment.ReviewId); - var user = await _userProfileRepository.TryGetUserProfileAsync(username); - await SendUserEmailsAsync(review, user, GetCommentTagPlainTextContent(comment), GetCommentTagHtmlContent(comment, review)); + foreach (string username in comment.TaggedUsers) + { + if(string.IsNullOrEmpty(username)) continue; + var review = await _reviewRepository.GetReviewAsync(comment.ReviewId); + var user = await _userProfileRepository.TryGetUserProfileAsync(username); + await SendUserEmailsAsync(review, user, GetCommentTagHtmlContent(comment, review)); + } } public async Task NotifyApproversOfReview(ClaimsPrincipal user, string reviewId, HashSet reviewers) @@ -60,7 +69,6 @@ public async Task NotifyApproversOfReview(ClaimsPrincipal user, string reviewId, { var reviewerProfile = await _userProfileRepository.TryGetUserProfileAsync(reviewer); await SendUserEmailsAsync(review, reviewerProfile, - GetApproverReviewContentHeading(userProfile, false), GetApproverReviewHtmlContent(userProfile, review)); } } @@ -68,12 +76,10 @@ await SendUserEmailsAsync(review, reviewerProfile, public async Task NotifySubscribersOnNewRevisionAsync(ReviewRevisionModel revision, ClaimsPrincipal user) { var review = revision.Review; - var uri = new Uri($"{_endpoint}/Assemblies/Review/{review.ReviewId}"); - var plainTextContent = $"A new revision, {revision.DisplayName}," + - $" was uploaded by {revision.Author}."; + var uri = new Uri($"{_apiviewEndpoint}/Assemblies/Review/{review.ReviewId}"); var htmlContent = $"A new revision, {revision.DisplayName}," + $" was uploaded by {revision.Author}."; - await SendEmailsAsync(review, user, plainTextContent, htmlContent); + await SendEmailsAsync(review, user, htmlContent, null); } public async Task ToggleSubscribedAsync(ClaimsPrincipal user, string reviewId) @@ -116,10 +122,10 @@ public static string GetUserEmail(ClaimsPrincipal user) => private string GetApproverReviewHtmlContent(UserProfileModel user, ReviewModel review) { var reviewName = review.Name; - var reviewLink = new Uri($"{_endpoint}/Assemblies/Review/{review.ReviewId}"); + var reviewLink = new Uri($"{_apiviewEndpoint}/Assemblies/Review/{review.ReviewId}"); var poster = user.UserName; - var userLink = new Uri($"{_endpoint}/Assemblies/Profile/{poster}"); - var requestsLink = new Uri($"{_endpoint}/Assemblies/RequestedReviews/"); + var userLink = new Uri($"{_apiviewEndpoint}/Assemblies/Profile/{poster}"); + var requestsLink = new Uri($"{_apiviewEndpoint}/Assemblies/RequestedReviews/"); var sb = new StringBuilder(); sb.Append($"{poster}"); sb.Append($" requested you to review {reviewName}"); @@ -131,10 +137,10 @@ private string GetApproverReviewHtmlContent(UserProfileModel user, ReviewModel r private string GetCommentTagHtmlContent(CommentModel comment, ReviewModel review) { var reviewName = review.Name; - var reviewLink = new Uri($"{_endpoint}/Assemblies/Review/{review.ReviewId}#{Uri.EscapeDataString(comment.ElementId)}"); + var reviewLink = new Uri($"{_apiviewEndpoint}/Assemblies/Review/{review.ReviewId}#{Uri.EscapeDataString(comment.ElementId)}"); var commentText = comment.Comment; var poster = comment.Username; - var userLink = new Uri($"{_endpoint}/Assemblies/Profile/{poster}"); + var userLink = new Uri($"{_apiviewEndpoint}/Assemblies/Profile/{poster}"); var sb = new StringBuilder(); sb.Append($"{poster}"); sb.Append($" mentioned you in {reviewName}"); @@ -148,7 +154,7 @@ private string GetCommentTagHtmlContent(CommentModel comment, ReviewModel review private string GetHtmlContent(CommentModel comment, ReviewModel review) { - var uri = new Uri($"{_endpoint}/Assemblies/Review/{review.ReviewId}#{Uri.EscapeDataString(comment.ElementId)}"); + var uri = new Uri($"{_apiviewEndpoint}/Assemblies/Review/{review.ReviewId}#{Uri.EscapeDataString(comment.ElementId)}"); var sb = new StringBuilder(); sb.Append(GetContentHeading(comment, true)); sb.Append("

"); @@ -158,94 +164,84 @@ private string GetHtmlContent(CommentModel comment, ReviewModel review) return sb.ToString(); } - private static string GetCommentTagPlainTextContent(CommentModel comment) - { - var sb = new StringBuilder(); - sb.Append(GetCommentTagContentHeading(comment, false)); - return sb.ToString(); - } - - private string GetPlainTextContent(CommentModel comment) - { - var sb = new StringBuilder(); - sb.Append(GetContentHeading(comment, false)); - sb.Append("\r\n"); - sb.Append(CommentMarkdownExtensions.MarkdownAsPlainText(comment.Comment)); - return sb.ToString(); - } - - private static string GetApproverReviewContentHeading(UserProfileModel user, bool includeHtml) => - $"{(includeHtml ? $"{user.UserName}" : $"{user.UserName}")} requested you to review API."; - - private static string GetCommentTagContentHeading(CommentModel comment, bool includeHtml) => - $"{(includeHtml ? $"{comment.Username}" : $"{comment.Username}")} tagged you in a comment."; - private static string GetContentHeading(CommentModel comment, bool includeHtml) => $"{(includeHtml ? $"{comment.Username}" : $"{comment.Username}")} commented on this review."; - private async Task SendUserEmailsAsync(ReviewModel review, UserProfileModel user, string plainTextContent, string htmlContent) + private async Task SendUserEmailsAsync(ReviewModel review, UserProfileModel user, string htmlContent) { - var userBackup = new ClaimsPrincipal(); - EmailAddress e; - if (!user.Email.IsNullOrEmpty()) - { - e = new EmailAddress(user.Email, user.UserName); - } - else + // Always send email to a test address when test address is configured. + if (string.IsNullOrEmpty(user.Email)) { - var backupEmail = GetUserEmail(userBackup); - if (!backupEmail.IsNullOrEmpty()) - { - e = new EmailAddress(backupEmail, user.UserName); - } - else - { - return; - } + _telemetryClient.TrackTrace($"Email address is not available for user {user.UserName}. Not sending email."); + return; } - var from = new EmailAddress(FROM_ADDRESS); - var msg = MailHelper.CreateSingleEmail( - from, - e, - user.UserName, - plainTextContent, - htmlContent); - var threadHeader = $"<{review.ReviewId}{FROM_ADDRESS}>"; - msg.AddHeader(REPLY_TO_HEADER, threadHeader); - msg.AddHeader(REFERENCES_HEADER, threadHeader); - await _sendGridClient.SendEmailAsync(msg); - } - private async Task SendEmailsAsync(ReviewModel review, ClaimsPrincipal user, string plainTextContent, string htmlContent) + + await SendEmail(user.Email, $"Notification from APIView - {review.DisplayName}", htmlContent); + } + private async Task SendEmailsAsync(ReviewModel review, ClaimsPrincipal user, string htmlContent, ISet notifiedUsers) { var initiatingUserEmail = GetUserEmail(user); + // Find email address of already tagged users in comment + HashSet notifiedEmails = new HashSet(); + foreach(var usename in notifiedUsers) + { + var email = await GetEmailAddress(usename); + notifiedEmails.Add(email); + } var subscribers = review.Subscribers.ToList() - .Where(e => e != initiatingUserEmail) // don't include the initiating user in the email - .Select(e => new EmailAddress(e)) + .Where(e => e != initiatingUserEmail && !notifiedEmails.Contains(e)) // don't include the initiating user and tagged users in the comment .ToList(); if (subscribers.Count == 0) { return; } - var from = new EmailAddress(FROM_ADDRESS, GetUserName(user)); - var msg = MailHelper.CreateMultipleEmailsToMultipleRecipients( - from, - subscribers, - Enumerable.Repeat(review.DisplayName, review.Subscribers.Count).ToList(), - plainTextContent, - htmlContent, - Enumerable.Repeat(new Dictionary(), review.Subscribers.Count).ToList()); - var threadHeader = $"<{review.ReviewId}{FROM_ADDRESS}>"; - msg.AddHeader(REPLY_TO_HEADER, threadHeader); - msg.AddHeader(REFERENCES_HEADER, threadHeader); - await _sendGridClient.SendEmailAsync(msg); + var emailToList = string.Join(",", subscribers); + if (string.IsNullOrEmpty(emailToList)) + { + _telemetryClient.TrackTrace($"Email address is not available for subscribers, review {review.ReviewId}. Not sending email."); + return; + } + + await SendEmail(emailToList, $"Update on APIView - {review.DisplayName} from {GetUserName(user)}", htmlContent); } + private async Task SendEmail(string emailToList, string subject, string content) + { + if (string.IsNullOrEmpty(_emailSenderServiceUrl)) + { + _telemetryClient.TrackTrace($"Email sender service URL is not configured. Email will not be sent to {emailToList} with subject: {subject}"); + return; + } + + var requestBody = new EmailModel(_testEmailToAddress ?? emailToList, subject, content); + var httpClient = new HttpClient(); + try + { + var response = await httpClient.PostAsync(_emailSenderServiceUrl, JsonContent.Create(requestBody)); + if (response.StatusCode != HttpStatusCode.OK && response.StatusCode != HttpStatusCode.Accepted) + { + _telemetryClient.TrackTrace($"Failed to send email to user {emailToList} with subject: {subject}, status code: {response.StatusCode}, Details: {response.ToString}"); + } + } + catch (Exception ex) + { + _telemetryClient.TrackException(ex); + } + } private static string GetUserName(ClaimsPrincipal user) { var name = user.FindFirstValue(ClaimConstants.Name); return string.IsNullOrEmpty(name) ? user.FindFirstValue(ClaimConstants.Login) : name; } + + private async Task GetEmailAddress(string username) + { + if (string.IsNullOrEmpty(username)) + return ""; + var user = await _userProfileRepository.TryGetUserProfileAsync(username); + return user.Email; + } } } diff --git a/src/dotnet/APIView/APIViewWeb/Models/CommentModel.cs b/src/dotnet/APIView/APIViewWeb/Models/CommentModel.cs index 1a84e52ea6d..fda162c07e1 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/CommentModel.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/CommentModel.cs @@ -1,4 +1,6 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; using System.Collections.Generic; using Newtonsoft.Json; diff --git a/src/dotnet/APIView/APIViewWeb/Models/CommentThreadModel.cs b/src/dotnet/APIView/APIViewWeb/Models/CommentThreadModel.cs index afb8f5333d1..9e06446e552 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/CommentThreadModel.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/CommentThreadModel.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Collections.Generic; using System.Linq; namespace APIViewWeb.Models diff --git a/src/dotnet/APIView/APIViewWeb/Models/EmailModel.cs b/src/dotnet/APIView/APIViewWeb/Models/EmailModel.cs new file mode 100644 index 00000000000..319a9d177ce --- /dev/null +++ b/src/dotnet/APIView/APIViewWeb/Models/EmailModel.cs @@ -0,0 +1,26 @@ +using System.Text.Json.Serialization; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using Newtonsoft.Json; + +namespace APIViewWeb.Models +{ + public class EmailModel + { + public string EmailTo { get; set; } + public string Subject { get; set; } + public string Body { get; set; } + + public EmailModel(string emailTo, string subject, string body) + { + EmailTo = emailTo; + Subject = subject; + Body = body; + } + + public string Serialize() + { + return JsonConvert.SerializeObject(this); + } + } +} diff --git a/src/dotnet/APIView/APIViewWeb/Models/GithubUser.cs b/src/dotnet/APIView/APIViewWeb/Models/GithubUser.cs index 5dcae02780e..717d27b7369 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/GithubUser.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/GithubUser.cs @@ -1,4 +1,6 @@ -using Newtonsoft.Json; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using Newtonsoft.Json; namespace APIViewWeb.Models { diff --git a/src/dotnet/APIView/APIViewWeb/Models/OpenSourceUserInfo.cs b/src/dotnet/APIView/APIViewWeb/Models/OpenSourceUserInfo.cs index 2f9ad811513..2c3b20c4d0b 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/OpenSourceUserInfo.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/OpenSourceUserInfo.cs @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. using System.Collections.Generic; using Newtonsoft.Json; diff --git a/src/dotnet/APIView/APIViewWeb/Models/PackageGroupMdel.cs b/src/dotnet/APIView/APIViewWeb/Models/PackageGroupMdel.cs index fc9ed6ddfe2..bbb7541f847 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/PackageGroupMdel.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/PackageGroupMdel.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Collections.Generic; namespace APIViewWeb.Models { diff --git a/src/dotnet/APIView/APIViewWeb/Models/PackageModel.cs b/src/dotnet/APIView/APIViewWeb/Models/PackageModel.cs index 45e1e1ff9b9..d3a7f584352 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/PackageModel.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/PackageModel.cs @@ -1,4 +1,6 @@ -using CsvHelper.Configuration.Attributes; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using CsvHelper.Configuration.Attributes; namespace APIViewWeb.Models { diff --git a/src/dotnet/APIView/APIViewWeb/Models/ReviewDisplayModel.cs b/src/dotnet/APIView/APIViewWeb/Models/ReviewDisplayModel.cs index 9b4a425619d..bd86082f88c 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/ReviewDisplayModel.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/ReviewDisplayModel.cs @@ -1,4 +1,6 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System; namespace APIViewWeb.Models { diff --git a/src/dotnet/APIView/APIViewWeb/Models/ReviewGenPipelineParamModel.cs b/src/dotnet/APIView/APIViewWeb/Models/ReviewGenPipelineParamModel.cs index 1efd454fec3..bd3c42453ef 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/ReviewGenPipelineParamModel.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/ReviewGenPipelineParamModel.cs @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. namespace APIViewWeb.Models { public class ReviewGenPipelineParamModel diff --git a/src/dotnet/APIView/APIViewWeb/Models/ReviewType.cs b/src/dotnet/APIView/APIViewWeb/Models/ReviewType.cs index adfc09db149..ea3c45adb3f 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/ReviewType.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/ReviewType.cs @@ -1,5 +1,3 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. diff --git a/src/dotnet/APIView/APIViewWeb/Models/ServiceGroupModel.cs b/src/dotnet/APIView/APIViewWeb/Models/ServiceGroupModel.cs index 4ce8d8672a1..b321e081060 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/ServiceGroupModel.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/ServiceGroupModel.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using System.Collections.Generic; namespace APIViewWeb.Models { diff --git a/src/dotnet/APIView/APIViewWeb/Models/UsageSampleModel.cs b/src/dotnet/APIView/APIViewWeb/Models/UsageSampleModel.cs index 52831e42b8f..fa5f967c8f9 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/UsageSampleModel.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/UsageSampleModel.cs @@ -1,4 +1,6 @@ -using Newtonsoft.Json; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +using Newtonsoft.Json; using System.Collections.Generic; namespace APIViewWeb diff --git a/src/dotnet/APIView/APIViewWeb/Models/UserPreferenceModel.cs b/src/dotnet/APIView/APIViewWeb/Models/UserPreferenceModel.cs index 80b3f5a2079..d4a9d26c92e 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/UserPreferenceModel.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/UserPreferenceModel.cs @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. using System.Collections.Generic; using CsvHelper.Configuration.Attributes; using Newtonsoft.Json; diff --git a/src/dotnet/APIView/APIViewWeb/Models/UserProfileModel.cs b/src/dotnet/APIView/APIViewWeb/Models/UserProfileModel.cs index 946f38132d4..1a1a55a40d6 100644 --- a/src/dotnet/APIView/APIViewWeb/Models/UserProfileModel.cs +++ b/src/dotnet/APIView/APIViewWeb/Models/UserProfileModel.cs @@ -1,3 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. using System.Collections.Generic; using System.Security.Claims; using Newtonsoft.Json;