Intro
In this time, I try creating and downloading files in these conditions.
- The appliction searchs book data.
- It creates spreadsheets from the result.
- It creates a spreadsheet per genres.
- It will return as a spreadsheet file if it creates only one file.
- It will return as a ZIP archive if it creates two or more files.
- If it doesn't create any files, an alert will be shown in client-side.
Environments
- .NET ver.5.0.101
- ClosedXML ver.0.95.4
- Newtonsoft.Json ver.12.0.3
package.json
{
"dependencies": {
"ts-loader": "^8.0.14",
"tsc": "^1.20150623.0",
"typescript": "^4.1.3",
"webpack": "^5.14.0",
"webpack-cli": "^4.3.1"
}
}
Search book data
In this samples, I just create some sample data and search them.
Book.cs
using System;
namespace FileDownloadSample.Books.Models
{
public record Book(int Id, string Name, Genre Genre,
DateTime? PublishDate, decimal? Price);
}
Genre.cs
namespace FileDownloadSample.Books.Models
{
public record Genre(int Id, string Name);
}
IBooks.cs
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using FileDownloadSample.Books.Models;
namespace FileDownloadSample.Books
{
public interface IBooks
{
Task<List<Book>> GetBooksAsync(string? genre,
DateTime? publishDateFrom, DateTime? publishDateTo);
}
}
Books.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using FileDownloadSample.Books.Models;
namespace FileDownloadSample.Books
{
public class Books: IBooks
{
private readonly List<Book> allBooks;
public Books()
{
allBooks = new List<Book>();
SetBooks();
}
public async Task<List<Book>> GetBooksAsync(string? genre,
DateTime? publishDateFrom, DateTime? publishDateTo)
{
// TODO: Implement
}
private void SetBooks()
{
var programming = new Genre(0, "Programming");
var manga = new Genre(1, "Manga");
var novels = new Genre(2, "Novels");
allBooks.Add(new Book(0, "Programming C# 8.0", programming,
DateTime.Parse("2019-11-26"), 6851m));
allBooks.Add(new Book(1, "Programming C# 5.0", programming,
DateTime.Parse("2012-11-13"), 2216m));
allBooks.Add(new Book(2, "SHY", manga,
DateTime.Parse("2019-12-6"), 237m));
}
}
}
Dynamically change search criteria
If "genre" value isn't null or empty, I want to add it for search criteria.
I can add multiple where clauses.
Books.cs
...
public class Books: IBooks
{
...
public async Task<List<Book>> GetBooksAsync(string? genre,
DateTime? publishDateFrom, DateTime? publishDateTo)
{
if(string.IsNullOrEmpty(genre) &&
publishDateFrom == null &&
publishDateTo == null)
{
return allBooks;
}
var queryable = allBooks.AsQueryable();
if(string.IsNullOrEmpty(genre) == false)
{
queryable = queryable.Where(b => b.Genre.Name == genre);
}
if(publishDateFrom != null)
{
queryable = queryable.Where(b => b.PublishDate != null &&
b.PublishDate >= publishDateFrom);
}
if(publishDateTo != null)
{
queryable = queryable.Where(b => b.PublishDate != null &&
b.PublishDate <= publishDateTo);
}
return await Task.FromResult(queryable.ToList());
}
...
I also can use "Expressions".
Books.cs
...
public class Books: IBooks
{
...
public async Task<List<Book>> GetBooksAsync(string? genre,
DateTime? publishDateFrom, DateTime? publishDateTo)
{
if(string.IsNullOrEmpty(genre) &&
publishDateFrom == null &&
publishDateTo == null)
{
return allBooks;
}
return await Task.FromResult(
allBooks.AsQueryable()
.Where(GetWhereClauses(genre, publishDateFrom, publishDateTo))
.ToList());
}
private Expression<Func<Book, bool>> GetWhereClauses(string? genre,
DateTime? publishDateFrom, DateTime? publishDateTo)
{
// This parameters are required inside Lambda expreassions
var param = Expression.Parameter(typeof(Book), "b");
Expression? baseExpr = null;
if(string.IsNullOrEmpty(genre) == false)
{
Expression<Func<Book, bool>> genreExpr = b => b.Genre.Name == genre;
baseExpr = Expression.Invoke(genreExpr, param);
}
if(publishDateFrom != null)
{
Expression<Func<Book, bool>> dateFromExpr = b => b.PublishDate != null &&
b.PublishDate >= (DateTime)publishDateFrom;
if(baseExpr == null)
{
baseExpr = Expression.Invoke(dateFromExpr, param);
}
else
{
baseExpr = Expression.AndAlso(baseExpr,
Expression.Invoke(dateFromExpr, param));
}
}
if(publishDateTo != null)
{
Expression<Func<Book, bool>> dateToExpr = b => b.PublishDate != null &&
b.PublishDate <= (DateTime)publishDateTo;
if(baseExpr == null)
{
baseExpr = Expression.Invoke(dateToExpr, param);
}
else
{
baseExpr = Expression.AndAlso(baseExpr, dateToExpr.Body);
}
}
return Expression.Lambda<Func<Book, bool>>(baseExpr!, true, param);
}
...
- System.Linq.Expressions Namespace | Microsoft Docs
- Expression Class (System.Linq.Expressions) | Microsoft Docs
- How do I combine LINQ expressions into one? - Stack Overflow
- c# - Combining two expressions (Expression>) - Stack Overflow
Create spreadsheets and ZIP archives
From the search results, I want to create spreadsheets and ZIP archives.
Create spreadsheets
DownloadFile.cs
namespace FileDownloadSample.Books
{
public record DownloadFile(string Name, byte[] File, string ContentType);
}
ISpreadsheetGenerator.cs
using System.Collections.Generic;
using FileDownloadSample.Books.Models;
namespace FileDownloadSample.Books
{
public interface ISpreadsheetGenerator
{
DownloadFile Generate(List<Book> books);
}
}
SpreadsheetGenerator.cs
using System.IO.Compression;
using System.Text;
using System.IO;
using System.Linq;
using System.Collections.Generic;
using FileDownloadSample.Books.Models;
using ClosedXML.Excel;
using Newtonsoft.Json;
using FileDownloadSample.OperationResults;
using Newtonsoft.Json.Serialization;
namespace FileDownloadSample.Books
{
public class SpreadsheetGenerator: ISpreadsheetGenerator
{
private record SavedFile(string Name, byte[] FileData);
public DownloadFile Generate(List<Book> books)
{
List<SavedFile> files = CreateSpreadsheet(books);
// TODO: create return values.
}
private List<SavedFile> CreateSpreadsheet(List<Book> books)
{
var results = new List<SavedFile>();
// Separate by genre
var genres = books.Select(b => b.Genre.Name)
.Distinct()
.ToArray();
foreach(var genre in genres)
{
using(var book = new XLWorkbook())
{
var sheet = book.Worksheets.Add(genre);
sheet.Cell(1, 1).Value = "Genre";
sheet.Cell(1, 2).Value = genre;
var titleId = sheet.Cell(3, 1);
var titleName = sheet.Cell(3, 2);
var titlePublishDate = sheet.Cell(3, 3);
var titlePrice = sheet.Cell(3, 4);
titleId.Value = "ID";
titleId.Style.Fill.BackgroundColor = XLColor.LightBlue;
titleName.Value = "Name";
titleName.Style.Fill.BackgroundColor = XLColor.LightBlue;
titlePublishDate.Value = "PublishDate";
titlePublishDate.Style.Fill.BackgroundColor = XLColor.LightBlue;
titlePrice.Value = "Price";
titlePrice.Style.Fill.BackgroundColor = XLColor.LightBlue;
var row = 4;
foreach(var b in books.Where(b => b.Genre.Name == genre))
{
sheet.Cell(row, 1).Value = b.Id;
sheet.Cell(row, 2).Value = b.Name;
sheet.Cell(row, 3).Value = b.PublishDate?.ToString("yyyy-MM-dd") ?? "";
sheet.Cell(row, 4).Value = b.Price?.ToString() ?? "";
row += 1;
}
byte[] savedFileData;
using(var stream = new MemoryStream())
{
book.SaveAs(stream);
savedFileData = stream.ToArray();
}
results.Add(new SavedFile(genre, savedFileData));
}
}
return results;
}
}
}
Create ZIP data
"ZipArchive" class can create ZIP archives.
SpreadsheetGenerator.cs
...
public class SpreadsheetGenerator: ISpreadsheetGenerator
{
private record SavedFile(string Name, byte[] FileData);
public DownloadFile Generate(List<Book> books)
{
List<SavedFile> files = CreateSpreadsheet(books);
byte[] zipArchive = CreateZip(files);
// TODO: create return values.
}
...
private byte[] CreateZip(List<SavedFile> files)
{
using(MemoryStream stream = new MemoryStream())
{
using(ZipArchive archive = new ZipArchive(stream, ZipArchiveMode.Create, true))
{
foreach(SavedFile file in files)
{
ZipArchiveEntry entry = archive.CreateEntry($"{file.Name}.xlsx");
using(Stream entryStream = entry.Open())
{
entryStream.Write(file.FileData, 0, file.FileData.Length);
}
}
// Don't do them here
// stream.Seek(0, SeekOrigin.Begin);
// return stream.ToArray();
}
stream.Seek(0, SeekOrigin.Begin);
return stream.ToArray();
}
}
}
}
One important thing is I can't get the byte data of ZipArchive before disposing the ZipArchive instance.
If I do that, I only can get a broken data because the entries haven't be saved yet.
Return file data
I could create spreadsheets and ZIP archives.
Now I try returning them.
SpreadsheetGenerator.cs
...
public class SpreadsheetGenerator: ISpreadsheetGenerator
{
...
public DownloadFile Generate(List<Book> books)
{
var fileData = CreateSpreadsheet(books);
if(fileData.Count <= 0)
{
// use lower camel case
var serializerSettings = new JsonSerializerSettings();
serializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
var noItems = JsonConvert.SerializeObject(new SearchResult(true, "No items"), serializerSettings);
return new DownloadFile("SearchResult", Encoding.UTF8.GetBytes(noItems), "application/json");
}
if(fileData.Count == 1)
{
return new DownloadFile(fileData[0].Name, fileData[0].FileData, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
}
return new DownloadFile("SearchResult", CreateZip(fileData), "application/zip");
}
...
IBookFileGenerator.cs
using System;
using System.Threading.Tasks;
namespace FileDownloadSample.Books
{
public interface IBookFileGenerator
{
Task<DownloadFile> GetBookAsync(string? genre,
DateTime? publishDateFrom, DateTime? publishDateTo);
}
}
BookFileGenerator.cs
using System;
using System.Threading.Tasks;
namespace FileDownloadSample.Books
{
public class BookFileGenerator: IBookFileGenerator
{
private readonly IBooks books;
private readonly ISpreadsheetGenerator spreadsheets;
public BookFileGenerator(IBooks books,
ISpreadsheetGenerator spreadsheets)
{
this.books = books;
this.spreadsheets = spreadsheets;
}
public async Task<DownloadFile> GetBookAsync(string? genre,
DateTime? publishDateFrom, DateTime? publishDateTo)
{
return spreadsheets.Generate(
await books.GetBooksAsync(genre, publishDateFrom, publishDateTo));
}
}
}
HomeController.cs
using System;
using System.Threading.Tasks;
using FileDownloadSample.Books;
using Microsoft.AspNetCore.Mvc;
namespace FileDownloadSample.Controllers
{
public class HomeConroller: Controller
{
private readonly IBookFileGenerator books;
public HomeConroller(IBookFileGenerator books)
{
this.books = books;
}
[Route("")]
public IActionResult Index()
{
return View("Views/Index.cshtml");
}
[Route("Download")]
public async Task<IActionResult> Download(string? genre, string? dateFrom, string? dateTo)
{
DateTime? publishDateFrom = ConvertToDateTime(dateFrom);
DateTime? publishDateTo = ConvertToDateTime(dateTo);
DownloadFile result = await books.GetBookAsync(genre, publishDateFrom, publishDateTo);
return File(result.File, result.ContentType, result.Name);
}
private DateTime? ConvertToDateTime(string? dateText)
{
if(string.IsNullOrEmpty(dateText) == false &&
DateTime.TryParse(dateText, out var d))
{
return d;
}
return null;
}
}
}
I also can create file data instead of "File(result.File, result.ContentType, result.Name)" by "FileContentResult".
return new FileContentResult(result.File, result.ContentType)
{
FileDownloadName = result.Name
};
Download files (client-side)
Because I must show an alert if I can't find any book data, I can't download result files directly.
So I use "fetch" to get result first.
Index.cshtml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>FileDownloadSample</title>
</head>
<body>
<input type="text" id="search_genre">
<button onclick="Page.downloadFile()">Download</button>
<a id="download_target" href="#"></a>
<script src="js/mainPage.js"></script>
</body>
</html>
searchResult.ts
export type SearchResult = {
failed: boolean,
errorMessage: string
};
main.page.ts
import { SearchResult } from "./searchResult";
export async function downloadFile() {
const genreElement = document.getElementById('search_genre') as HTMLInputElement;
await downloadAsync(genreElement.value);
}
async function downloadAsync(genre: string|null): Promise<SearchResult>
{
let result: SearchResult = {
failed: true,
errorMessage: 'No data',
};
await fetch(`Download?genre=${genre}`,
{
method: 'GET',
mode: 'cors'
})
.then(response => {
// TODO: implement
})
.catch(error => console.error(error));
return result;
}
Handle result
I can get Content-Type from the response headers.
So I can separate operations by it.
main.page.ts
...
async function downloadAsync(genre: string|null): Promise<SearchResult>
{
let result: SearchResult = {
failed: true,
errorMessage: 'No data',
};
await fetch(`Download?genre=${genre}`,
{
method: 'GET',
mode: 'cors'
})
.then(async response => await handleResponse(response))
.catch(error => console.error(error));
return result;
}
async function handleResponse(response: Response) {
if(response.ok === false){
alert(response.statusText);
return;
}
switch(response.headers.get("Content-Type")){
case 'application/json':
await handleResultAsJson(response);
break;
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
case 'application/zip':
await handleResultAsDownloadFile(response);
default:
alert('invalid data');
break;
}
}
async function handleResultAsJson(response: Response){
const json = await response.json();
const result = JSON.parse(JSON.stringify(json)) as SearchResult;
if(result == null) {
alert('No results');
return;
}
if(result.failed === true){
alert(result.errorMessage);
return;
}
}
async function handleResultAsDownloadFile(response: Response){
const blob = await response.blob();
const target = document.getElementById('download_target') as HTMLAnchorElement;
target.href = window.URL.createObjectURL(blob);
target.click();
}
No file names
Although I can download files.
But I have still had a problem.
The files don't have their names.
So I add a response header item and set the file name.
HomeController.cs
...
public class HomeConroller: Controller
{
...
[Route("Download")]
public async Task<IActionResult> Download(string? genre, string? dateFrom, string? dateTo)
{
var publishDateFrom = ConvertToDateTime(dateFrom);
var publishDateTo = ConvertToDateTime(dateTo);
var result = await books.GetBookAsync(genre, publishDateFrom, publishDateTo);
Response.Headers["File-Name"] = result.Name;
return File(result.File, result.ContentType, result.Name);
}
...
main.page.ts
...
async function handleResultAsDownloadFile(response: Response){
const blob = await response.blob();
const target = document.getElementById('download_target') as HTMLAnchorElement;
target.href = window.URL.createObjectURL(blob);
const fileName = response.headers.get('File-Name');
target.download = (fileName == null)? 'sample': fileName;
target.click();
}
Top comments (0)