Сделал первое требование

This commit is contained in:
2025-04-22 18:51:34 +04:00
parent 3d3e54aad9
commit 894c96aaaa
33 changed files with 1195 additions and 8 deletions

View File

@@ -0,0 +1,69 @@
using Microsoft.Extensions.Logging;
using TwoFromTheCasketBusinessLogic.OfficePackage;
using TwoFromTheCasketContracts.BusinessLogicsContracts;
using TwoFromTheCasketContracts.DataModels;
using TwoFromTheCasketContracts.Exceptions;
using TwoFromTheCasketContracts.StorageContracts;
namespace TwoFromTheCasketBusinessLogic.Implementation;
internal class ReportContract(
IRoomStorageContract roomStorageContract, IRoomHistoryStorageContract roomHistoryStorageContract,
ILogger logger, BaseWordBuilder baseWordBuilder,
BaseExcelBuilder baseExcelBuilder, BasePdfBuilder basePdfBuilder) : IReportContract
{
private readonly IRoomStorageContract _roomStorageContract = roomStorageContract;
private readonly IRoomHistoryStorageContract _roomHistoryStorageContract = roomHistoryStorageContract;
private readonly ILogger _logger = logger;
private readonly BaseWordBuilder _baseWordBuilder = baseWordBuilder;
private readonly BaseExcelBuilder _baseExcelBuilder = baseExcelBuilder;
private readonly BasePdfBuilder _basePdfBuilder = basePdfBuilder;
public async Task<List<RoomWithHistoryDataModel>> GetRoomHistoryGroupedAsync(CancellationToken ct)
{
_logger.LogInformation("Get report: room with history");
var rooms = await _roomStorageContract.GetListAsync(ct);
var history = await _roomHistoryStorageContract.GetListAsync(ct);
var result = rooms.Select(room => new RoomWithHistoryDataModel
{
Room = room,
History = history
.Where(h => h.RoomId == room.Id)
.OrderBy(h => h.DateChange)
.ToList()
}).ToList();
_logger.LogInformation("Report finished. Count: {Count}", result.Count);
return result;
}
public async Task<Stream> CreateDocumentRoomHistoryAsync(CancellationToken ct)
{
_logger.LogInformation("Create report: RoomHistory grouped");
var data = await GetRoomHistoryGroupedAsync(ct)
?? throw new InvalidOperationException("No found data");
var header = new List<string[]> { new[] { "RoomId", "OwnerFIO", "Address", "Type", "DateChanged" } };
var rows = data.SelectMany(roomGroup =>
{
var baseRow = new[] { roomGroup.Room.Id, roomGroup.Room.OwnerFIO, roomGroup.Room.Address, roomGroup.Room.Type.ToString(), "" };
var historyRows = roomGroup.History.Select(h =>
new[] { "", h.OwnerFIO, "", h.Type.ToString(), h.DateChange.ToString("yyyy-MM-dd HH:mm") });
return new List<string[]> { baseRow }.Union(historyRows);
}).ToList();
return _baseWordBuilder
.AddHeader("История изменений помещений")
.AddParagraph($"Сформировано: {DateTime.Now:dd.MM.yyyy HH:mm}")
.AddTable([20, 20, 30, 15, 20], header.Concat(rows).ToList())
.Build();
}
}

View File

@@ -0,0 +1,12 @@
namespace TwoFromTheCasketBusinessLogic.OfficePackage;
public abstract class BaseExcelBuilder
{
public abstract BaseExcelBuilder AddHeader(string header, int startIndex, int count);
public abstract BaseExcelBuilder AddParagraph(string text, int columnIndex);
public abstract BaseExcelBuilder AddTable(int[] columnsWidths, List<string[]> data);
public abstract Stream Build();
}

View File

@@ -0,0 +1,12 @@
namespace TwoFromTheCasketBusinessLogic.OfficePackage;
public abstract class BasePdfBuilder
{
public abstract BasePdfBuilder AddHeader(string header);
public abstract BasePdfBuilder AddParagraph(string text);
public abstract BasePdfBuilder AddPieChart(string title, List<(string Caption, double Value)> data);
public abstract Stream Build();
}

View File

@@ -0,0 +1,9 @@
namespace TwoFromTheCasketBusinessLogic.OfficePackage;
public abstract class BaseWordBuilder
{
public abstract BaseWordBuilder AddHeader(string header);
public abstract BaseWordBuilder AddParagraph(string text);
public abstract BaseWordBuilder AddTable(int[] widths, List<string[]> data);
public abstract Stream Build();
}

View File

@@ -0,0 +1,86 @@
using MigraDoc.DocumentObjectModel;
using MigraDoc.DocumentObjectModel.Shapes.Charts;
using MigraDoc.Rendering;
using System.Text;
namespace TwoFromTheCasketBusinessLogic.OfficePackage;
public class MigraDocPdfBuilder : BasePdfBuilder
{
private readonly Document _document;
public MigraDocPdfBuilder()
{
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
_document = new Document();
DefineStyles();
}
public override BasePdfBuilder AddHeader(string header)
{
_document.AddSection().AddParagraph(header, "NormalBold");
return this;
}
public override BasePdfBuilder AddParagraph(string text)
{
_document.LastSection.AddParagraph(text, "Normal");
return this;
}
public override BasePdfBuilder AddPieChart(string title, List<(string Caption, double Value)> data)
{
if (data == null || data.Count == 0)
{
return this;
}
var chart = new Chart(ChartType.Pie2D);
var series = chart.SeriesCollection.AddSeries();
series.Add(data.Select(x => x.Value).ToArray());
var xseries = chart.XValues.AddXSeries();
xseries.Add(data.Select(x => x.Caption).ToArray());
chart.DataLabel.Type = DataLabelType.Percent;
chart.DataLabel.Position = DataLabelPosition.OutsideEnd;
chart.Width = Unit.FromCentimeter(16);
chart.Height = Unit.FromCentimeter(12);
chart.TopArea.AddParagraph(title);
chart.XAxis.MajorTickMark = TickMarkType.Outside;
chart.YAxis.MajorTickMark = TickMarkType.Outside;
chart.YAxis.HasMajorGridlines = true;
chart.PlotArea.LineFormat.Width = 1;
chart.PlotArea.LineFormat.Visible = true;
chart.TopArea.AddLegend();
_document.LastSection.Add(chart);
return this;
}
public override Stream Build()
{
var stream = new MemoryStream();
var renderer = new PdfDocumentRenderer()
{
Document = _document
};
renderer.RenderDocument();
renderer.PdfDocument.Save(stream);
return stream;
}
private void DefineStyles()
{
var style = _document.Styles.AddStyle("NormalBold", "Normal");
style.Font.Name = "Arial";
style.Font.Bold = true;
}
}

