Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V2dev.full crud #1038

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
<PackageVersion Include="Ardalis.Specification.EntityFrameworkCore" Version="8.0.0" />
<PackageVersion Include="Asp.Versioning.Http" Version="8.1.0" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageVersion Include="ClosedXML" Version="0.102.3" />
<PackageVersion Include="ClosedXML.Report" Version="0.2.10" />
<PackageVersion Include="Finbuckle.MultiTenant" Version="7.0.2" />
<PackageVersion Include="Finbuckle.MultiTenant.AspNetCore" Version="7.0.2" />
<PackageVersion Include="Finbuckle.MultiTenant.EntityFrameworkCore" Version="7.0.2" />
Expand All @@ -36,7 +38,7 @@
<PackageVersion Include="OpenTelemetry.Instrumentation.Process" Version="0.5.0-beta.6" />
<PackageVersion Include="Serilog.Expressions" Version="5.0.0" />
<PackageVersion Include="Serilog.Sinks.OpenTelemetry" Version="4.0.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.7.2" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.8.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Common" Version="4.7.0" />
Expand Down
3 changes: 3 additions & 0 deletions src/FSH.Starter.sln
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Migrations", "Migrations", "{12F8343D-20A6-4E24-B0F5-3A66F2228CF6}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "WebApi", "WebApi", "{CE64E92B-E088-46FB-9028-7FB6B67DEC55}"
ProjectSection(SolutionItems) = preProject
Directory.Packages.props = Directory.Packages.props
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Blazor", "Blazor", "{2B1F75CE-07A6-4C19-A2E3-F9E062CFDDFB}"
EndProject
Expand Down
8 changes: 8 additions & 0 deletions src/api/framework/Core/DataIO/IDataExport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace FSH.Framework.Core.DataIO;

public interface IDataExport
{
byte[] ListToByteArray<T>(IList<T> list);
Stream WriteToStream<T>(IList<T> data);
Stream WriteToTemplate<T>(T data, string templateFile, string outputFolder);
}
10 changes: 10 additions & 0 deletions src/api/framework/Core/DataIO/IDataImport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using FSH.Framework.Core.Storage.File;
using FSH.Framework.Core.Storage.File.Features;

namespace FSH.Framework.Core.DataIO;

public interface IDataImport
{

Task<IList<T>> ToListAsync<T>(FileUploadCommand request, FileType supportedFileType, string sheetName = "Sheet1");
}
8 changes: 8 additions & 0 deletions src/api/framework/Core/DataIO/ImportResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace FSH.Framework.Core.DataIO;

public class ImportResponse
{
public int TotalRecords { get; set; }

public string? Message { get; set; }
}
11 changes: 10 additions & 1 deletion src/api/framework/Core/Storage/File/FileType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,14 @@ namespace FSH.Framework.Core.Storage.File;
public enum FileType
{
[Description(".jpg,.png,.jpeg")]
Image
Image,

[Description(".xls,.xlsx")]
Excel,

[Description(".zip")]
QuizMedia,

[Description(".pdf,.doc,.zip,.rar")]
Doc
}
158 changes: 158 additions & 0 deletions src/api/framework/Infrastructure/DataIO/DataExport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
using ClosedXML.Excel;
using ClosedXML.Report;
using System.ComponentModel;
using System.Data;
using System.Reflection;
using FSH.Framework.Core.DataIO;

namespace FSH.Framework.Infrastructure.DataIO;

