forked from ddrilling/AsbCloudServer
Add DrillingProgramController
This commit is contained in:
parent
002da70471
commit
14cf99a1dd
@ -5,8 +5,10 @@ namespace AsbCloudApp.Data
|
||||
public class FileInfoDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int IdWell { get; set; }
|
||||
public int IdCategory { get; set; }
|
||||
public int IdAuthor { get; set; }
|
||||
public string Name { get; set; }
|
||||
public DateTime UploadDate { get; set; }
|
||||
public string AuthorName { get; set; }
|
||||
public int CompanyId { get; set; }
|
||||
|
11
AsbCloudApp/Services/IDrillingProgramService.cs
Normal file
11
AsbCloudApp/Services/IDrillingProgramService.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using AsbCloudApp.Data;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AsbCloudApp.Services
|
||||
{
|
||||
public interface IDrillingProgramService
|
||||
{
|
||||
Task<FileInfoDto> GetAsync(int idWell, CancellationToken token = default);
|
||||
}
|
||||
}
|
@ -10,20 +10,20 @@ namespace AsbCloudApp.Services
|
||||
public interface IFileService
|
||||
{
|
||||
string RootPath { get; }
|
||||
IDictionary<string, int> SaveFileInfos(int idWell, int idUser,
|
||||
IEnumerable<FileInfoDto> filesInfo);
|
||||
|
||||
Task SaveFile(int idWell, int idCategory, int fileId,
|
||||
string fileExtension, Stream fileStream);
|
||||
Task<FileInfoDto> SaveAsync(int idWell, int idUser, int idCategory, string fileFullName, Stream fileStream, CancellationToken token = default);
|
||||
|
||||
Task<PaginationContainer<FileInfoDto>> GetFilesInfoAsync(int idWell,
|
||||
Task<PaginationContainer<FileInfoDto>> GetInfosAsync(int idWell,
|
||||
int idCategory, IEnumerable<int> companies, DateTime begin, DateTime end,
|
||||
int skip, int take, CancellationToken token = default);
|
||||
|
||||
Task<FileInfoDto> GetFileInfoAsync(int fileId,
|
||||
Task<FileInfoDto> GetInfoAsync(int fileId,
|
||||
CancellationToken token);
|
||||
|
||||
Task<int> DeleteFileAsync(int idFile,
|
||||
Task<int> MarkAsDeletedAsync(int idFile,
|
||||
CancellationToken token = default);
|
||||
Task<IEnumerable<FileInfoDto>> GetInfosByCategoryAsync(int idWell, int idCategory, CancellationToken token = default);
|
||||
Task<int> DeletedAsync(int id, CancellationToken token);
|
||||
string GetFileName(FileInfoDto fileInfo);
|
||||
}
|
||||
}
|
||||
|
@ -13,5 +13,6 @@ namespace AsbCloudApp.Services
|
||||
Task<IEnumerable<WellOperationDto>> GetOperationsAsync(int idWell, CancellationToken token);
|
||||
Task<string> GetWellCaptionByIdAsync(int idWell, CancellationToken token);
|
||||
Task<IEnumerable<CompanyDto>> GetCompaniesAsync(int idWell, CancellationToken token);
|
||||
Task<WellDto> GetAsync(int idWell, CancellationToken token);
|
||||
}
|
||||
}
|
||||
|
@ -285,7 +285,8 @@ namespace AsbCloudDb.Model
|
||||
new FileCategory {Id = 9, Name = "Последний замер бурового раствора ФАКТ", ShortName = "fluidFactLastData"},
|
||||
new FileCategory {Id = 10, Name = "Последние данные Шламограммы", ShortName = "mudLastData"},
|
||||
new FileCategory {Id = 11, Name = "Последние данные ННБ", ShortName = "nnbLastData"},
|
||||
new FileCategory {Id = 12, Name = "Рапорт", ShortName = "report"}
|
||||
new FileCategory {Id = 12, Name = "Рапорт", ShortName = "report"},
|
||||
new FileCategory {Id = 13, Name = "Программа бурения", ShortName = "ПБ"},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -12,6 +12,7 @@
|
||||
<PackageReference Include="itext7" Version="7.1.15" />
|
||||
<PackageReference Include="Mapster" Version="7.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" />
|
||||
<PackageReference Include="NPOI" Version="2.5.4" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -39,6 +39,7 @@ namespace AsbCloudInfrastructure
|
||||
services.AddTransient<IWellOperationService, WellOperationService>();
|
||||
services.AddTransient<IWellOperationsStatService, WellOperationsStatService>();
|
||||
services.AddTransient<IMeasureService, MeasureService>();
|
||||
services.AddTransient<IDrillingProgramService, DrillingProgramService>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
81
AsbCloudInfrastructure/Services/DrillingProgramService.cs
Normal file
81
AsbCloudInfrastructure/Services/DrillingProgramService.cs
Normal file
@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AsbCloudApp.Data;
|
||||
using AsbCloudApp.Services;
|
||||
using AsbCloudDb.Model;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NPOI.SS.UserModel;
|
||||
using NPOI.XSSF.UserModel;
|
||||
|
||||
namespace AsbCloudInfrastructure.Services
|
||||
{
|
||||
public class DrillingProgramService : IDrillingProgramService
|
||||
{
|
||||
private readonly IFileService fileService;
|
||||
private readonly WellService wellService;
|
||||
private const int idFileCategoryPlan = 13;
|
||||
|
||||
public DrillingProgramService(IFileService fileService, WellService wellService)
|
||||
{
|
||||
this.fileService = fileService;
|
||||
this.wellService = wellService;
|
||||
}
|
||||
|
||||
public async Task<FileInfoDto> GetAsync(int idWell, CancellationToken token = default)
|
||||
{
|
||||
var filesInfos = await fileService.GetInfosByCategoryAsync(idWell, idFileCategoryPlan, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var well = await wellService.GetAsync(idWell, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var resultFileName = $"Программа бурения {well.Cluster} {well.Caption}.xlsx";
|
||||
|
||||
var matchFile = filesInfos.FirstOrDefault(f=>f.Name == resultFileName);
|
||||
if (matchFile is not null) {
|
||||
if (filesInfos.All(f => f.UploadDate <= matchFile.UploadDate))
|
||||
return matchFile;
|
||||
else
|
||||
await fileService.DeletedAsync(matchFile.Id, token);
|
||||
}
|
||||
|
||||
var fileNames = filesInfos
|
||||
.Where(f => f.Name != resultFileName)
|
||||
.Select(f => fileService.GetFileName(f));
|
||||
|
||||
var stream = new MemoryStream(1024 * 1024);
|
||||
UniteExcelFiles(fileNames, stream);
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
return await fileService.SaveAsync(idWell, 0, idFileCategoryPlan, resultFileName, stream, token)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static void UniteExcelFiles(IEnumerable<string> excelFilesNames, Stream stream)
|
||||
{
|
||||
IWorkbook product = new XSSFWorkbook();
|
||||
|
||||
foreach (var excelFileName in excelFilesNames)
|
||||
{
|
||||
IWorkbook book = new XSSFWorkbook(new FileStream(excelFileName, FileMode.Open));
|
||||
|
||||
for (int i = 0; i < book.NumberOfSheets; i++)
|
||||
{
|
||||
ISheet sheet = book.GetSheetAt(i);
|
||||
try
|
||||
{
|
||||
sheet.CopyTo(product, sheet.SheetName, true, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
//what can't be done - can't be done. ignore it.
|
||||
}
|
||||
}
|
||||
}
|
||||
product.Write(stream);
|
||||
}
|
||||
}
|
||||
}
|
@ -23,35 +23,25 @@ namespace AsbCloudInfrastructure.Services
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
public IDictionary<string, int> SaveFileInfos(int idWell, int idUser,
|
||||
IEnumerable<FileInfoDto> filesInfo)
|
||||
public async Task<FileInfoDto> SaveAsync(int idWell, int idUser, int idCategory, string fileFullName, Stream fileStream, CancellationToken token = default)
|
||||
{
|
||||
var fileIdsToNames = new Dictionary<string, int>();
|
||||
|
||||
foreach (var fileInfo in filesInfo)
|
||||
//save info to db
|
||||
var fileInfo = new AsbCloudDb.Model.FileInfo()
|
||||
{
|
||||
var file = new AsbCloudDb.Model.FileInfo()
|
||||
{
|
||||
Name = fileInfo.Name,
|
||||
IdWell = idWell,
|
||||
IdCategory = fileInfo.IdCategory,
|
||||
UploadDate = fileInfo.UploadDate,
|
||||
IdAuthor = idUser
|
||||
IdAuthor = idUser,
|
||||
IdCategory = idCategory,
|
||||
Name = Path.GetFileName(fileFullName),
|
||||
UploadDate = DateTime.Now,
|
||||
IsDeleted = false,
|
||||
};
|
||||
|
||||
db.Files.Add(file);
|
||||
var entry = db.Files.Add(fileInfo);
|
||||
db.SaveChanges();
|
||||
fileIdsToNames.Add(file.Name, file.Id);
|
||||
}
|
||||
|
||||
return fileIdsToNames;
|
||||
}
|
||||
|
||||
public async Task SaveFile(int idWell, int idCategory, int fileId,
|
||||
string fileExtension, Stream fileStream)
|
||||
{
|
||||
var fileId = entry.Entity.Id;
|
||||
//save stream to disk
|
||||
var relativePath = Path.Combine(RootPath, $"{idWell}",
|
||||
$"{idCategory}", $"{fileId}" + $"{fileExtension}");
|
||||
$"{idCategory}", $"{fileId}" + $"{Path.GetExtension(fileFullName)}");
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(relativePath));
|
||||
|
||||
@ -59,9 +49,30 @@ namespace AsbCloudInfrastructure.Services
|
||||
{
|
||||
await fileStream.CopyToAsync(newfileStream);
|
||||
}
|
||||
|
||||
var dto = entry.Entity.Adapt<FileInfoDto>();
|
||||
return dto;
|
||||
}
|
||||
|
||||
public async Task<PaginationContainer<FileInfoDto>> GetFilesInfoAsync(int idWell,
|
||||
public async Task<IEnumerable<FileInfoDto>> GetInfosByCategoryAsync(int idWell,
|
||||
int idCategory, CancellationToken token = default)
|
||||
{
|
||||
var entities = await db.Files
|
||||
.Include(f => f.Author)
|
||||
.Where(e => e.IdWell == idWell && e.IdCategory == idCategory)
|
||||
.AsNoTracking()
|
||||
.ToListAsync(token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var dtos = entities.Adapt<FileInfoDto, AsbCloudDb.Model.FileInfo>((d,s) => {
|
||||
d.AuthorName = s.Author?.Name;
|
||||
d.CompanyId = s.Author?.IdCompany ?? 0;
|
||||
});
|
||||
|
||||
return dtos;
|
||||
}
|
||||
|
||||
public async Task<PaginationContainer<FileInfoDto>> GetInfosAsync(int idWell,
|
||||
int idCategory, IEnumerable<int> companies = default, DateTime begin = default,
|
||||
DateTime end = default, int skip = 0, int take = 32, CancellationToken token = default)
|
||||
{
|
||||
@ -115,7 +126,7 @@ namespace AsbCloudInfrastructure.Services
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<FileInfoDto> GetFileInfoAsync(int fileId,
|
||||
public async Task<FileInfoDto> GetInfoAsync(int fileId,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var entity = await db.Files
|
||||
@ -132,17 +143,42 @@ namespace AsbCloudInfrastructure.Services
|
||||
return dto;
|
||||
}
|
||||
|
||||
public async Task<int> DeleteFileAsync(int idFile,
|
||||
public async Task<int> MarkAsDeletedAsync(int idFile,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
var fileInfo = db.Files.FirstOrDefault(f => f.Id == idFile);
|
||||
var fileInfo = await db.Files.FirstOrDefaultAsync(f => f.Id == idFile, token).ConfigureAwait(false);
|
||||
|
||||
if (fileInfo is null)
|
||||
return 0;
|
||||
|
||||
fileInfo.IsDeleted = true;
|
||||
|
||||
return await db.SaveChangesAsync(token);
|
||||
return await db.SaveChangesAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task<int> DeletedAsync(int idFile, CancellationToken token)
|
||||
{
|
||||
var fileInfo = await db.Files
|
||||
.FirstOrDefaultAsync(f => f.Id == idFile, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (fileInfo is null)
|
||||
return 0;
|
||||
|
||||
var fileName = GetFileName(fileInfo.Adapt<FileInfoDto>());
|
||||
if (File.Exists(fileName))
|
||||
File.Delete(fileName);
|
||||
|
||||
db.Files.Remove(fileInfo);
|
||||
return await db.SaveChangesAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public string GetFileName(FileInfoDto fileInfo)
|
||||
{
|
||||
var fileName = Path.Combine(fileInfo.Id.ToString(), Path.GetExtension(fileInfo.Name));
|
||||
fileName = Path.Combine(RootPath, fileInfo.IdWell.ToString(), fileInfo.IdCategory.ToString(), fileName);
|
||||
fileName = Path.GetFullPath(fileName);
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -77,6 +77,19 @@ namespace AsbCloudInfrastructure.Services
|
||||
return wellDto;
|
||||
}
|
||||
|
||||
public async Task<WellDto> GetAsync(int idWell, CancellationToken token)
|
||||
{
|
||||
var entity = await db.Wells
|
||||
.Include(w=>w.Cluster)
|
||||
.ThenInclude(c=>c.Deposit)
|
||||
.FirstOrDefaultAsync(w => w.Id == idWell, token)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var dto = entity.Adapt<WellDto>();
|
||||
dto.Cluster = entity.Cluster?.Caption;
|
||||
dto.Deposit = entity.Cluster?.Deposit?.Caption;
|
||||
return dto;
|
||||
}
|
||||
public async Task<string> GetWellCaptionByIdAsync(int idWell, CancellationToken token)
|
||||
{
|
||||
var entity = await cacheWells.FirstOrDefaultAsync(w => w.Id == idWell, token).ConfigureAwait(false);
|
||||
|
34
AsbCloudWebApi/Controllers/DrillingProgramController.cs
Normal file
34
AsbCloudWebApi/Controllers/DrillingProgramController.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using AsbCloudApp.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AsbCloudWebApi.Controllers
|
||||
{
|
||||
[Route("api/well/{idWell}/files")]
|
||||
[ApiController]
|
||||
[Authorize]
|
||||
public class DrillingProgramController : ControllerBase
|
||||
{
|
||||
private readonly IDrillingProgramService drillingProgramService;
|
||||
private readonly IFileService fileService;
|
||||
|
||||
public DrillingProgramController(IDrillingProgramService drillingProgramService, IFileService fileService)
|
||||
{
|
||||
this.drillingProgramService = drillingProgramService;
|
||||
this.fileService = fileService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[ProducesResponseType(typeof(PhysicalFileResult), (int)System.Net.HttpStatusCode.OK)]
|
||||
public async Task<IActionResult> GetAsync(int idWell, CancellationToken token = default)
|
||||
{
|
||||
var fileInfo = await drillingProgramService.GetAsync(idWell, token)
|
||||
.ConfigureAwait(false);
|
||||
var relativePath = fileService.GetFileName(fileInfo);
|
||||
return PhysicalFile(Path.GetFullPath(relativePath), "application/octet-stream", fileInfo.Name);
|
||||
}
|
||||
}
|
||||
}
|
@ -56,20 +56,11 @@ namespace AsbCloudWebApi.Controllers
|
||||
UploadDate = DateTime.Now
|
||||
});
|
||||
|
||||
var fileNamesAndIds = fileService.SaveFileInfos(idWell, (int)idUser,
|
||||
fileInfoCollection);
|
||||
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var fileExtension = Path.GetExtension(file.FileName);
|
||||
|
||||
var fileId = fileNamesAndIds[file.FileName];
|
||||
|
||||
var fileStream = file.OpenReadStream();
|
||||
|
||||
await fileService.SaveFile(idWell, idCategory, fileId,
|
||||
fileExtension, fileStream);
|
||||
await fileService.SaveAsync(idWell, idUser??0, idCategory, file.FileName,
|
||||
fileStream);
|
||||
}
|
||||
|
||||
return Ok();
|
||||
@ -100,7 +91,7 @@ namespace AsbCloudWebApi.Controllers
|
||||
idWell, token).ConfigureAwait(false))
|
||||
return Forbid();
|
||||
|
||||
var filesInfo = await fileService.GetFilesInfoAsync(idWell, idCategory,
|
||||
var filesInfo = await fileService.GetInfosAsync(idWell, idCategory,
|
||||
companies, begin, end, skip, take, token).ConfigureAwait(false);
|
||||
|
||||
if (filesInfo is null || !filesInfo.Items.Any())
|
||||
@ -133,14 +124,13 @@ namespace AsbCloudWebApi.Controllers
|
||||
idWell, token).ConfigureAwait(false))
|
||||
return Forbid();
|
||||
|
||||
var fileInfo = await fileService.GetFileInfoAsync(fileId, token);
|
||||
var fileInfo = await fileService.GetInfoAsync(fileId, token);
|
||||
|
||||
if (fileInfo is null)
|
||||
throw new FileNotFoundException();
|
||||
|
||||
// TODO: словарь content typoв
|
||||
var relativePath = Path.Combine(fileService.RootPath, $"{idWell}", $"{fileInfo.IdCategory}",
|
||||
$"{fileInfo.Id}" + Path.GetExtension($"{fileInfo.Name}"));
|
||||
var relativePath = fileService.GetFileName(fileInfo);
|
||||
return PhysicalFile(Path.GetFullPath(relativePath), "application/octet-stream", fileInfo.Name);
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
@ -167,7 +157,7 @@ namespace AsbCloudWebApi.Controllers
|
||||
idWell, token).ConfigureAwait(false))
|
||||
return Forbid();
|
||||
|
||||
var result = await fileService.DeleteFileAsync(idFile, token);
|
||||
var result = await fileService.MarkAsDeletedAsync(idFile, token);
|
||||
|
||||
return Ok(result);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
|
||||
<PackageReference Include="DocumentFormat.OpenXml" Version="2.13.1" />
|
||||
<PackageReference Include="Mapster" Version="7.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.5" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
|
@ -2,6 +2,9 @@
|
||||
using AsbCloudDb.Model;
|
||||
using AsbCloudInfrastructure.Services;
|
||||
using AsbCloudInfrastructure.Services.Cache;
|
||||
using DocumentFormat.OpenXml;
|
||||
using DocumentFormat.OpenXml.Packaging;
|
||||
using DocumentFormat.OpenXml.Spreadsheet;
|
||||
//using AsbSaubReport;
|
||||
//using AutoMapper;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -15,29 +18,38 @@ namespace ConsoleApp1
|
||||
static void Main(/*string[] args*/)
|
||||
{
|
||||
|
||||
var options = new DbContextOptionsBuilder<AsbCloudDbContext>()
|
||||
.UseNpgsql("Host=localhost;Database=postgres;Username=postgres;Password=q;Persist Security Info=True")
|
||||
.Options;
|
||||
var context = new AsbCloudDbContext(options);
|
||||
//var options = new DbContextOptionsBuilder<AsbCloudDbContext>()
|
||||
// .UseNpgsql("Host=localhost;Database=postgres;Username=postgres;Password=q;Persist Security Info=True")
|
||||
// .Options;
|
||||
//var context = new AsbCloudDbContext(options);
|
||||
|
||||
var mservice = new MeasureService(context, new CacheDb());
|
||||
var r = mservice.GetAllLastAsync(1, default).Result;
|
||||
var fileNames = new string[] { @"d:\temp\1\book1.xlsx", @"d:\temp\1\book2.xlsx" };
|
||||
|
||||
//var idWell = 1;
|
||||
//var dataSource = new ReportDataSourcePgCloud(context, idWell);
|
||||
//var generator = new PdfGenerator(dataSource)
|
||||
//{
|
||||
// ReportDirectory = $"{idWell}",
|
||||
// Begin = DateTime.Now.AddYears(-30),
|
||||
// End = DateTime.Now.AddYears(30),
|
||||
// Step = TimeSpan.FromDays(1),
|
||||
// WithCharts = true,
|
||||
// WithEvents = true
|
||||
//};
|
||||
using var spreadsheetDocument = SpreadsheetDocument.Create(@"d:\temp\1\b.xlsx", SpreadsheetDocumentType.Workbook);
|
||||
// Add a WorkbookPart and Workbook objects.
|
||||
WorkbookPart workbookpart = spreadsheetDocument.AddWorkbookPart();
|
||||
workbookpart.Workbook = new Workbook();
|
||||
|
||||
//generator.OnProgress += Generator_OnProgress;
|
||||
//var s = generator.GetPagesCount();
|
||||
//var fileName = generator.Make();
|
||||
// Add a WorksheetPart to the WorkbookPart.
|
||||
WorksheetPart worksheetPart = workbookpart.AddNewPart<WorksheetPart>();
|
||||
worksheetPart.Worksheet = new Worksheet(new SheetData());
|
||||
|
||||
// Add Sheets to the Workbook.
|
||||
Sheets sheets = spreadsheetDocument.WorkbookPart.Workbook.
|
||||
AppendChild<Sheets>(new Sheets());
|
||||
|
||||
// Append a new worksheet and associate it with the workbook.
|
||||
Sheet sheet = new Sheet()
|
||||
{
|
||||
Id = spreadsheetDocument.WorkbookPart.
|
||||
GetIdOfPart(worksheetPart),
|
||||
SheetId = 1,
|
||||
Name = "mySheetNameISHere!"
|
||||
};
|
||||
sheets.Append(sheet);
|
||||
|
||||
spreadsheetDocument.Save();
|
||||
spreadsheetDocument.Close();
|
||||
|
||||
Console.WriteLine("Done. Press any key to quit.");
|
||||
Console.ReadKey();
|
||||
|
Loading…
Reference in New Issue
Block a user