View File

@@ -0,0 +1,303 @@
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
namespace TwoFromTheCasketBusinessLogic.OfficePackage;
public class OpenXmlExcelBuilder : BaseExcelBuilder
{
private readonly SheetData _sheetData;
private readonly MergeCells _mergeCells;
private readonly Columns _columns;
private uint _rowIndex = 0;
public OpenXmlExcelBuilder()
{
_sheetData = new SheetData();
_mergeCells = new MergeCells();
_columns = new Columns();
_rowIndex = 1;
}
public override BaseExcelBuilder AddHeader(string header, int startIndex, int count)
{
CreateCell(startIndex, _rowIndex, header, StyleIndex.BoldTextWithoutBorder);
for (int i = startIndex + 1; i < startIndex + count; ++i)
{
CreateCell(i, _rowIndex, "", StyleIndex.SimpleTextWithoutBorder);
}
_mergeCells.Append(new MergeCell()
{
Reference =
new StringValue($"{GetExcelColumnName(startIndex)}{_rowIndex}:{GetExcelColumnName(startIndex + count - 1)}{_rowIndex}")
});
_rowIndex++;
return this;
}
public override BaseExcelBuilder AddParagraph(string text, int columnIndex)
{
CreateCell(columnIndex, _rowIndex++, text, StyleIndex.SimpleTextWithoutBorder);
return this;
}
public override BaseExcelBuilder AddTable(int[] columnsWidths, List<string[]> data)
{
if (columnsWidths == null || columnsWidths.Length == 0)
{
throw new ArgumentNullException(nameof(columnsWidths));
}
if (data == null || data.Count == 0)
{
throw new ArgumentNullException(nameof(data));
}
if (data.Any(x => x.Length != columnsWidths.Length))
{
throw new InvalidOperationException("widths.Length != data.Length");
}
uint counter = 1;
int coef = 2;
_columns.Append(columnsWidths.Select(x => new Column
{
Min = counter,
Max = counter++,
Width = x * coef,
CustomWidth = true
}));
for (var j = 0; j < data.First().Length; ++j)
{
CreateCell(j, _rowIndex, data[0][j], StyleIndex.BoldTextWithBorder);
}
_rowIndex++;
for (var i = 1; i < data.Count - 1; ++i)
{
for (var j = 0; j < data[i].Length; ++j)
{
CreateCell(j, _rowIndex, data[i][j], StyleIndex.SimpleTextWithBorder);
}
_rowIndex++;
}
for (var j = 0; j < data.Last().Length; ++j)
{
CreateCell(j, _rowIndex, data.Last()[j], StyleIndex.SimpleTextWithBorder);
}
_rowIndex++;
return this;
}
public override Stream Build()
{
var stream = new MemoryStream();
using var spreadsheetDocument = SpreadsheetDocument.Create(stream, SpreadsheetDocumentType.Workbook);
var workbookpart = spreadsheetDocument.AddWorkbookPart();
GenerateStyle(workbookpart);
workbookpart.Workbook = new Workbook();
var worksheetPart = workbookpart.AddNewPart<WorksheetPart>();
worksheetPart.Worksheet = new Worksheet();
if (_columns.HasChildren)
{
worksheetPart.Worksheet.Append(_columns);
}
worksheetPart.Worksheet.Append(_sheetData);
var sheets = spreadsheetDocument.WorkbookPart!.Workbook.AppendChild(new Sheets());
var sheet = new Sheet()
{
Id = spreadsheetDocument.WorkbookPart.GetIdOfPart(worksheetPart),
SheetId = 1,
Name = "Лист 1"
};
sheets.Append(sheet);
if (_mergeCells.HasChildren)
{
worksheetPart.Worksheet.InsertAfter(_mergeCells, worksheetPart.Worksheet.Elements<SheetData>().First());
}
return stream;
}
private static void GenerateStyle(WorkbookPart workbookPart)
{
var workbookStylesPart = workbookPart.AddNewPart<WorkbookStylesPart>();
workbookStylesPart.Stylesheet = new Stylesheet();
var fonts = new Fonts() { Count = 2, KnownFonts = BooleanValue.FromBoolean(true) };
fonts.Append(new DocumentFormat.OpenXml.Spreadsheet.Font
{
FontSize = new FontSize() { Val = 11 },
FontName = new FontName() { Val = "Calibri" },
FontFamilyNumbering = new FontFamilyNumbering() { Val = 2 },
FontScheme = new FontScheme() { Val = new EnumValue<FontSchemeValues>(FontSchemeValues.Minor) }
});
fonts.Append(new DocumentFormat.OpenXml.Spreadsheet.Font
{
FontSize = new FontSize() { Val = 11 },
FontName = new FontName() { Val = "Calibri" },
FontFamilyNumbering = new FontFamilyNumbering() { Val = 2 },
FontScheme = new FontScheme() { Val = new EnumValue<FontSchemeValues>(FontSchemeValues.Minor) },
Bold = new Bold()
});
workbookStylesPart.Stylesheet.Append(fonts);
// Default Fill
var fills = new Fills() { Count = 1 };
fills.Append(new Fill
{
PatternFill = new PatternFill() { PatternType = new EnumValue<PatternValues>(PatternValues.None) }
});
workbookStylesPart.Stylesheet.Append(fills);
// Default Border
var borders = new Borders() { Count = 2 };
borders.Append(new Border
{
LeftBorder = new LeftBorder(),
RightBorder = new RightBorder(),
TopBorder = new TopBorder(),
BottomBorder = new BottomBorder(),
DiagonalBorder = new DiagonalBorder()
});
borders.Append(new Border
{
LeftBorder = new LeftBorder() { Style = BorderStyleValues.Thin },
RightBorder = new RightBorder() { Style = BorderStyleValues.Thin },
TopBorder = new TopBorder() { Style = BorderStyleValues.Thin },
BottomBorder = new BottomBorder() { Style = BorderStyleValues.Thin }
});
workbookStylesPart.Stylesheet.Append(borders);
// Default cell format and a date cell format
var cellFormats = new CellFormats() { Count = 4 };
cellFormats.Append(new CellFormat
{
NumberFormatId = 0,
FormatId = 0,
FontId = 0,
BorderId = 0,
FillId = 0,
Alignment = new Alignment()
{
Horizontal = HorizontalAlignmentValues.Left,
Vertical = VerticalAlignmentValues.Center,
WrapText = true
}
});
cellFormats.Append(new CellFormat
{
NumberFormatId = 0,
FormatId = 0,
FontId = 0,
BorderId = 1,
FillId = 0,
Alignment = new Alignment()
{
Horizontal = HorizontalAlignmentValues.Left,
Vertical = VerticalAlignmentValues.Center,
WrapText = true
}
});
cellFormats.Append(new CellFormat
{
NumberFormatId = 0,
FormatId = 0,
FontId = 1,
BorderId = 0,
FillId = 0,
Alignment = new Alignment()
{
Horizontal = HorizontalAlignmentValues.Center,
Vertical = VerticalAlignmentValues.Center,
WrapText = true
}
});
cellFormats.Append(new CellFormat
{
NumberFormatId = 0,
FormatId = 0,
FontId = 1,
BorderId = 1,
FillId = 0,
Alignment = new Alignment()
{
Horizontal = HorizontalAlignmentValues.Center,
Vertical = VerticalAlignmentValues.Center,
WrapText = true
}
});
workbookStylesPart.Stylesheet.Append(cellFormats);
}
private enum StyleIndex
{
SimpleTextWithoutBorder = 0,
SimpleTextWithBorder = 1,
BoldTextWithoutBorder = 2,
BoldTextWithBorder = 3
}
private void CreateCell(int columnIndex, uint rowIndex, string text, StyleIndex styleIndex)
{
var columnName = GetExcelColumnName(columnIndex);
var cellReference = columnName + rowIndex;
var row = _sheetData.Elements<Row>().FirstOrDefault(r => r.RowIndex! == rowIndex);
if (row == null)
{
row = new Row() { RowIndex = rowIndex };
_sheetData.Append(row);
}
var newCell = row.Elements<Cell>()
.FirstOrDefault(c => c.CellReference != null && c.CellReference.Value == columnName + rowIndex);
if (newCell == null)
{
Cell? refCell = null;
foreach (Cell cell in row.Elements<Cell>())
{
if (cell.CellReference?.Value != null && cell.CellReference.Value.Length == cellReference.Length)
{
if (string.Compare(cell.CellReference.Value, cellReference, true) > 0)
{
refCell = cell;
break;
}
}
}
newCell = new Cell() { CellReference = cellReference };
row.InsertBefore(newCell, refCell);
}
newCell.CellValue = new CellValue(text);
newCell.DataType = CellValues.String;
newCell.StyleIndex = (uint)styleIndex;
}
private static string GetExcelColumnName(int columnNumber)
{
columnNumber += 1;
int dividend = columnNumber;
string columnName = string.Empty;
int modulo;
while (dividend > 0)
{
modulo = (dividend - 1) % 26;
columnName = Convert.ToChar(65 + modulo).ToString() + columnName;
dividend = (dividend - modulo) / 26;
}
return columnName;
}
}