public class DataExport : IDataExport
{
/// <summary>
///
/// </summary>
/// <param name="list"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public byte[] ListToByteArray<T>(IList<T> list)
{
if (list is null || list.Count is 0)
{
throw new ArgumentNullException(nameof(list));
}

// Create DataTable from List

DataTable dataTable = ListToDataTable(list);

// Create IXLWorkbook from DataTable
// IXLWorkbook workbook = DataTableToIXLWorkbook(typeof(T).Name, dataTable)

XLWorkbook workbook = DataTableToIxlWorkbook("Sheet1", dataTable);

// Convert IXLWorkbook to ByteArray

using MemoryStream memoryStream = new();
workbook.SaveAs(memoryStream);
byte[] fileByteArray = memoryStream.ToArray();

return fileByteArray ;
}

/// <summary>
/// Creates a DataTable from a List of type <typeparamref name="T"/>; using the properties of <typeparamref name="T"/> to create the DataTable Columns and the items from List of type <typeparamref name="T"/> to create the DataTables Rows.
/// </summary>
/// <typeparam name="T">DataType used to create the DataTable; DataType properties are used to create the DataTable Columns.</typeparam>
/// <param name="list">List of items to create the rows of the DataTable.</param>
/// <returns>Returns a DataTable created from the List of type <typeparamref name="T"/></returns>
///
private static DataTable ListToDataTable<T>(IList<T> list)
{
if (list is null || list.Count is 0)
{
throw new ArgumentNullException(nameof(list));
}

DataTable dataTable = new(typeof(T).Name);

// Create data table columns from data model properties
PropertyInfo[] properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (PropertyInfo property in properties)
{
dataTable.Columns.Add(property.Name);
}

// Create data table rows from list items
foreach (T item in list)
{
object?[] values = new object?[properties.Length];
for (int i = 0; i < properties.Length; i++)
{
//inserting property values to datatable rows
values[i] = properties[i].GetValue(item, null);
}

dataTable.Rows.Add(values);
}

return dataTable;
}

/// <summary>
/// Create XLWorkbook from Datatable
/// </summary>
/// <param name="workbookName"></param>
/// <param name="dataTable"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
private static XLWorkbook DataTableToIxlWorkbook(string workbookName, DataTable dataTable)
{
if (string.IsNullOrWhiteSpace(workbookName))
{
throw new ArgumentNullException(nameof(workbookName));
}

if (dataTable is null || dataTable.Rows.Count is 0)
{
throw new ArgumentNullException(nameof(dataTable));
}

XLWorkbook workbook = new();
workbook.Worksheets.Add(dataTable, workbookName);
return workbook;
}

public Stream WriteToStream<T>(IList<T> data)
{
var properties = TypeDescriptor.GetProperties(typeof(T));
var table = new DataTable("Sheet1", "table"); // "Sheet1" = typeof(T).Name

foreach (PropertyDescriptor prop in properties)
table.Columns.Add(prop.Name, Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);

foreach (var item in data)
{
var row = table.NewRow();
foreach (PropertyDescriptor prop in properties)
row[prop.Name] = prop.GetValue(item) ?? DBNull.Value;
table.Rows.Add(row);
}

using var wb = new XLWorkbook();
wb.Worksheets.Add(table);

Stream stream = new MemoryStream();
wb.SaveAs(stream);
stream.Seek(0, SeekOrigin.Begin);

return stream;
}

/// <summary>
///
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data"></param>
/// <param name="templateFile"></param>
/// <param name="outputFolder"></param>
/// <returns></returns>
public Stream WriteToTemplate<T>(T data, string templateFile, string outputFolder)
{
var template = new XLTemplate(templateFile);
template.AddVariable(data);
template.Generate();

// save to file on API server
//const string outputFile = @".\Output\AssetDeliveryFrom.xlsx"
string outputFile = outputFolder + templateFile;
template.SaveAs(outputFile);

// or get bytes to return excel file from web api
Stream stream = new MemoryStream();
template.Workbook.SaveAs(stream);
stream.Seek(0, SeekOrigin.Begin);
return stream;
}
}
110 changes: 110 additions & 0 deletions src/api/framework/Infrastructure/DataIO/DataImport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using System.Text.RegularExpressions;
using ClosedXML.Excel;
using FSH.Framework.Core.DataIO;
using FSH.Framework.Core.Exceptions;
using FSH.Framework.Core.Storage.File;
using FSH.Framework.Core.Storage.File.Features;

namespace FSH.Framework.Infrastructure.DataIO;

