diff --git a/src/Bootstrap/dist/css/bootstrap-theme.css b/src/Bootstrap/dist/css/bootstrap-theme.css index 6e8c4bf916..139ff80772 100644 --- a/src/Bootstrap/dist/css/bootstrap-theme.css +++ b/src/Bootstrap/dist/css/bootstrap-theme.css @@ -732,6 +732,10 @@ img.reserved-indicator-icon { margin-top: 6px; margin-bottom: 6px; } +.page-add-organization .required:after { + color: red; + content: " *"; +} .page-admin-index h2 { margin-bottom: 8px; margin-top: 32px; diff --git a/src/Bootstrap/less/theme/page-add-organization.less b/src/Bootstrap/less/theme/page-add-organization.less index 4c94bdd1ae..d5dedbfca2 100644 --- a/src/Bootstrap/less/theme/page-add-organization.less +++ b/src/Bootstrap/less/theme/page-add-organization.less @@ -1,6 +1,11 @@ .page-add-organization { - .owner-image { - margin-top: 6px; - margin-bottom: 6px; - } -} \ No newline at end of file + .owner-image { + margin-top: 6px; + margin-bottom: 6px; + } + + .required:after { + color: red; + content: " *"; + } +} diff --git a/src/Bootstrap/package-lock.json b/src/Bootstrap/package-lock.json index 7a6e011fb7..bc8f27b528 100644 --- a/src/Bootstrap/package-lock.json +++ b/src/Bootstrap/package-lock.json @@ -2389,9 +2389,9 @@ "dev": true }, "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", "dev": true }, "yargs": { diff --git a/src/DatabaseMigrationTools/DatabaseMigrationTools.csproj b/src/DatabaseMigrationTools/DatabaseMigrationTools.csproj index 518044b58a..97776f9be9 100644 --- a/src/DatabaseMigrationTools/DatabaseMigrationTools.csproj +++ b/src/DatabaseMigrationTools/DatabaseMigrationTools.csproj @@ -65,7 +65,7 @@ all - 2.84.0 + 2.86.0 diff --git a/src/GitHubVulnerabilities2Db/GitHubVulnerabilities2Db.csproj b/src/GitHubVulnerabilities2Db/GitHubVulnerabilities2Db.csproj index 2d39ba30e6..7eb0662ef1 100644 --- a/src/GitHubVulnerabilities2Db/GitHubVulnerabilities2Db.csproj +++ b/src/GitHubVulnerabilities2Db/GitHubVulnerabilities2Db.csproj @@ -89,7 +89,7 @@ 4.3.0-dev-3612825 - 2.84.0 + 2.86.0 diff --git a/src/NuGet.Services.Entities/Package.cs b/src/NuGet.Services.Entities/Package.cs index 3354797ff5..18a2601d7e 100644 --- a/src/NuGet.Services.Entities/Package.cs +++ b/src/NuGet.Services.Entities/Package.cs @@ -12,6 +12,7 @@ namespace NuGet.Services.Entities public class Package : IPackageEntity { + private string _id; #pragma warning disable 618 // TODO: remove Package.Authors completely once production services definitely no longer need it public Package() @@ -264,7 +265,17 @@ public bool HasEmbeddedReadme public virtual ICollection SymbolPackages { get; set; } - public string Id => PackageRegistration.Id; + /// + /// The package ID with casing specific to this version if available, otherwise it will fallback to the ID on + /// the package registration. WARNING: this property should not be used for comparisons in LINQ to SQL because + /// it may be null sometimes. Use instead. + /// + [StringLength(Constants.MaxPackageIdLength)] + public string Id + { + get => _id ?? PackageRegistration?.Id; + set => _id = value; + } public EmbeddedLicenseFileType EmbeddedLicenseType { get; set; } diff --git a/src/NuGetGallery.Core/Auditing/CloudAuditingService.cs b/src/NuGetGallery.Core/Auditing/CloudAuditingService.cs index 829d6c34f1..78c116b446 100644 --- a/src/NuGetGallery.Core/Auditing/CloudAuditingService.cs +++ b/src/NuGetGallery.Core/Auditing/CloudAuditingService.cs @@ -21,17 +21,17 @@ public class CloudAuditingService : AuditingService, ICloudStorageStatusDependen { public static readonly string DefaultContainerName = "auditing"; - private CloudBlobContainer _auditContainer; + private Func _auditContainerFactory; private Func> _getOnBehalfOf; - public CloudAuditingService(string storageConnectionString, bool readAccessGeoRedundant, Func> getOnBehalfOf) - : this(GetContainer(storageConnectionString, readAccessGeoRedundant), getOnBehalfOf) + public CloudAuditingService(Func cloudBlobClientFactory, Func> getOnBehalfOf) + : this(() => GetContainer(cloudBlobClientFactory), getOnBehalfOf) { } - public CloudAuditingService(CloudBlobContainer auditContainer, Func> getOnBehalfOf) + public CloudAuditingService(Func auditContainerFactory, Func> getOnBehalfOf) { - _auditContainer = auditContainer; + _auditContainerFactory = auditContainerFactory; _getOnBehalfOf = getOnBehalfOf; } @@ -52,7 +52,8 @@ protected override async Task SaveAuditRecordAsync(string auditData, string reso $"{filePath.Replace(Path.DirectorySeparatorChar, '/')}/" + $"{Guid.NewGuid().ToString("N")}-{action.ToLowerInvariant()}.audit.v1.json"; - var blob = _auditContainer.GetBlockBlobReference(fullPath); + var container = _auditContainerFactory(); + var blob = container.GetBlobReference(fullPath); bool retry = false; try { @@ -74,37 +75,23 @@ protected override async Task SaveAuditRecordAsync(string auditData, string reso { // Create the container and try again, // this time we let exceptions bubble out - await Task.Factory.FromAsync( - (cb, s) => _auditContainer.BeginCreateIfNotExists(cb, s), - ar => _auditContainer.EndCreateIfNotExists(ar), - null); + await container.CreateIfNotExistAsync(permissions: null); await WriteBlob(auditData, fullPath, blob); } } - private static CloudBlobContainer GetContainer(string storageConnectionString, bool readAccessGeoRedundant) + private static ICloudBlobContainer GetContainer(Func cloudBlobClientFactory) { - var cloudBlobClient = CloudStorageAccount.Parse(storageConnectionString).CreateCloudBlobClient(); - if (readAccessGeoRedundant) - { - cloudBlobClient.DefaultRequestOptions.LocationMode = LocationMode.PrimaryThenSecondary; - } + var cloudBlobClient = cloudBlobClientFactory(); return cloudBlobClient.GetContainerReference(DefaultContainerName); } - private static async Task WriteBlob(string auditData, string fullPath, CloudBlockBlob blob) + private static async Task WriteBlob(string auditData, string fullPath, ISimpleCloudBlob blob) { try { - var strm = await Task.Factory.FromAsync( - (cb, s) => blob.BeginOpenWrite( - AccessCondition.GenerateIfNoneMatchCondition("*"), - new BlobRequestOptions(), - new OperationContext(), - cb, s), - ar => blob.EndOpenWrite(ar), - null); - using (var writer = new StreamWriter(strm)) + using (var stream = await blob.OpenWriteAsync(AccessCondition.GenerateIfNoneMatchCondition("*"))) + using (var writer = new StreamWriter(stream)) { await writer.WriteAsync(auditData); } @@ -125,7 +112,7 @@ private static async Task WriteBlob(string auditData, string fullPath, CloudBloc public Task IsAvailableAsync(BlobRequestOptions options, OperationContext operationContext) { - return _auditContainer.ExistsAsync(options, operationContext); + return _auditContainerFactory().ExistsAsync(options, operationContext); } public override string RenderAuditEntry(AuditEntry entry) diff --git a/src/NuGetGallery.Core/Infrastructure/AzureEntityList.cs b/src/NuGetGallery.Core/Infrastructure/AzureEntityList.cs index 408a5d89e0..584dde9c7b 100644 --- a/src/NuGetGallery.Core/Infrastructure/AzureEntityList.cs +++ b/src/NuGetGallery.Core/Infrastructure/AzureEntityList.cs @@ -22,25 +22,25 @@ namespace NuGetGallery.Infrastructure private const string IndexPartitionKey = "INDEX"; private const string IndexRowKey = "0"; - private CloudTable _tableRef; + private readonly string _tableName; + private readonly bool _readAccessGeoRedundant; + private readonly Func _connectionStringFactory; - public AzureEntityList(string connStr, string tableName, bool readAccessGeoRedundant) + public AzureEntityList(Func connectionStringFactory, string tableName, bool readAccessGeoRedundant) { - var tableClient = CloudStorageAccount.Parse(connStr).CreateCloudTableClient(); - if (readAccessGeoRedundant) - { - tableClient.DefaultRequestOptions.LocationMode = LocationMode.PrimaryThenSecondary; - } - _tableRef = tableClient.GetTableReference(tableName); + _tableName = tableName ?? throw new ArgumentNullException(nameof(tableName)); + _readAccessGeoRedundant = readAccessGeoRedundant; + _connectionStringFactory = connectionStringFactory ?? throw new ArgumentNullException(nameof(connectionStringFactory)); + var tableRef = GetTableReference(); // Create the actual Azure Table, if it doesn't yet exist. - bool newTable = _tableRef.CreateIfNotExists(); + bool newTable = tableRef.CreateIfNotExists(); // Create the Index if it doesn't yet exist. bool needsIndex = newTable; if (!newTable) { - var indexResult = _tableRef.Execute( + var indexResult = tableRef.Execute( TableOperation.Retrieve(IndexPartitionKey, IndexRowKey)); needsIndex = (indexResult.HttpStatusCode == 404); @@ -49,7 +49,7 @@ public AzureEntityList(string connStr, string tableName, bool readAccessGeoRedun if (needsIndex) { // Create the index - var result = _tableRef.Execute( + var result = tableRef.Execute( TableOperation.Insert(new Index { Count = 0, @@ -102,7 +102,8 @@ public T this[long index] string partitionKey = FormatPartitionKey(page); string rowKey = FormatRowKey(row); - var response = _tableRef.Execute(TableOperation.Retrieve(partitionKey, rowKey)); + var tableRef = GetTableReference(); + var response = tableRef.Execute(TableOperation.Retrieve(partitionKey, rowKey)); if (response.HttpStatusCode == 404) { throw new ArgumentOutOfRangeException(nameof(index), index, CoreStrings.Http404NotFound); @@ -129,8 +130,10 @@ public T this[long index] value.PartitionKey = FormatPartitionKey(page); value.RowKey = FormatRowKey(row); + var tableRef = GetTableReference(); + // Just do an unconditional update - if you wanted any *real* benefit of atomic update then you would need a more complex method signature that calls you back when optimistic updates fail ETAG checks! - _tableRef.Execute(TableOperation.Replace(value)); + tableRef.Execute(TableOperation.Replace(value)); } } @@ -161,13 +164,14 @@ public long Add(T entity) /// public IEnumerator GetEnumerator() { + var tableRef = GetTableReference(); for (long page = 0;; page++) { string partitionKey = FormatPartitionKey(page); var chunkQuery = new TableQuery().Where( TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, partitionKey)); - var chunk = _tableRef.ExecuteQuery(chunkQuery).ToArray(); + var chunk = tableRef.ExecuteQuery(chunkQuery).ToArray(); foreach (var item in chunk) { @@ -191,6 +195,7 @@ public IEnumerable GetRange(long pos, int n) int done = 0; long page = pos / 1000; long offset = pos % 1000; + var tableRef = GetTableReference(); while (done < n) { string partitionKey = FormatPartitionKey(page); @@ -201,7 +206,7 @@ public IEnumerable GetRange(long pos, int n) TableOperators.And, TableQuery.GenerateFilterCondition("RowKey", QueryComparisons.GreaterThanOrEqual, rowKey))); - var chunk = _tableRef.ExecuteQuery(chunkQuery).ToArray(); + var chunk = tableRef.ExecuteQuery(chunkQuery).ToArray(); if (chunk.Length == 0) { break; // Reached the end of the list @@ -235,9 +240,10 @@ private long AtomicIncrementCount() // 2) use ETAG to do a conditional +1 update // 3) retry if that optimistic concurrency attempt failed long pos = -1; // To avoid compiler warnings, grr - should never be returned + var tableRef = GetTableReference(); DoReplaceWithRetry(() => { - var result1 = _tableRef.Execute( + var result1 = tableRef.Execute( TableOperation.Retrieve(IndexPartitionKey, IndexRowKey)); ThrowIfErrorStatus(result1); @@ -260,6 +266,7 @@ private long AtomicIncrementCount() private void InsertIfNotExistsWithRetry(Func valueGenerator) where T2 : ITableEntity { TableResult storeResult; + var tableRef = GetTableReference(); do { var entity = valueGenerator(); @@ -268,7 +275,7 @@ private void InsertIfNotExistsWithRetry(Func valueGenerator) where T2 : // - the dummy MERGES with existing data instead of overwriting it, so no data loss. // 2) Use its ETAG to conditionally replace the item // 3) return true if success, false to allow retry on failure - var dummyResult = _tableRef.Execute( + var dummyResult = tableRef.Execute( TableOperation.InsertOrMerge(new HazardEntry { PartitionKey = entity.PartitionKey, @@ -281,7 +288,7 @@ private void InsertIfNotExistsWithRetry(Func valueGenerator) where T2 : } entity.ETag = dummyResult.Etag; - storeResult = _tableRef.Execute(TableOperation.Replace(entity)); + storeResult = tableRef.Execute(TableOperation.Replace(entity)); } while (storeResult.HttpStatusCode == 412); ThrowIfErrorStatus(storeResult); @@ -290,9 +297,10 @@ private void InsertIfNotExistsWithRetry(Func valueGenerator) where T2 : private void DoReplaceWithRetry(Func valueGenerator) where T2 : ITableEntity { TableResult storeResult; + var tableRef = GetTableReference(); do { - storeResult = _tableRef.Execute(TableOperation.Replace(valueGenerator.Invoke())); + storeResult = tableRef.Execute(TableOperation.Replace(valueGenerator.Invoke())); } while (storeResult.HttpStatusCode == 412); ThrowIfErrorStatus(storeResult); @@ -300,7 +308,8 @@ private void DoReplaceWithRetry(Func valueGenerator) where T2 : ITableEn private Index ReadIndex() { - var response = _tableRef.Execute(TableOperation.Retrieve(IndexPartitionKey, IndexRowKey)); + var tableRef = GetTableReference(); + var response = tableRef.Execute(TableOperation.Retrieve(IndexPartitionKey, IndexRowKey)); return (Index)response.Result; } @@ -333,6 +342,16 @@ System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() return GetEnumerator(); } + private CloudTable GetTableReference() + { + var tableClient = CloudStorageAccount.Parse(_connectionStringFactory()).CreateCloudTableClient(); + if (_readAccessGeoRedundant) + { + tableClient.DefaultRequestOptions.LocationMode = LocationMode.PrimaryThenSecondary; + } + return tableClient.GetTableReference(_tableName); + } + class HazardEntry : ITableEntity { private const string PlaceHolderPropertyName = "Place_Held"; diff --git a/src/NuGetGallery.Core/Infrastructure/TableErrorLog.cs b/src/NuGetGallery.Core/Infrastructure/TableErrorLog.cs index ee811c12fa..314f16a220 100644 --- a/src/NuGetGallery.Core/Infrastructure/TableErrorLog.cs +++ b/src/NuGetGallery.Core/Infrastructure/TableErrorLog.cs @@ -141,13 +141,11 @@ public class TableErrorLog : ErrorLog { public const string TableName = "ElmahErrors"; - private readonly string _connectionString; private readonly AzureEntityList _entityList; - public TableErrorLog(string connectionString, bool readAccessGeoRedundant) + public TableErrorLog(Func connectionStringFactory, bool readAccessGeoRedundant) { - _connectionString = connectionString; - _entityList = new AzureEntityList(connectionString, TableName, readAccessGeoRedundant); + _entityList = new AzureEntityList(connectionStringFactory, TableName, readAccessGeoRedundant); } public override ErrorLogEntry GetError(string id) diff --git a/src/NuGetGallery.Core/NuGetGallery.Core.csproj b/src/NuGetGallery.Core/NuGetGallery.Core.csproj index 93ad9afea9..45ede5eec6 100644 --- a/src/NuGetGallery.Core/NuGetGallery.Core.csproj +++ b/src/NuGetGallery.Core/NuGetGallery.Core.csproj @@ -47,7 +47,7 @@ 5.9.0 - 2.84.0 + 2.86.0 9.3.3 @@ -56,13 +56,13 @@ - 2.84.0 + 2.86.0 - 2.84.0 + 2.86.0 - 2.84.0 + 2.86.0 1.2.2 diff --git a/src/NuGetGallery.Core/Services/CorePackageService.cs b/src/NuGetGallery.Core/Services/CorePackageService.cs index bbd2243ec3..57966f2414 100644 --- a/src/NuGetGallery.Core/Services/CorePackageService.cs +++ b/src/NuGetGallery.Core/Services/CorePackageService.cs @@ -202,6 +202,17 @@ public virtual async Task UpdateIsLatestAsync(PackageRegistration packageRegistr } } + // Update the ID on the PackageRegistration if the value differs only by case from the absolute latest + // (stable or prerelease) SemVer 2.0.0 package. This is a best effort flow because in general package IDs + // are compared in a case-insensitive manner and therefore the PackageRegistration ID casing should not + // have any functional impact. The specific casing is only a display concern. + if (latestSemVer2Package != null + && string.Equals(latestSemVer2Package.Id, packageRegistration.Id, StringComparison.OrdinalIgnoreCase) + && !string.Equals(latestSemVer2Package.Id, packageRegistration.Id, StringComparison.Ordinal)) + { + packageRegistration.Id = latestSemVer2Package.Id; + } + if (commitChanges) { await _packageRepository.CommitChangesAsync(); diff --git a/src/NuGetGallery.Services/NuGetGallery.Services.csproj b/src/NuGetGallery.Services/NuGetGallery.Services.csproj index 8dedef05f8..8f2f32d2bd 100644 --- a/src/NuGetGallery.Services/NuGetGallery.Services.csproj +++ b/src/NuGetGallery.Services/NuGetGallery.Services.csproj @@ -309,10 +309,10 @@ 5.9.0 - 2.84.0 + 2.86.0 - 2.84.0 + 2.86.0 0.2.0 diff --git a/src/NuGetGallery.Services/PackageManagement/PackageService.cs b/src/NuGetGallery.Services/PackageManagement/PackageService.cs index 02fade1b03..8b1877b4d2 100644 --- a/src/NuGetGallery.Services/PackageManagement/PackageService.cs +++ b/src/NuGetGallery.Services/PackageManagement/PackageService.cs @@ -636,6 +636,8 @@ public virtual Package EnrichPackageFromNuGetPackage( PackageStreamMetadata packageStreamMetadata, User user) { + package.Id = packageMetadata.Id; + // Version must always be the exact string from the nuspec, which OriginalVersion will return to us. // However, we do also store a normalized copy for looking up later. package.Version = packageMetadata.Version.OriginalVersion; diff --git a/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs b/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs index f12a369868..22e134ba0a 100644 --- a/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs +++ b/src/NuGetGallery/App_Start/DefaultDependenciesModule.cs @@ -78,6 +78,8 @@ public static class BindingKeys public const string EmailPublisherTopic = "EmailPublisherBindingKey"; public const string PreviewSearchClient = "PreviewSearchClientBindingKey"; + + public const string AuditKey = "AuditKey"; } public static class ParameterNames @@ -468,24 +470,20 @@ protected override void Load(ContainerBuilder builder) .As() .InstancePerLifetimeScope(); - IAuditingService defaultAuditingService = null; - switch (configuration.Current.StorageType) { case StorageType.FileSystem: case StorageType.NotSpecified: ConfigureForLocalFileSystem(builder, configuration); - defaultAuditingService = GetAuditingServiceForLocalFileSystem(configuration); break; case StorageType.AzureStorage: ConfigureForAzureStorage(builder, configuration, telemetryService); - defaultAuditingService = GetAuditingServiceForAzureStorage(builder, configuration); break; } RegisterAsynchronousValidation(builder, loggerFactory, configuration, secretInjector); - RegisterAuditingServices(builder, defaultAuditingService); + RegisterAuditingServices(builder, configuration.Current.StorageType); RegisterCookieComplianceService(configuration, loggerFactory); @@ -707,27 +705,41 @@ private static void RegisterDeleteAccountService(ContainerBuilder builder, Confi private static void RegisterStatisticsServices(ContainerBuilder builder, IGalleryConfigurationService configuration, ITelemetryService telemetryService) { + // when running on Windows Azure, download counts come from the downloads.v1.json blob + builder.Register(c => new SimpleBlobStorageConfiguration(configuration.Current.AzureStorage_Statistics_ConnectionString, configuration.Current.AzureStorageReadAccessGeoRedundant)) + .SingleInstance() + .Keyed(BindingKeys.PrimaryStatisticsKey); + + builder.Register(c => new SimpleBlobStorageConfiguration(configuration.Current.AzureStorage_Statistics_ConnectionString_Alternate, configuration.Current.AzureStorageReadAccessGeoRedundant)) + .SingleInstance() + .Keyed(BindingKeys.AlternateStatisticsKey); + // when running on Windows Azure, we use a back-end job to calculate stats totals and store in the blobs - builder.RegisterInstance(new JsonAggregateStatsService(configuration.Current.AzureStorage_Statistics_ConnectionString, configuration.Current.AzureStorageReadAccessGeoRedundant)) + builder.Register(c => + { + var primaryConfiguration = c.ResolveKeyed(BindingKeys.PrimaryStatisticsKey); + var alternateConfiguration = c.ResolveKeyed(BindingKeys.AlternateStatisticsKey); + var featureFlagService = c.Resolve(); + var jsonAggregateStatsService = new JsonAggregateStatsService(featureFlagService, primaryConfiguration, alternateConfiguration); + return jsonAggregateStatsService; + }) .AsSelf() .As() .SingleInstance(); // when running on Windows Azure, pull the statistics from the warehouse via storage - builder.RegisterInstance(new CloudReportService(configuration.Current.AzureStorage_Statistics_ConnectionString, configuration.Current.AzureStorageReadAccessGeoRedundant)) + builder.Register(c => + { + var primaryConfiguration = c.ResolveKeyed(BindingKeys.PrimaryStatisticsKey); + var alternateConfiguration = c.ResolveKeyed(BindingKeys.AlternateStatisticsKey); + var featureFlagService = c.Resolve(); + var cloudReportService = new CloudReportService(featureFlagService, primaryConfiguration, alternateConfiguration); + return cloudReportService; + }) .AsSelf() .As() .SingleInstance(); - // when running on Windows Azure, download counts come from the downloads.v1.json blob - builder.Register(c => new SimpleBlobStorageConfiguration(configuration.Current.AzureStorage_Statistics_ConnectionString, configuration.Current.AzureStorageReadAccessGeoRedundant)) - .SingleInstance() - .Keyed(BindingKeys.PrimaryStatisticsKey); - - builder.Register(c => new SimpleBlobStorageConfiguration(configuration.Current.AzureStorage_Statistics_ConnectionString_Alternate, configuration.Current.AzureStorageReadAccessGeoRedundant)) - .SingleInstance() - .Keyed(BindingKeys.AlternateStatisticsKey); - builder.Register(c => { var primaryConfiguration = c.ResolveKeyed(BindingKeys.PrimaryStatisticsKey); @@ -1362,10 +1374,10 @@ private static void ConfigureForLocalFileSystem(ContainerBuilder builder, IGalle .SingleInstance(); } - private static IAuditingService GetAuditingServiceForLocalFileSystem(IGalleryConfigurationService configuration) + private static IAuditingService GetAuditingServiceForLocalFileSystem(IAppConfiguration configuration) { var auditingPath = Path.Combine( - FileSystemFileStorageService.ResolvePath(configuration.Current.FileStorageDirectory), + FileSystemFileStorageService.ResolvePath(configuration.FileStorageDirectory), FileSystemAuditingService.DefaultContainerName); return new FileSystemAuditingService(auditingPath, AuditActor.GetAspNetOnBehalfOfAsync); @@ -1422,7 +1434,11 @@ private static void ConfigureForAzureStorage(ContainerBuilder builder, IGalleryC RegisterStatisticsServices(builder, configuration, telemetryService); - builder.RegisterInstance(new TableErrorLog(configuration.Current.AzureStorage_Errors_ConnectionString, configuration.Current.AzureStorageReadAccessGeoRedundant)) + builder.Register(c => + { + var configurationFactory = c.Resolve>(); + return new TableErrorLog(() => configurationFactory().AzureStorage_Errors_ConnectionString, configurationFactory().AzureStorageReadAccessGeoRedundant); + }) .As() .SingleInstance(); @@ -1431,17 +1447,6 @@ private static void ConfigureForAzureStorage(ContainerBuilder builder, IGalleryC .SingleInstance(); } - private static IAuditingService GetAuditingServiceForAzureStorage(ContainerBuilder builder, IGalleryConfigurationService configuration) - { - var service = new CloudAuditingService(configuration.Current.AzureStorage_Auditing_ConnectionString, configuration.Current.AzureStorageReadAccessGeoRedundant, AuditActor.GetAspNetOnBehalfOfAsync); - - builder.RegisterInstance(service) - .As() - .SingleInstance(); - - return service; - } - private static IAuditingService CombineAuditingServices(IEnumerable services) { if (!services.Any()) @@ -1473,19 +1478,54 @@ private static IEnumerable GetAddInServices(Action } } - private static void RegisterAuditingServices(ContainerBuilder builder, IAuditingService defaultAuditingService) + private static void RegisterAuditingServices(ContainerBuilder builder, string storageType) { - var auditingServices = GetAddInServices(); - var services = new List(auditingServices); - - if (defaultAuditingService != null) + if (storageType == StorageType.AzureStorage) { - services.Add(defaultAuditingService); + builder.Register(c => + { + var configuration = c.Resolve(); + return new CloudBlobClientWrapper(configuration.AzureStorage_Auditing_ConnectionString, configuration.AzureStorageReadAccessGeoRedundant); + }) + .SingleInstance() + .Keyed(BindingKeys.AuditKey); + + builder.Register(c => + { + var blobClientFactory = c.ResolveKeyed>(BindingKeys.AuditKey); + return new CloudAuditingService(blobClientFactory, AuditActor.GetAspNetOnBehalfOfAsync); + }) + .SingleInstance() + .AsSelf() + .As(); } - var service = CombineAuditingServices(services); + builder.Register(c => + { + var configuration = c.Resolve(); + IAuditingService defaultAuditingService = null; + switch (storageType) + { + case StorageType.FileSystem: + case StorageType.NotSpecified: + defaultAuditingService = GetAuditingServiceForLocalFileSystem(configuration); + break; + + case StorageType.AzureStorage: + defaultAuditingService = c.Resolve(); + break; + } + + var auditingServices = GetAddInServices(); + var services = new List(auditingServices); - builder.RegisterInstance(service) + if (defaultAuditingService != null) + { + services.Add(defaultAuditingService); + } + + return CombineAuditingServices(services); + }) .AsSelf() .As() .SingleInstance(); diff --git a/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs b/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs index 4ff277915e..63408679a9 100644 --- a/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs +++ b/src/NuGetGallery/Areas/Admin/Controllers/DeleteController.cs @@ -109,7 +109,7 @@ private DeleteSearchResult CreateDeleteSearchResult(Package package) { return new DeleteSearchResult { - PackageId = package.PackageRegistration.Id, + PackageId = package.Id, PackageVersionNormalized = !string.IsNullOrEmpty(package.NormalizedVersion) ? package.NormalizedVersion : NuGetVersion.Parse(package.Version).ToNormalizedString(), diff --git a/src/NuGetGallery/Helpers/ViewModelExtensions/PackageViewModelFactory.cs b/src/NuGetGallery/Helpers/ViewModelExtensions/PackageViewModelFactory.cs index 8859de3a4c..1695835256 100644 --- a/src/NuGetGallery/Helpers/ViewModelExtensions/PackageViewModelFactory.cs +++ b/src/NuGetGallery/Helpers/ViewModelExtensions/PackageViewModelFactory.cs @@ -35,7 +35,7 @@ public PackageViewModel Setup(PackageViewModel viewModel, Package package) viewModel.FullVersion = NuGetVersionFormatter.ToFullString(package.Version); - viewModel.Id = package.PackageRegistration.Id; + viewModel.Id = package.Id; viewModel.Version = String.IsNullOrEmpty(package.NormalizedVersion) ? NuGetVersionFormatter.Normalize(package.Version) : package.NormalizedVersion; diff --git a/src/NuGetGallery/Infrastructure/LuceneDocumentFactory.cs b/src/NuGetGallery/Infrastructure/LuceneDocumentFactory.cs index 8a438b6f59..c10436647c 100644 --- a/src/NuGetGallery/Infrastructure/LuceneDocumentFactory.cs +++ b/src/NuGetGallery/Infrastructure/LuceneDocumentFactory.cs @@ -49,23 +49,23 @@ public Document Create(Package package) // Style 1: As-Is Id, no tokenizing (so you can search using dot or dash-joined terms) // Boost this one - field = new Field("Id", package.PackageRegistration.Id, Field.Store.NO, Field.Index.ANALYZED); + field = new Field("Id", package.Id, Field.Store.NO, Field.Index.ANALYZED); document.Add(field); // Style 2: dot+dash tokenized (so you can search using undotted terms) - field = new Field("Id", SplitId(package.PackageRegistration.Id), Field.Store.NO, Field.Index.ANALYZED); + field = new Field("Id", SplitId(package.Id), Field.Store.NO, Field.Index.ANALYZED); field.Boost = 0.8f; document.Add(field); // Style 3: camel-case tokenized (so you can search using parts of the camelCasedWord). // De-boosted since matches are less likely to be meaningful - field = new Field("Id", CamelSplitId(package.PackageRegistration.Id), Field.Store.NO, Field.Index.ANALYZED); + field = new Field("Id", CamelSplitId(package.Id), Field.Store.NO, Field.Index.ANALYZED); field.Boost = 0.25f; document.Add(field); // If an element does not have a Title, fall back to Id, same as the website. var workingTitle = String.IsNullOrEmpty(package.Title) - ? package.PackageRegistration.Id + ? package.Id : package.Title; // As-Is (stored for search results) @@ -113,7 +113,7 @@ public Document Create(Package package) document.Add(new Field("FlattenedPackageTypes", package.FlattenedPackageTypes.ToStringSafe(), Field.Store.YES, Field.Index.NO)); document.Add(new Field("Hash", package.Hash.ToStringSafe(), Field.Store.YES, Field.Index.NO)); document.Add(new Field("HashAlgorithm", package.HashAlgorithm.ToStringSafe(), Field.Store.YES, Field.Index.NO)); - document.Add(new Field("Id-Original", package.PackageRegistration.Id, Field.Store.YES, Field.Index.NO)); + document.Add(new Field("Id-Original", package.Id, Field.Store.YES, Field.Index.NO)); document.Add(new Field("IsVerified-Original", package.PackageRegistration.IsVerified.ToString(), Field.Store.YES, Field.Index.NO)); document.Add(new Field("LastUpdated", package.LastUpdated.ToString(CultureInfo.InvariantCulture), Field.Store.YES, Field.Index.NO)); if (package.LastEdited != null) diff --git a/src/NuGetGallery/Migrations/202104062157118_AddVersionSpecificId.Designer.cs b/src/NuGetGallery/Migrations/202104062157118_AddVersionSpecificId.Designer.cs new file mode 100644 index 0000000000..3d98f2176c --- /dev/null +++ b/src/NuGetGallery/Migrations/202104062157118_AddVersionSpecificId.Designer.cs @@ -0,0 +1,29 @@ +// +namespace NuGetGallery.Migrations +{ + using System.CodeDom.Compiler; + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + [GeneratedCode("EntityFramework.Migrations", "6.4.0-preview3-19553-01")] + public sealed partial class AddVersionSpecificId : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(AddVersionSpecificId)); + + string IMigrationMetadata.Id + { + get { return "202104062157118_AddVersionSpecificId"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/src/NuGetGallery/Migrations/202104062157118_AddVersionSpecificId.cs b/src/NuGetGallery/Migrations/202104062157118_AddVersionSpecificId.cs new file mode 100644 index 0000000000..fc487a7e86 --- /dev/null +++ b/src/NuGetGallery/Migrations/202104062157118_AddVersionSpecificId.cs @@ -0,0 +1,18 @@ +namespace NuGetGallery.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class AddVersionSpecificId : DbMigration + { + public override void Up() + { + AddColumn("dbo.Packages", "Id", c => c.String(maxLength: 128)); + } + + public override void Down() + { + DropColumn("dbo.Packages", "Id"); + } + } +} diff --git a/src/NuGetGallery/Migrations/202104062157118_AddVersionSpecificId.resx b/src/NuGetGallery/Migrations/202104062157118_AddVersionSpecificId.resx new file mode 100644 index 0000000000..36b961e5c7 --- /dev/null +++ b/src/NuGetGallery/Migrations/202104062157118_AddVersionSpecificId.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + + dbo + + \ No newline at end of file diff --git a/src/NuGetGallery/NuGetGallery.csproj b/src/NuGetGallery/NuGetGallery.csproj index 8b38d2ab70..029adb2e42 100644 --- a/src/NuGetGallery/NuGetGallery.csproj +++ b/src/NuGetGallery/NuGetGallery.csproj @@ -303,6 +303,10 @@ 202007220027197_AddEmbeddedReadmeTypeColumn.cs + + + 202104062157118_AddVersionSpecificId.cs + @@ -1640,6 +1644,9 @@ 202007220027197_AddEmbeddedReadmeTypeColumn.cs + + 202104062157118_AddVersionSpecificId.cs + @@ -2243,13 +2250,13 @@ 5.9.0 - 2.84.0 + 2.86.0 - 2.84.0 + 2.86.0 - 2.84.0 + 2.86.0 1.0.0 @@ -2293,9 +2300,6 @@ 1.7.0 - - 4.1.3 - 9.3.3 diff --git a/src/NuGetGallery/Services/CloudReportService.cs b/src/NuGetGallery/Services/CloudReportService.cs index bc6ae735b1..f30dd780a7 100644 --- a/src/NuGetGallery/Services/CloudReportService.cs +++ b/src/NuGetGallery/Services/CloudReportService.cs @@ -1,23 +1,30 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Threading.Tasks; using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage.Blob; using Microsoft.WindowsAzure.Storage.RetryPolicies; +using NuGetGallery.Services; namespace NuGetGallery { public class CloudReportService : IReportService { private const string _statsContainerName = "nuget-cdnstats"; - private readonly string _connectionString; - private readonly bool _readAccessGeoRedundant; + private readonly IFeatureFlagService _featureFlagService; + private readonly IBlobStorageConfiguration _primaryStorageConfiguration; + private readonly IBlobStorageConfiguration _alternateBlobStorageConfiguration; - public CloudReportService(string connectionString, bool readAccessGeoRedundant) + public CloudReportService( + IFeatureFlagService featureFlagService, + IBlobStorageConfiguration primaryBlobStorageConfiguration, + IBlobStorageConfiguration alternateBlobStorageConfiguration) { - _connectionString = connectionString; - _readAccessGeoRedundant = readAccessGeoRedundant; + _featureFlagService = featureFlagService ?? throw new ArgumentNullException(nameof(featureFlagService)); + _primaryStorageConfiguration = primaryBlobStorageConfiguration ?? throw new ArgumentNullException(nameof(primaryBlobStorageConfiguration)); + _alternateBlobStorageConfiguration = alternateBlobStorageConfiguration; } public async Task Load(string reportName) @@ -42,10 +49,19 @@ public async Task Load(string reportName) private CloudBlobContainer GetCloudBlobContainer() { - var storageAccount = CloudStorageAccount.Parse(_connectionString); + var connectionString = _primaryStorageConfiguration.ConnectionString; + var readAccessGeoRedundant = _primaryStorageConfiguration.ReadAccessGeoRedundant; + + if(_alternateBlobStorageConfiguration != null && _featureFlagService.IsAlternateStatisticsSourceEnabled()) + { + connectionString = _alternateBlobStorageConfiguration.ConnectionString; + readAccessGeoRedundant = _alternateBlobStorageConfiguration.ReadAccessGeoRedundant; + } + + var storageAccount = CloudStorageAccount.Parse(connectionString); var blobClient = storageAccount.CreateCloudBlobClient(); - if (_readAccessGeoRedundant) + if (readAccessGeoRedundant) { blobClient.DefaultRequestOptions.LocationMode = LocationMode.PrimaryThenSecondary; } diff --git a/src/NuGetGallery/Services/JsonAggregateStatsService.cs b/src/NuGetGallery/Services/JsonAggregateStatsService.cs index ab8b5ce1c7..032bad7d01 100644 --- a/src/NuGetGallery/Services/JsonAggregateStatsService.cs +++ b/src/NuGetGallery/Services/JsonAggregateStatsService.cs @@ -1,31 +1,47 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Threading.Tasks; using Microsoft.WindowsAzure.Storage; using Microsoft.WindowsAzure.Storage.Blob; using Microsoft.WindowsAzure.Storage.RetryPolicies; using Newtonsoft.Json; +using NuGetGallery.Services; namespace NuGetGallery { public class JsonAggregateStatsService : IAggregateStatsService { - private readonly string _connectionString; - private readonly bool _readAccessGeoRedundant; + private readonly IFeatureFlagService _featureFlagService; + private readonly IBlobStorageConfiguration _primaryStorageConfiguration; + private readonly IBlobStorageConfiguration _alternateBlobStorageConfiguration; - public JsonAggregateStatsService(string connectionString, bool readAccessGeoRedundant) + public JsonAggregateStatsService( + IFeatureFlagService featureFlagService, + IBlobStorageConfiguration primaryBlobStorageConfiguration, + IBlobStorageConfiguration alternateBlobStorageConfiguration) { - _connectionString = connectionString; - _readAccessGeoRedundant = readAccessGeoRedundant; + _featureFlagService = featureFlagService ?? throw new ArgumentNullException(nameof(featureFlagService)); + _primaryStorageConfiguration = primaryBlobStorageConfiguration ?? throw new ArgumentNullException(nameof(primaryBlobStorageConfiguration)); + _alternateBlobStorageConfiguration = alternateBlobStorageConfiguration; } public async Task GetAggregateStats() { - CloudStorageAccount storageAccount = CloudStorageAccount.Parse(_connectionString); + var connectionString = _primaryStorageConfiguration.ConnectionString; + var readAccessGeoRedundant = _primaryStorageConfiguration.ReadAccessGeoRedundant; + + if (_alternateBlobStorageConfiguration != null && _featureFlagService.IsAlternateStatisticsSourceEnabled()) + { + connectionString = _alternateBlobStorageConfiguration.ConnectionString; + readAccessGeoRedundant = _alternateBlobStorageConfiguration.ReadAccessGeoRedundant; + } + + CloudStorageAccount storageAccount = CloudStorageAccount.Parse(connectionString); CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient(); - if (_readAccessGeoRedundant) + if (readAccessGeoRedundant) { blobClient.DefaultRequestOptions.LocationMode = LocationMode.PrimaryThenSecondary; } diff --git a/src/NuGetGallery/Services/PackageMetadataValidationService.cs b/src/NuGetGallery/Services/PackageMetadataValidationService.cs index 959f621c5d..f384bfc37b 100644 --- a/src/NuGetGallery/Services/PackageMetadataValidationService.cs +++ b/src/NuGetGallery/Services/PackageMetadataValidationService.cs @@ -458,6 +458,16 @@ private async Task CheckReadmeMetadataAsync(PackageArch } var readmeFileEntry = nuGetPackage.GetEntry(readmeFilePath); + + if (readmeFileEntry.Length == 0) + { + return PackageValidationResult.Invalid( + string.Format( + Strings.ReadmeErrorEmpty, + Strings.UploadPackage_ReadmeFileType, + readmeFilePath)); + } + if (readmeFileEntry.Length > MaxAllowedReadmeLengthForUploading) { return PackageValidationResult.Invalid( diff --git a/src/NuGetGallery/Strings.Designer.cs b/src/NuGetGallery/Strings.Designer.cs index 51d32b707e..c23dc2cb63 100644 --- a/src/NuGetGallery/Strings.Designer.cs +++ b/src/NuGetGallery/Strings.Designer.cs @@ -1588,6 +1588,15 @@ public static string PreviewReadMe_ReadMeMissing { } } + /// + /// Looks up a localized string similar to The readme file '{0}' cannot be empty.. + /// + public static string ReadmeErrorEmpty { + get { + return ResourceManager.GetString("ReadmeErrorEmpty", resourceCulture); + } + } + /// /// Looks up a localized string similar to '{0}' is not a valid Markdown Documentation source type.. /// @@ -2598,7 +2607,7 @@ public static string UploadPackage_InvalidPackage { } /// - /// Looks up a localized string similar to The readme file has an invalid extension '{0}'. Extension must be one of the following: {1}.. + /// Looks up a localized string similar to The readme file has an invalid extension '{0}'. The extension must be: '{1}'.. /// public static string UploadPackage_InvalidReadmeFileExtension { get { diff --git a/src/NuGetGallery/Strings.resx b/src/NuGetGallery/Strings.resx index b841942641..a7b8dcbfc9 100644 --- a/src/NuGetGallery/Strings.resx +++ b/src/NuGetGallery/Strings.resx @@ -1166,7 +1166,7 @@ The {1} Team The <readme> element is not currently supported. - The readme file has an invalid extension '{0}'. Extension must be one of the following: {1}. + The readme file has an invalid extension '{0}'. The extension must be: '{1}'. {0} is the readme file extension specified in the .nuspec, {1} is the list of allowed extensions @@ -1215,4 +1215,7 @@ The {1} Team The package ID is reserved. You can upload your package with a different package ID. Reach out to <a href="mailto:support@nuget.org">support@nuget.org</a> if you have questions. + + The readme file '{0}' cannot be empty. + \ No newline at end of file diff --git a/src/NuGetGallery/ViewModels/PackageManagerViewModel.cs b/src/NuGetGallery/ViewModels/PackageManagerViewModel.cs index 54f5522338..d4f68ef6b5 100644 --- a/src/NuGetGallery/ViewModels/PackageManagerViewModel.cs +++ b/src/NuGetGallery/ViewModels/PackageManagerViewModel.cs @@ -30,9 +30,9 @@ public PackageManagerViewModel(string name) public string CommandPrefix { get; set; } /// - /// A string that represents the command used to install a specific package. + /// One or more strings that represent the command(s) used to install a specific package. /// - public string InstallPackageCommand { get; set; } + public string[] InstallPackageCommands { get; set; } /// /// The alert message that contains clarifications about the command/scenario diff --git a/src/NuGetGallery/Views/Organizations/Add.cshtml b/src/NuGetGallery/Views/Organizations/Add.cshtml index 3c950d2023..00539a119b 100644 --- a/src/NuGetGallery/Views/Organizations/Add.cshtml +++ b/src/NuGetGallery/Views/Organizations/Add.cshtml @@ -46,14 +46,14 @@ @Html.AntiForgeryToken()
- @Html.ShowLabelFor(m => m.OrganizationName) + @Html.ShowLabelFor(m => m.OrganizationName) @Html.ShowTextBoxFor(m => m.OrganizationName) This will be your organization account on @Url.User("{username}", relativeUrl: false). @Html.ShowValidationMessagesFor(m => m.OrganizationName)
- @Html.ShowLabelFor(m => m.OrganizationEmailAddress) + @Html.ShowLabelFor(m => m.OrganizationEmailAddress) @Html.ShowTextBoxFor(m => m.OrganizationEmailAddress) Users can contact your organization at this email address. @Html.ShowValidationMessagesFor(m => m.OrganizationEmailAddress) diff --git a/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml b/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml index b134d7d6ef..5e8cd2e526 100644 --- a/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml +++ b/src/NuGetGallery/Views/Packages/DisplayPackage.cshtml @@ -28,19 +28,32 @@ { packageManagers = new PackageManagerViewModel[] { - new PackageManagerViewModel(".NET CLI") + new PackageManagerViewModel(".NET CLI (Global)") { - Id = "dotnet-cli", + Id = "dotnet-cli-global", + CommandPrefix = "> ", + InstallPackageCommands = new [] { string.Format("dotnet tool install --global {0} --version {1}", Model.Id, Model.Version) }, + AlertLevel = AlertLevel.Info, + AlertMessage = "This package contains a .NET tool you can call from the shell/command line.", + }, + + new PackageManagerViewModel(".NET CLI (Local)") + { + Id = "dotnet-cli-local", CommandPrefix = "> ", - InstallPackageCommand = string.Format("dotnet tool install --global {0} --version {1}", Model.Id, Model.Version), + InstallPackageCommands = new [] + { + "dotnet new tool-manifest # if you are setting up this repo", + string.Format("dotnet tool install --local {0} --version {1}", Model.Id, Model.Version), + }, AlertLevel = AlertLevel.Info, - AlertMessage = "This package contains a .NET Core Global Tool you can call from the shell/command line.", + AlertMessage = "This package contains a .NET tool you can call from the shell/command line.", }, new ThirdPartyPackageManagerViewModel("Cake", "https://cakebuild.net/support/nuget") { Id = "cake-dotnet-tool", - InstallPackageCommand = Model.GetCakeInstallPackageCommand(), + InstallPackageCommands = new [] { Model.GetCakeInstallPackageCommand() }, }, }; } @@ -52,7 +65,7 @@ { Id = "dotnet-cli", CommandPrefix = "> ", - InstallPackageCommand = string.Format("dotnet new --install {0}::{1}", Model.Id, Model.Version), + InstallPackageCommands = new [] { string.Format("dotnet new --install {0}::{1}", Model.Id, Model.Version) }, AlertLevel = AlertLevel.Info, AlertMessage = "This package contains a .NET Core Template Package you can call from the shell/command line.", } @@ -66,26 +79,26 @@ { Id = "package-manager", CommandPrefix = "PM> ", - InstallPackageCommand = string.Format("Install-Package {0} -Version {1}", Model.Id, Model.Version) + InstallPackageCommands = new [] { string.Format("Install-Package {0} -Version {1}", Model.Id, Model.Version) }, }, new PackageManagerViewModel(".NET CLI") { Id = "dotnet-cli", CommandPrefix = "> ", - InstallPackageCommand = string.Format("dotnet add package {0} --version {1}", Model.Id, Model.Version) + InstallPackageCommands = new [] { string.Format("dotnet add package {0} --version {1}", Model.Id, Model.Version) }, }, new PackageManagerViewModel("PackageReference") { Id = "package-reference", - InstallPackageCommand = Model.DevelopmentDependency + InstallPackageCommands = new [] { Model.DevelopmentDependency ? string.Format(string.Join(Environment.NewLine, "", " all", " runtime; build; native; contentfiles; analyzers", ""), Model.Id, Model.Version) - : string.Format("", Model.Id, Model.Version), + : string.Format("", Model.Id, Model.Version) }, AlertLevel = AlertLevel.Info, AlertMessage = string.Format("For projects that support PackageReference, copy this XML node into the project file to reference the package.", "https://docs.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files"), @@ -96,24 +109,22 @@ { Id = "paket-cli", CommandPrefix = "> ", - InstallPackageCommand = string.Format("paket add {0} --version {1}", Model.Id, Model.Version), + InstallPackageCommands = new [] { string.Format("paket add {0} --version {1}", Model.Id, Model.Version) }, }, - new PackageManagerViewModel("F# Interactive") + new PackageManagerViewModel("Script & Interactive") { - Id = "fsharp-interactive", + Id = "script-interactive", CommandPrefix = "> ", - InstallPackageCommand = string.Format("#r \"nuget: {0}, {1}\"", Model.Id, Model.Version), + InstallPackageCommands = new [] { string.Format("#r \"nuget: {0}, {1}\"", Model.Id, Model.Version) }, AlertLevel = AlertLevel.Info, - AlertMessage = string.Format( - "For F# scripts that support #r syntax, copy this into the source code to reference the package.", - "https://docs.microsoft.com/en-us/dotnet/fsharp/tools/fsharp-interactive/#referencing-packages-in-f-interactive") + AlertMessage = "#r directive can be used in F# Interactive, C# scripting and .NET Interactive. Copy this into the interactive tool or source code of the script to reference the package." }, new ThirdPartyPackageManagerViewModel("Cake", "https://cakebuild.net/support/nuget") { Id = Model.IsCakeExtension() ? "cake-extension" : "cake", - InstallPackageCommand = Model.GetCakeInstallPackageCommand() + InstallPackageCommands = new [] { Model.GetCakeInstallPackageCommand() }, }, }; } @@ -165,11 +176,15 @@ @helper CommandPanel(PackageManagerViewModel packageManager, bool active) { var thirdPartyPackageManager = packageManager as ThirdPartyPackageManagerViewModel; -
-
@packageManager.InstallPackageCommand
+ @{ + var lastIndex = packageManager.InstallPackageCommands.Length - 1; + var cs = packageManager.InstallPackageCommands.Select((c, i) => i < lastIndex ? c + Environment.NewLine : c); + } + @* Writing out the install command must be on a single line to avoid undesired whitespace in the
 tag. *@
+                
@foreach (var c in cs) {@c}