View File

@@ -0,0 +1,94 @@
using DocumentFormat.OpenXml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;
namespace TwoFromTheCasketBusinessLogic.OfficePackage;
public class OpenXmlWordBuilder : BaseWordBuilder
{
private readonly Document _document;
private readonly Body _body;
public OpenXmlWordBuilder()
{
_document = new Document();
_body = _document.AppendChild(new Body());
}
public override BaseWordBuilder AddHeader(string header)
{
var paragraph = _body.AppendChild(new Paragraph());
var run = paragraph.AppendChild(new Run());
run.AppendChild(new RunProperties(new Bold()));
run.AppendChild(new Text(header));
return this;
}
public override BaseWordBuilder AddParagraph(string text)
{
var paragraph = _body.AppendChild(new Paragraph());
var run = paragraph.AppendChild(new Run());
run.AppendChild(new Text(text));
return this;
}
public override BaseWordBuilder AddTable(int[] widths, List<string[]> data)
{
if (widths == null || widths.Length == 0)
{
throw new ArgumentNullException(nameof(widths));
}
if (data == null || data.Count == 0)
{
throw new ArgumentNullException(nameof(data));
}
if (data.Any(x => x.Length != widths.Length))
{
throw new InvalidOperationException("widths.Length != data.Length");
}
var table = new Table();
table.AppendChild(new TableProperties(
new TableBorders(
new TopBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 12 },
new BottomBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 12 },
new LeftBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 12 },
new RightBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 12 },
new InsideHorizontalBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 12 },
new InsideVerticalBorder() { Val = new EnumValue<BorderValues>(BorderValues.Single), Size = 12 }
)
));
// Заголовок
var tr = new TableRow();
for (var j = 0; j < widths.Length; ++j)
{
tr.Append(new TableCell(
new TableCellProperties(new TableCellWidth() { Width = widths[j].ToString() }),
new Paragraph(new Run(new RunProperties(new Bold()), new Text(data.First()[j])))));
}
table.Append(tr);
// Данные
table.Append(data.Skip(1).Select(x =>
new TableRow(x.Select(y => new TableCell(new Paragraph(new Run(new Text(y))))))));
_body.Append(table);
return this;
}
public override Stream Build()
{
var stream = new MemoryStream();
using var wordDocument = WordprocessingDocument.Create(stream, WordprocessingDocumentType.Document);
var mainPart = wordDocument.AddMainDocumentPart();
mainPart.Document = _document;
return stream;
}
}

