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

CSV Download #128

Merged
merged 2 commits into from
Dec 14, 2023
Merged
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
6 changes: 6 additions & 0 deletions Src/Application/Common/Interfaces/ICsvBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Northwind.Application.Common.Interfaces;

public interface ICsvBuilder
{
Task<byte[]> GetCsvBytes<T>(IEnumerable<T> records);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using AutoMapper;
using Northwind.Application.Common.Mappings;
using Northwind.Domain.Customers;

namespace Northwind.Application.Customers.Queries.GetCustomersCsv;

public class CustomerCsvLookupDto : IMapFrom<Customer>
{
public required string Id { get; init; }
public required string Name { get; init; }

public void Mapping(Profile profile)
{
profile.CreateMap<Customer, CustomerCsvLookupDto>()
.ForMember(d => d.Id, opt => opt.MapFrom(s => s.Id.Value))
.ForMember(d => d.Name, opt => opt.MapFrom(s => s.CompanyName));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Northwind.Application.Customers.Queries.GetCustomersCsv;

public class CustomersCsvVm
{
public required byte[] Data { get; set; }
public required string FileName { get; set; }
public readonly string ContentType = "text/csv";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using AutoMapper;
using AutoMapper.QueryableExtensions;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Northwind.Application.Common.Interfaces;

namespace Northwind.Application.Customers.Queries.GetCustomersCsv;

public sealed record GetCustomersCsvQuery : IRequest<CustomersCsvVm>;

public sealed class GetCustomersCsvQueryHandler(
INorthwindDbContext context,
IMapper mapper,
IDateTime dateTime,
ICsvBuilder csvBuilder) : IRequestHandler<GetCustomersCsvQuery, CustomersCsvVm>
{
public async Task<CustomersCsvVm> Handle(GetCustomersCsvQuery request, CancellationToken cancellationToken)
{
IEnumerable<CustomerCsvLookupDto> customers = await context.Customers
.ProjectTo<CustomerCsvLookupDto>(mapper.ConfigurationProvider)
.ToListAsync(cancellationToken);

byte[] data = await csvBuilder.GetCsvBytes(customers);

return new CustomersCsvVm
{
Data = data,
FileName = $"{dateTime.Now:yyyy-MM-dd}-Products.csv",
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@

namespace Northwind.Application.Products.Queries.GetProductsFile;

public record GetProductsFileQuery : IRequest<ProductsFileVm>;
public sealed record GetProductsFileQuery : IRequest<ProductsFileVm>;

// ReSharper disable once UnusedType.Global
public class GetProductsFileQueryHandler(INorthwindDbContext context, ICsvFileBuilder fileBuilder, IMapper mapper,
public sealed class GetProductsFileQueryHandler(INorthwindDbContext context, ICsvBuilder fileBuilder, IMapper mapper,
IDateTime dateTime)
: IRequestHandler<GetProductsFileQuery, ProductsFileVm>
{
Expand All @@ -22,7 +21,7 @@ public async Task<ProductsFileVm> Handle(GetProductsFileQuery request, Cancellat
.ProjectTo<ProductRecordDto>(mapper.ConfigurationProvider)
.ToListAsync(cancellationToken);

var fileContent = fileBuilder.BuildProductsFile(records);
var fileContent = await fileBuilder.GetCsvBytes(records);

var vm = new ProductsFileVm
{
Expand Down

This file was deleted.

2 changes: 1 addition & 1 deletion Src/Infrastructure/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi

private static void AddFiles(IServiceCollection services)
{
services.AddTransient<ICsvFileBuilder, CsvFileBuilder>();
services.AddTransient<ICsvBuilder, CsvBuilder>();
}

private static void AddServices(IServiceCollection services)
Expand Down
21 changes: 21 additions & 0 deletions Src/Infrastructure/Files/CsvBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using CsvHelper;
using Northwind.Application.Common.Interfaces;
using System.Globalization;

namespace Northwind.Infrastructure.Files;

public class CsvBuilder : ICsvBuilder
{
public Task<byte[]> GetCsvBytes<T>(IEnumerable<T> records)
{
using var stream = new MemoryStream();
using var streamWriter = new StreamWriter(stream);
using (var csvWriter = new CsvWriter(streamWriter, CultureInfo.InvariantCulture))
{
csvWriter.Context.ConfigureMappingProvider<T>();
csvWriter.WriteRecords(records);
}

return Task.FromResult(stream.ToArray());
}
}
21 changes: 0 additions & 21 deletions Src/Infrastructure/Files/CsvFileBuilder.cs

This file was deleted.

20 changes: 20 additions & 0 deletions Src/Infrastructure/Files/CsvMapProviders.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using CsvHelper;
using Northwind.Application.Customers.Queries.GetCustomersCsv;
using Northwind.Application.Products.Queries.GetProductsFile;

namespace Northwind.Infrastructure.Files;

public static class CsvMapProviders
{
private static readonly IReadOnlyDictionary<Type, Action<CsvContext>> TypeConfiguration = new Dictionary<Type, Action<CsvContext>>
{
{ typeof(ProductRecordDto), context => context.RegisterClassMap<ProductFileRecordMap>() },
{ typeof(CustomerCsvLookupDto), context => context.RegisterClassMap<CustomerFileRecordMap>() },
};

public static void ConfigureMappingProvider<T>(
this CsvContext csvContext)
{
TypeConfiguration.GetValueOrDefault(typeof(T))?.Invoke(csvContext);
}
}
13 changes: 13 additions & 0 deletions Src/Infrastructure/Files/CustomerFileRecordMap.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using CsvHelper.Configuration;
using Northwind.Application.Customers.Queries.GetCustomersCsv;
using System.Globalization;

namespace Northwind.Infrastructure.Files;

public sealed class CustomerFileRecordMap : ClassMap<CustomerCsvLookupDto>
{
public CustomerFileRecordMap()
{
AutoMap(CultureInfo.InvariantCulture);
}
}
40 changes: 40 additions & 0 deletions Src/WebUI/ClientApp/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Src/WebUI/ClientApp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"angular-feather": "^6.0.2",
"aspnet-prerendering": "^3.0.1",
"bootstrap": "^5.2.3",
"file-saver": "^2.0.5",
"jquery": "^3.6.4",
"ngx-bootstrap": "^5.1.1",
"popper.js": "^1.16.0",
Expand All @@ -40,6 +41,7 @@
"@angular-devkit/build-angular": "^15.2.7",
"@angular/cli": "^15.2.7",
"@angular/compiler-cli": "^15.2.8",
"@types/file-saver": "^2.0.7",
"@types/jasmine": "~4.3.1",
"@types/jasminewd2": "~2.0.10",
"@types/node": "^18.16.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
<h1 class="h2">Customers</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group mr-2">
<button class="btn btn-sm btn-outline-secondary">Share</button>
<button class="btn btn-sm btn-outline-secondary">Export</button>
<button class="btn btn-sm btn-outline-secondary"
(click)="exportAsCsv()">
Export
</button>
</div>
</div>
</div>
Expand Down
29 changes: 19 additions & 10 deletions Src/WebUI/ClientApp/src/app/customers/customers.component.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,38 @@
import { Component } from '@angular/core';
import { Client, CustomerDetailVm, CustomersListVm } from '../northwind-traders-api';
import { Component, inject, OnInit } from '@angular/core';
import { Client, CustomersListVm } from '../northwind-traders-api';
import { CustomerDetailComponent } from '../customer-detail/customer-detail.component';
import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
import { BsModalService } from 'ngx-bootstrap/modal';
import { saveAs } from 'file-saver';

@Component({
selector: 'app-customers',
templateUrl: './customers.component.html'
})
export class CustomersComponent {
export class CustomersComponent implements OnInit {
private client = inject(Client);
private modalService =inject(BsModalService);

public vm: CustomersListVm = new CustomersListVm();
private bsModalRef: BsModalRef;

constructor(private client: Client, private modalService: BsModalService) {
client.getCustomersList().subscribe(result => {
ngOnInit(): void {
this.client.getCustomersList().subscribe(result => {
this.vm = result;
}, error => console.error(error));
});
}

public customerDetail(id: string) {
this.client.getCustomer(id).subscribe(result => {
const initialState = {
customer: result
};
this.bsModalRef = this.modalService.show(CustomerDetailComponent, {initialState});
}, error => console.error(error));
this.modalService.show(CustomerDetailComponent, {initialState});
});
}

protected exportAsCsv() {
this.client.getCustomersCsv().subscribe(result => {
const blob = new Blob([result.data], { type: result.headers.contentType });
saveAs(blob, result.fileName);
});
}
}
Loading