public class DataImport : IDataImport
{
public async Task<IList<T>> ToListAsync<T>(FileUploadCommand request, FileType supportedFileType, string sheetName = "Sheet1")
{
// string base64Data = Regex.Match(request.Data, string.Format("data:{0}/(?<type>.+?),(?<data>.+)", supportedFileType.ToString().ToLower())).Groups["data"].Value
string base64Data = Regex.Match(request.Data,
$"data:{supportedFileType.ToString().ToLower()}/(?<type>.+?),(?<data>.+)").Groups["data"].Value;

var streamData = new MemoryStream(Convert.FromBase64String(base64Data));

List<T> list = [];
Type typeOfObject = typeof(T);

using (IXLWorkbook workbook = new XLWorkbook(streamData))
{
// Read the first Sheet from Excel file.
var worksheet = workbook.Worksheets.FirstOrDefault(w => w.Name == sheetName)
?? throw new NotFoundException($"Sheet with name {sheetName} does not exist!");


var properties = typeOfObject.GetProperties();
// header column texts
var columns = worksheet.FirstRow().Cells().Select((v, i) => new { v.Value, Index = i + 1 });

// indexing in closedxml starts with 1 not from 0
// Skip first row which is used for column header texts
foreach (IXLRow row in worksheet.RowsUsed().Skip(1))
{
T item = (T)Activator.CreateInstance(typeOfObject)!;

foreach (var prop in properties)
{
try
{
var propertyType = prop.PropertyType;
var col = columns.SingleOrDefault(c => c.Value.ToString() == prop.Name);
if (col == null) continue;

object? obj = GetObjectByDataType(propertyType, row.Cell(col.Index).Value);

if(obj != null) prop.SetValue(item, obj);
}
catch
{
// if any error
// return await Task.FromResult(new List<T>())
}
}

list.Add(item);
}
}

return await Task.FromResult(list);
}

private static object? GetObjectByDataType(Type propertyType, XLCellValue cellValue)
{
if (cellValue.ToString() == "null" || cellValue.IsBlank)
{
return null;
}

object? val;
if (propertyType.IsEnum)
{
val = Convert.ToInt32(cellValue.GetNumber());
return Enum.ToObject(propertyType, val);
}
else if (propertyType == typeof(Guid) || propertyType == typeof(Guid?))
{
val = Guid.Parse(cellValue.ToString());
}
else if (propertyType == typeof(int) || propertyType == typeof(int?))
{
val = Convert.ToInt32(cellValue.GetNumber());
}
else if (propertyType == typeof(decimal))
{
val = Convert.ToDecimal(cellValue.GetNumber());
}
else if (propertyType == typeof(long))
{
val = Convert.ToInt64(cellValue.GetNumber());
}
else if (propertyType == typeof(bool) || propertyType == typeof(bool?))
{
val = Convert.ToBoolean(cellValue.GetBoolean());
}
else if (propertyType == typeof(DateTime) || propertyType == typeof(DateTime?))
{
val = Convert.ToDateTime(cellValue.GetDateTime());
}
else
{
val = cellValue.ToString();
}

return Convert.ChangeType(val, Nullable.GetUnderlyingType(propertyType) ?? propertyType);
}
}
15 changes: 15 additions & 0 deletions src/api/framework/Infrastructure/DataIO/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using FSH.Framework.Core.DataIO;
using Microsoft.Extensions.DependencyInjection;

namespace FSH.Framework.Infrastructure.DataIO;

internal static class Extensions
{
internal static IServiceCollection ConfigureDataImportExport(this IServiceCollection services)
{
services.AddTransient<IDataExport, DataExport>();
services.AddTransient<IDataImport, DataImport>();

return services;
}
}
2 changes: 2 additions & 0 deletions src/api/framework/Infrastructure/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using FSH.Framework.Infrastructure.Behaviours;
using FSH.Framework.Infrastructure.Caching;
using FSH.Framework.Infrastructure.Cors;
using FSH.Framework.Infrastructure.DataIO;
using FSH.Framework.Infrastructure.Exceptions;
using FSH.Framework.Infrastructure.Identity;
using FSH.Framework.Infrastructure.Jobs;
Expand Down Expand Up @@ -49,6 +50,7 @@ public static WebApplicationBuilder ConfigureFshFramework(this WebApplicationBui
builder.Services.AddExceptionHandler<CustomExceptionHandler>();
builder.Services.AddProblemDetails();
builder.Services.AddHealthChecks();
builder.Services.ConfigureDataImportExport();
builder.Services.AddOptions<OriginOptions>().BindConfiguration(nameof(OriginOptions));

// Define module assemblies
Expand Down
2 changes: 2 additions & 0 deletions src/api/framework/Infrastructure/Infrastructure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
<ItemGroup>
<PackageReference Include="Asp.Versioning.Http" />
<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" />
<PackageReference Include="ClosedXML" />
<PackageReference Include="ClosedXML.Report" />
<PackageReference Include="Finbuckle.MultiTenant" />
<PackageReference Include="Finbuckle.MultiTenant.AspNetCore" />
<PackageReference Include="Finbuckle.MultiTenant.EntityFrameworkCore" />
Expand Down
Loading