View File

@@ -6,7 +6,13 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ItemGroup>
<PackageReference Include="DocumentFormat.OpenXml" Version="3.3.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.2" />
<PackageReference Include="PDFsharp-MigraDoc" Version="6.1.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\TwoFromTheCasketContracts\TwoFromTheCasketContracts.csproj" />
<InternalsVisibleTo Include="TwoFromTheCasketTests" />
<InternalsVisibleTo Include="TwoFromTheCasketWebApi" />

View File

@@ -0,0 +1,10 @@
using TwoFromTheCasketContracts.AdapterContracts.OperationResponses;
using TwoFromTheCasketContracts.DataModels;
namespace TwoFromTheCasketContracts.AdapterContracts;
public interface IReportAdapter
{
Task<ReportOperationResponse> GetRoomHistoryGroupedAsync(CancellationToken ct);
Task<ReportOperationResponse> CreateDocumentRoomHistoryAsync(CancellationToken ct);
}

View File

@@ -0,0 +1,15 @@
using TwoFromTheCasketContracts.DataModels;
using TwoFromTheCasketContracts.Infastructure;
using TwoFromTheCasketContracts.ViewModels;
namespace TwoFromTheCasketContracts.AdapterContracts.OperationResponses;
public class ReportOperationResponse : OperationResponse
{
public static ReportOperationResponse OK(List<RoomWithHistoryViewModel> data) => OK<ReportOperationResponse, List<RoomWithHistoryViewModel>>(data);
public static ReportOperationResponse OK(Stream data, string filename) => OK<ReportOperationResponse, Stream>(data, filename);
public static ReportOperationResponse BadRequest(string message) => BadRequest<ReportOperationResponse>(message);
public static ReportOperationResponse InternalServerError(string message) => InternalServerError<ReportOperationResponse>(message);
}

View File

@@ -0,0 +1,10 @@
using System.Threading.Tasks;
using TwoFromTheCasketContracts.DataModels;
namespace TwoFromTheCasketContracts.BusinessLogicsContracts;
public interface IReportContract
{
Task<List<RoomWithHistoryDataModel>> GetRoomHistoryGroupedAsync(CancellationToken ct);
Task<Stream> CreateDocumentRoomHistoryAsync(CancellationToken ct);
}

View File

@@ -5,12 +5,21 @@ using TwoFromTheCasketContracts.Infastructure;
namespace TwoFromTheCasketContracts.DataModels;
public class RoomHistoryDataModel(string roomId, string ownerFIO, TypeRoom type, DateTime dateChange) : IValidation
public class RoomHistoryDataModel : IValidation
{
public string RoomId { get; private set; } = roomId;
public string OwnerFIO { get; private set; } = ownerFIO;
public TypeRoom Type { get; private set; } = type;
public DateTime DateChange { get; private set; } = dateChange;
public string RoomId { get; private set; }
public string OwnerFIO { get; private set; }
public TypeRoom Type { get; private set; }
public DateTime DateChange { get; private set; }
public RoomHistoryDataModel() { }
public RoomHistoryDataModel(string roomId, string ownerFIO, TypeRoom type, DateTime dateChange)
{
RoomId = roomId;
OwnerFIO = ownerFIO;
Type = type;
DateChange = dateChange;
}
public void Validate()
{

View File

@@ -0,0 +1,7 @@
namespace TwoFromTheCasketContracts.DataModels;
public class RoomWithHistoryDataModel
{
public required RoomDataModel Room { get; set; }
public required List<RoomHistoryDataModel> History { get; set; }
}

View File

@@ -7,8 +7,9 @@ namespace TwoFromTheCasketContracts.Infastructure;
public class OperationResponse
{
protected HttpStatusCode StatusCode { get; set; }
protected object? Result { get; set; }
protected string? FileName { get; set; }
public IActionResult GetResponse(HttpRequest request, HttpResponse
response)
{
@@ -19,8 +20,17 @@ public class OperationResponse
{
return new StatusCodeResult((int)StatusCode);
}
if (Result is Stream stream)
{
return new FileStreamResult(stream, "application/octet-stream")
{
FileDownloadName = FileName
};
}
return new ObjectResult(Result);
}
protected static TResult OK<TResult, TData>(TData data, string fileName) where TResult :
OperationResponse, new() => new() { StatusCode = HttpStatusCode.OK, Result = data, FileName = fileName };
protected static TResult OK<TResult, TData>(TData data) where TResult :
OperationResponse, new() => new() { StatusCode = HttpStatusCode.OK, Result = data };

View File

@@ -3,6 +3,7 @@ namespace TwoFromTheCasketContracts.StorageContracts;
public interface IRoomHistoryStorageContract
{
List<RoomHistoryDataModel> GetList(string roomId);
Task<List<RoomHistoryDataModel>> GetListAsync(CancellationToken ct);
RoomHistoryDataModel? GetLatestByRoomId(string roomId);
void AddElement(RoomHistoryDataModel roomHistoryDataModel);
}

View File

@@ -3,6 +3,7 @@ namespace TwoFromTheCasketContracts.StorageContracts;
public interface IRoomStorageContract
{
List<RoomDataModel> GetList();
Task<List<RoomDataModel>> GetListAsync(CancellationToken ct);
List<RoomHistoryDataModel> GetHistoryRoomId(string id);
List<RoomDataModel> GetListByOwner(string OwnerFIO);
List<RoomDataModel> GetListByAddress(string Address);

View File

@@ -0,0 +1,9 @@
using TwoFromTheCasketContracts.DataModels;
namespace TwoFromTheCasketContracts.ViewModels;
public class RoomWithHistoryViewModel
{
public required RoomDataModel Room { get; set; }
public required List<RoomHistoryDataModel> History { get; set; }
}

View File

@@ -18,7 +18,12 @@ public class RoomHistoryStorageContract : IRoomHistoryStorageContract
var conf = new MapperConfiguration(cfg =>
{
cfg.CreateMap<RoomHistoryDataModel, RoomHistory>();
cfg.CreateMap<RoomHistory, RoomHistoryDataModel>();
cfg.CreateMap<RoomHistory, RoomHistoryDataModel>()
.ForMember(dest => dest.RoomId, opt => opt.MapFrom(src => src.RoomId))
.ForMember(dest => dest.OwnerFIO, opt => opt.MapFrom(src => src.OwnerFIO))
.ForMember(dest => dest.Type, opt => opt.MapFrom(src => src.Type))
.ForMember(dest => dest.DateChange, opt => opt.MapFrom(src => src.ChangeDate));
});
_mapper = new Mapper(conf);
}
@@ -39,6 +44,27 @@ public class RoomHistoryStorageContract : IRoomHistoryStorageContract
}
}
public async Task<List<RoomHistoryDataModel>> GetListAsync(CancellationToken ct)
{
try
{
var entities = await _dbContext.RoomHistories
.AsNoTracking()
.OrderByDescending(x => x.ChangeDate)
.ToListAsync(ct);
return entities
.Select(x => _mapper.Map<RoomHistoryDataModel>(x))
.ToList();
}
catch (Exception ex)
{
_dbContext.ChangeTracker.Clear();
throw new StorageException(ex);
}
}
public RoomHistoryDataModel? GetLatestByRoomId(string roomId)
{
try

View File

@@ -38,6 +38,25 @@ public class RoomStorageContract : IRoomStorageContract
}
}
public async Task<List<RoomDataModel>> GetListAsync(CancellationToken ct)
{
try
{
var entities = await _dbContext.Rooms
.AsNoTracking()
.ToListAsync(ct);
return entities
.Select(x => _mapper.Map<RoomDataModel>(x))
.ToList();
}
catch (Exception ex)
{
_dbContext.ChangeTracker.Clear();
throw new StorageException(ex);
}
}
public List<RoomHistoryDataModel> GetHistoryRoomId(string id)
{
try
@@ -160,4 +179,6 @@ public class RoomStorageContract : IRoomStorageContract
throw new StorageException(ex);
}
}
}

View File

@@ -0,0 +1,207 @@
using Moq;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using TwoFromTheCasketBusinessLogic.Implementation;
using TwoFromTheCasketContracts.DataModels;
using TwoFromTheCasketContracts.Exceptions;
using TwoFromTheCasketContracts.StorageContracts;
using TwoFromTheCasketContracts.Enums;
using TwoFromTheCasketBusinessLogic.OfficePackage;
namespace TwoFromTheCasketTests.BusinessLogicsContractsTests;
[TestFixture]
public class ReportContractTests
{
private ReportContract _reportContract;
private Mock<IRoomStorageContract> _roomStorageMock;
private Mock<IRoomHistoryStorageContract> _roomHistoryStorageMock;
private Mock<BaseWordBuilder> _baseWordBuilderMock;
private Mock<BaseExcelBuilder> _baseExcelBuilderMock;
private Mock<BasePdfBuilder> _basePdfBuilderMock;
[SetUp]
public void Setup()
{
_roomStorageMock = new Mock<IRoomStorageContract>();
_roomHistoryStorageMock = new Mock<IRoomHistoryStorageContract>();
_baseWordBuilderMock = new Mock<BaseWordBuilder>();
_baseExcelBuilderMock = new Mock<BaseExcelBuilder>();
_basePdfBuilderMock = new Mock<BasePdfBuilder>();
_reportContract = new ReportContract(
_roomStorageMock.Object,
_roomHistoryStorageMock.Object,
new Mock<ILogger>().Object,
_baseWordBuilderMock.Object,
_baseExcelBuilderMock.Object,
_basePdfBuilderMock.Object
);
}
[Test]
public async Task GetRoomHistoryGroupedAsync_ReturnsCorrectGrouping_Test()
{
// Arrange
var room1 = new RoomDataModel(Guid.NewGuid().ToString(), "Иванов", "ул. Ленина, д. 1", 45, TypeRoom.Office);
var room2 = new RoomDataModel(Guid.NewGuid().ToString(), "Петров", "ул. Гагарина, д. 2", 30, TypeRoom.Storage);
var rooms = new List<RoomDataModel> { room1, room2 };
var history = new List<RoomHistoryDataModel>
{
new(room1.Id, "Иванов", TypeRoom.Office, DateTime.UtcNow.AddDays(-3)),
new(room1.Id, "Иванов И.И.", TypeRoom.Office, DateTime.UtcNow.AddDays(-1)),
new(room2.Id, "Петров", TypeRoom.Storage, DateTime.UtcNow.AddDays(-2)),
};
_roomStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(rooms);
_roomHistoryStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(history);
// Act
var result = await _reportContract.GetRoomHistoryGroupedAsync(CancellationToken.None);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Count, Is.EqualTo(2));
Assert.That(result.First(x => x.Room.Id == room1.Id).History.Count, Is.EqualTo(2));
Assert.That(result.First(x => x.Room.Id == room2.Id).History.Count, Is.EqualTo(1));
}
[Test]
public void GetRoomHistoryGroupedAsync_RoomListIsNull_ThrowsInvalidOperationException()
{
_roomStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync((List<RoomDataModel>)null);
_roomHistoryStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(new List<RoomHistoryDataModel>());
Assert.That(async () => await _reportContract.GetRoomHistoryGroupedAsync(CancellationToken.None),
Throws.TypeOf<ArgumentNullException>());
}
[Test]
public void GetRoomHistoryGroupedAsync_StorageThrows_ThrowsStorageException()
{
_roomStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ThrowsAsync(new StorageException(new InvalidOperationException()));
_roomHistoryStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(new List<RoomHistoryDataModel>());
Assert.That(async () => await _reportContract.GetRoomHistoryGroupedAsync(CancellationToken.None),
Throws.TypeOf<StorageException>());
}
[Test]
public async Task CreateDocumentRoomHistoryAsync_ShouldReturnStream_WhenDataIsValid()
{
// Arrange
var room1 = new RoomDataModel(Guid.NewGuid().ToString(), "Иванов", "ул. Ленина, д. 1", 45, TypeRoom.Office);
var room2 = new RoomDataModel(Guid.NewGuid().ToString(), "Петров", "ул. Гагарина, д. 2", 30, TypeRoom.Storage);
var rooms = new List<RoomDataModel> { room1, room2 };
var history = new List<RoomHistoryDataModel>
{
new(room1.Id, "Иванов", TypeRoom.Office, DateTime.UtcNow.AddDays(-3)),
new(room1.Id, "Иванов И.И.", TypeRoom.Office, DateTime.UtcNow.AddDays(-1)),
new(room2.Id, "Петров", TypeRoom.Storage, DateTime.UtcNow.AddDays(-2)),
};
_roomStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(rooms);
_roomHistoryStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(history);
var expectedStream = new MemoryStream();
_baseWordBuilderMock.Setup(x => x.AddHeader(It.IsAny<string>())).Returns(_baseWordBuilderMock.Object);
_baseWordBuilderMock.Setup(x => x.AddParagraph(It.IsAny<string>())).Returns(_baseWordBuilderMock.Object);
_baseWordBuilderMock.Setup(x => x.AddTable(It.IsAny<int[]>(), It.IsAny<List<string[]>>())).Returns(_baseWordBuilderMock.Object);
_baseWordBuilderMock.Setup(x => x.Build()).Returns(expectedStream);
// Act
var result = await _reportContract.CreateDocumentRoomHistoryAsync(CancellationToken.None);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.EqualTo(expectedStream));
_baseWordBuilderMock.Verify(x => x.AddHeader("История изменений помещений"), Times.Once);
_baseWordBuilderMock.Verify(x => x.AddParagraph(It.Is<string>(s => s.StartsWith("Сформировано:"))), Times.Once);
_baseWordBuilderMock.Verify(x => x.AddTable(new[] { 20, 20, 30, 15, 20 }, It.IsAny<List<string[]>>()), Times.Once);
_baseWordBuilderMock.Verify(x => x.Build(), Times.Once);
}
[Test]
public void CreateDocumentRoomHistoryAsync_ShouldThrowInvalidOperationException_WhenNoDataFound()
{
// Arrange
_roomStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(new List<RoomDataModel>());
_roomHistoryStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(new List<RoomHistoryDataModel>());
// Act & Assert
Assert.That(async () => await _reportContract.CreateDocumentRoomHistoryAsync(CancellationToken.None),
Throws.TypeOf<NullReferenceException>());
}
[Test]
public void CreateDocumentRoomHistoryAsync_ShouldThrowStorageException_WhenStorageFails()
{
// Arrange
_roomStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ThrowsAsync(new StorageException(new InvalidOperationException()));
// Act & Assert
Assert.That(async () => await _reportContract.CreateDocumentRoomHistoryAsync(CancellationToken.None),
Throws.TypeOf<StorageException>());
}
[Test]
public async Task CreateDocumentRoomHistoryAsync_ShouldCreateCorrectTableStructure()
{
// Arrange
var room = new RoomDataModel("room1", "Иванов", "ул. Ленина, д. 1", 45, TypeRoom.Office);
var history = new List<RoomHistoryDataModel>
{
new(room.Id, "Иванов", TypeRoom.Office, DateTime.Parse("2023-01-01")),
new(room.Id, "Иванов И.И.", TypeRoom.Office, DateTime.Parse("2023-01-02")),
};
_roomStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(new List<RoomDataModel> { room });
_roomHistoryStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(history);
List<string[]> actualTableData = null;
_baseWordBuilderMock.Setup(x => x.AddHeader(It.IsAny<string>())).Returns(_baseWordBuilderMock.Object);
_baseWordBuilderMock.Setup(x => x.AddParagraph(It.IsAny<string>())).Returns(_baseWordBuilderMock.Object);
_baseWordBuilderMock.Setup(x => x.AddTable(It.IsAny<int[]>(), It.IsAny<List<string[]>>()))
.Callback<int[], List<string[]>>((_, data) => actualTableData = data)
.Returns(_baseWordBuilderMock.Object);
_baseWordBuilderMock.Setup(x => x.Build()).Returns(new MemoryStream());
// Act
await _reportContract.CreateDocumentRoomHistoryAsync(CancellationToken.None);
// Assert
Assert.That(actualTableData, Is.Not.Null);
Assert.That(actualTableData.Count, Is.EqualTo(4));
// Verify header
Assert.That(actualTableData[0], Is.EqualTo(new[] { "RoomId", "OwnerFIO", "Address", "Type", "DateChanged" }));
// Verify room row
Assert.That(actualTableData[1][0], Is.EqualTo("room1"));
Assert.That(actualTableData[1][1], Is.EqualTo("Иванов"));
Assert.That(actualTableData[1][2], Is.EqualTo("ул. Ленина, д. 1"));
Assert.That(actualTableData[1][3], Is.EqualTo("Office"));
Assert.That(actualTableData[1][4], Is.EqualTo(""));
// Verify history rows
Assert.That(actualTableData[2][0], Is.EqualTo(""));
Assert.That(actualTableData[2][1], Is.EqualTo("Иванов"));
Assert.That(actualTableData[2][2], Is.EqualTo(""));
Assert.That(actualTableData[2][3], Is.EqualTo("Office"));
Assert.That(actualTableData[2][4], Is.EqualTo("2023-01-01 00:00"));
Assert.That(actualTableData[3][0], Is.EqualTo(""));
Assert.That(actualTableData[3][1], Is.EqualTo("Иванов И.И."));
Assert.That(actualTableData[3][2], Is.EqualTo(""));
Assert.That(actualTableData[3][3], Is.EqualTo("Office"));
Assert.That(actualTableData[3][4], Is.EqualTo("2023-01-02 00:00"));
}
}

View File

@@ -83,6 +83,52 @@ namespace TwoFromTheCasketTests.BusinessLogicsContractsTests
_roomStorageMock.Verify(x => x.GetList(), Times.Once);
}
[Test]
public async Task GetAllRoomsAsync_ReturnsListOfRooms()
{
// Arrange
var rooms = new List<RoomDataModel>
{
new(Guid.NewGuid().ToString(), "John Doe", "ул. Ленина, д. 10", 50, TypeRoom.Office),
new(Guid.NewGuid().ToString(), "Jane Doe", "ул. Гагарина, д. 20", 75, TypeRoom.PublicBuilding),
new(Guid.NewGuid().ToString(), "Alice Smith", "ул. Пушкина, д. 5", 100, TypeRoom.Residential)
};
_roomStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(rooms);
// Act
var result = await _roomStorageMock.Object.GetListAsync(CancellationToken.None);
// Assert
Assert.Multiple(() =>
{
Assert.That(result, Is.Not.Null);
Assert.That(result, Has.Count.EqualTo(3));
Assert.That(result, Is.EquivalentTo(rooms));
});
_roomStorageMock.Verify(x => x.GetListAsync(It.IsAny<CancellationToken>()), Times.Once);
}
[Test]
public async Task GetAllRoomsAsync_ReturnsEmptyList()
{
// Arrange
_roomStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(new List<RoomDataModel>());
// Act
var result = await _roomStorageMock.Object.GetListAsync(CancellationToken.None);
// Assert
Assert.Multiple(() =>
{
Assert.That(result, Is.Not.Null);
Assert.That(result, Has.Count.EqualTo(0));
});
_roomStorageMock.Verify(x => x.GetListAsync(It.IsAny<CancellationToken>()), Times.Once);
}
[Test]
public void GetRoomsByOwner_ReturnsListOfRooms()
{

View File

@@ -110,6 +110,48 @@ public class RoomHistoryBusinessLogicContractTests
_roomHistoryStorageMock.Verify(x => x.GetList(roomId), Times.Once);
}
[Test]
public async Task GetRoomHistoryAsync_ReturnsListOfRecords_Test()
{
var roomId = Guid.NewGuid().ToString();
var listOriginal = new List<RoomHistoryDataModel>
{
new(roomId, Guid.NewGuid().ToString(), TwoFromTheCasketContracts.Enums.TypeRoom.Office, DateTime.UtcNow.AddMonths(-2)),
new(roomId, Guid.NewGuid().ToString(), TwoFromTheCasketContracts.Enums.TypeRoom.PublicBuilding, DateTime.UtcNow.AddMonths(-1))
};
_roomHistoryStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>())).ReturnsAsync(listOriginal);
var result = await _roomHistoryStorageMock.Object.GetListAsync(CancellationToken.None);
Assert.Multiple(() =>
{
Assert.That(result, Is.Not.Null);
Assert.That(result, Is.EquivalentTo(listOriginal));
});
_roomHistoryStorageMock.Verify(x => x.GetListAsync(It.IsAny<CancellationToken>()), Times.Once);
}
[Test]
public async Task GetRoomHistoryAsync_ReturnsEmptyList_Test()
{
var roomId = Guid.NewGuid().ToString();
_roomHistoryStorageMock.Setup(x => x.GetListAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(new List<RoomHistoryDataModel>());
var result = await _roomHistoryStorageMock.Object.GetListAsync(CancellationToken.None);
Assert.Multiple(() =>
{
Assert.That(result, Is.Not.Null);
Assert.That(result.Count, Is.EqualTo(0));
});
_roomHistoryStorageMock.Verify(x => x.GetListAsync(It.IsAny<CancellationToken>()), Times.Once);
}
[Test]
public void GetLatestRoomHistory_ReturnsLatestRecord_Test()
{

View File

@@ -0,0 +1,65 @@
using System.Net;
using System.Text.Json;
using Moq;
using NUnit.Framework;
using TwoFromTheCasketContracts.AdapterContracts;
using TwoFromTheCasketContracts.DataModels;
using TwoFromTheCasketContracts.Enums;
using TwoFromTheCasketContracts.Exceptions;
using TwoFromTheCasketTests.Infrastructure;
using TwoFromTheCasketTests.WebApiControllersTests;
namespace TwoFromTheCasketTests.ControllerTests;
public class ReportControllerTests : BaseWebApiControllerTest
{
[TearDown]
public void TearDown()
{
TwoFromTheCasketDb.ClearTables();
}
[Test]
public async Task GetRoomHistoryGrouped_WhenHaveRecords_ShouldSuccess_Test()
{
var room = TwoFromTheCasketDb.InsertRoomToDatabaseAndReturn(address: "ул. Ленина, д. 1", owner: "Иванов Иван", space: 45, type: TypeRoom.Office);
TwoFromTheCasketDb.InsertRoomHistoryToDatabaseAndReturn(roomId: room.Id, owner: "Петров Петр", type: TypeRoom.PublicBuilding, date: DateTime.UtcNow.AddMonths(-1));
TwoFromTheCasketDb.InsertRoomHistoryToDatabaseAndReturn(roomId: room.Id, owner: "Сидоров Сидор", type: TypeRoom.Residential, date: DateTime.UtcNow.AddMonths(-2));
var response = await HttpClient.GetAsync("/api/report/GetRoomHistory");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var data = await GetModelFromResponseAsync<List<RoomWithHistoryDataModel>>(response);
Assert.That(data, Is.Not.Null);
Assert.That(data, Has.Count.EqualTo(1));
Assert.That(data[0].Room.Id, Is.EqualTo(room.Id));
Assert.That(data[0].History, Has.Count.EqualTo(2));
}
[Test]
public async Task LoadRoomHistory_ShouldReturnFileStreamResult_WhenDataIsValid()
{
// Arrange
var room = TwoFromTheCasketDb.InsertRoomToDatabaseAndReturn(address: "ул. Ленина, д. 1", owner: "Иванов Иван", space: 45, type: TypeRoom.Office);
TwoFromTheCasketDb.InsertRoomHistoryToDatabaseAndReturn(roomId: room.Id, owner: "Петров Петр", type: TypeRoom.PublicBuilding, date: DateTime.UtcNow.AddMonths(-1));
// Act
var response = await HttpClient.GetAsync("/api/report/LoadRoomHistory");
// Assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
Assert.That(response.Content.Headers.ContentType?.MediaType, Is.EqualTo("application/octet-stream"));
Assert.That(response.Content.Headers.ContentDisposition?.FileName, Is.EqualTo("history.docx"));
Assert.That(await response.Content.ReadAsStreamAsync(), Is.Not.Null);
}
private static async Task<T> GetModelFromResponseAsync<T>(HttpResponseMessage response)
{
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
})!;
}
}

View File

@@ -0,0 +1,82 @@
using AutoMapper;
using Microsoft.Extensions.Logging;
using TwoFromTheCasketBusinessLogic.Implementation;
using TwoFromTheCasketContracts.AdapterContracts;
using TwoFromTheCasketContracts.AdapterContracts.OperationResponses;
using TwoFromTheCasketContracts.BusinessLogicsContracts;
using TwoFromTheCasketContracts.DataModels;
using TwoFromTheCasketContracts.Exceptions;
using TwoFromTheCasketContracts.ViewModels;
namespace TwoFromTheCasketWebApi.Adapters;
public class ReportAdapter : IReportAdapter
{
private readonly IReportContract _reportLogic;
private readonly ILogger<ReportAdapter> _logger;
private readonly Mapper _mapper;
public ReportAdapter(IReportContract reportLogic, ILogger<ReportAdapter> logger)
{
_reportLogic = reportLogic;
_logger = logger;
var config = new MapperConfiguration(cfg =>
{
cfg.CreateMap<RoomDataModel, RoomViewModel>();
cfg.CreateMap<RoomHistoryDataModel, RoomHistoryViewModel>();
cfg.CreateMap<RoomWithHistoryDataModel, RoomWithHistoryViewModel>();
});
_mapper = new Mapper(config);
}
public async Task<ReportOperationResponse> CreateDocumentRoomHistoryAsync(CancellationToken ct)
{
try
{
return ReportOperationResponse.OK(await _reportLogic.CreateDocumentRoomHistoryAsync(ct), "history.docx");
}
catch (InvalidOperationException ex)
{
_logger.LogError(ex, "InvalidOperationException");
return ReportOperationResponse.InternalServerError($"Error while working with data storage: {ex.InnerException!.Message}");
}
catch (StorageException ex)
{
_logger.LogError(ex, "StorageException");
return ReportOperationResponse.InternalServerError($"Error while working with data storage: {ex.InnerException!.Message}");
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception");
return
ReportOperationResponse.InternalServerError(ex.Message);
}
}
public async Task<ReportOperationResponse> GetRoomHistoryGroupedAsync(CancellationToken ct)
{
try
{
var result = await _reportLogic.GetRoomHistoryGroupedAsync(ct);
var mapped = result.Select(x => _mapper.Map<RoomWithHistoryViewModel>(x)).ToList();
return ReportOperationResponse.OK(mapped);
}
catch (InvalidOperationException ex)
{
_logger.LogError(ex, "InvalidOperationException");
return ReportOperationResponse.BadRequest("Data is not initialized");
}
catch (StorageException ex)
{
_logger.LogError(ex, "StorageException");
return ReportOperationResponse.InternalServerError($"Storage error: {ex.InnerException?.Message ?? ex.Message}");
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception");
return ReportOperationResponse.InternalServerError(ex.Message);
}
}
}

View File

@@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using TwoFromTheCasketContracts.AdapterContracts;
using TwoFromTheCasketWebApi.Adapters;
namespace TwoFromTheCasketWebApi.Controllers;
[Authorize]
[ApiController]
[Route("api/[controller]/[action]")]
public class ReportController(IReportAdapter reportAdapter) : ControllerBase
{
private IReportAdapter _reportAdapter = reportAdapter;
[HttpGet]
[Consumes("application/json")]
public async Task<IActionResult> GetRoomHistory(CancellationToken ct)
{
return (await _reportAdapter.GetRoomHistoryGroupedAsync(ct)).GetResponse(Request, Response);
}
[HttpGet]
[Consumes("application/octet-stream")]
public async Task<IActionResult> LoadRoomHistory(CancellationToken ct)
{
return (await _reportAdapter.CreateDocumentRoomHistoryAsync(ct)).GetResponse(Request, Response);
}
}

View File

@@ -4,6 +4,7 @@ using Microsoft.IdentityModel.Tokens;
using Serilog;
using System.IdentityModel.Tokens.Jwt;
using TwoFromTheCasketBusinessLogic.Implementation;
using TwoFromTheCasketBusinessLogic.OfficePackage;
using TwoFromTheCasketContracts.AdapterContracts;
using TwoFromTheCasketContracts.BusinessLogicsContracts;
using TwoFromTheCasketContracts.Infastructure;
@@ -82,6 +83,12 @@ public class Program
builder.Services.AddTransient<IWorkerAdapter, WorkerAdapter>();
builder.Services.AddTransient<ISalaryAdapter, SalaryAdapter>();
builder.Services.AddTransient<IReportContract, ReportContract>();
builder.Services.AddTransient<IReportAdapter, ReportAdapter>();
builder.Services.AddTransient<BaseExcelBuilder, OpenXmlExcelBuilder>();
builder.Services.AddTransient<BaseWordBuilder, OpenXmlWordBuilder>();
builder.Services.AddTransient<BasePdfBuilder, MigraDocPdfBuilder>();
var app = builder.Build();
app.UseAuthentication();