diff --git a/README.md b/README.md index 3b9ac65..558d1d7 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ Column names in Excel Export can be configured using the below attributes To customize the Excel Column display in your report, use the following attribute * `[IncludeInReport]` * `[NestedIncludeInReport]` +* `[IncludeAllInReportAttribute]` +* `[ExcludeFromReportAttribute]` And here are the options, @@ -145,6 +147,135 @@ public async Task OnGetCSVAsync() } ``` +## Include All Modal Columns In Report using IncludeAllInReportAttribute + +```c# +[IncludeAllInReport] +public class DemoExcel +{ + public int Id { get; set; } + + public string Name { get; set; } + + public string Position { get; set; } + + public string Offices { get; set; } +} +``` + +## Exclude some Columns from Report using ExcludeFromReportAttribute + +for example here offices column is excluded +```c# +[IncludeAllInReport] +public class DemoExcel +{ + public int Id { get; set; } + + public string Name { get; set; } + + public string Position { get; set; } + + [ExcludeFromReport] + public string Offices { get; set; } +} +``` + + + +## Pass columns definitions as a parameter to the constructor + +For example if you have this Modal class +```c# +public class Employee +{ + public string EmployeeName { get; set; } + public DateTime? StartDate { get; set; } + public long? BasicSalary { get; set; } +} +``` + +## You have two ways to pass columns definitions + +## 1 - Pass columns definitions as tuple params + +Excel +```c# +public async Task OnGetExcelAsync() +{ + // Get you IEnumerable data + var results = await _demoService.GetDataAsync(); + return new ExcelResult( + data: results, + sheetName: "Fingers10", + fileName: "Fingers10", + ("EmployeeName", "Employee Name", 1), // prop name, label, order + ("StartDate", "Start Date", 2), + ("BasicSalary", "Basic Salary", 3) + ); +} +``` + +* CSV +```c# +public async Task OnGetExcelAsync() +{ + // Get you IEnumerable data + var results = await _demoService.GetDataAsync(); + return new CSVResult( + data: results, + fileName: "Fingers10", + ("EmployeeName", "Employee Name", 1), // prop name, label, order + ("StartDate", "Start Date", 2), + ("BasicSalary", "Basic Salary", 3) + ); +} +``` + +## 2 - Pass columns definitions as List of ExcelColumnDefinition + +## in this way the columns are ordered based on the order of the passed list + +Excel +```c# +public async Task OnGetExcelAsync() +{ + // Get you IEnumerable data + var results = await _demoService.GetDataAsync(); + return new ExcelResult( + data: results, + sheetName: "Fingers10", + fileName: "Fingers10", + new List() + { + new ("EmployeeName", "Employee Name"), + new ("StartDate", "Start Date"), + new ("BasicSalary", "Basic Salary") + } + ); +} +``` + +* CSV +```c# +public async Task OnGetExcelAsync() +{ + // Get you IEnumerable data + var results = await _demoService.GetDataAsync(); + return new CSVResult( + data: results, + fileName: "Fingers10", + new List() + { + new ("EmployeeName", "Employee Name"), + new ("StartDate", "Start Date"), + new ("BasicSalary", "Basic Salary") + } + ); +} +``` + + # Target Platform * .Net Standard 2.0 diff --git a/src/ApplicationCore/ActionResults/CSVResult.cs b/src/ApplicationCore/ActionResults/CSVResult.cs index 7ce7d7d..65829f4 100644 --- a/src/ApplicationCore/ActionResults/CSVResult.cs +++ b/src/ApplicationCore/ActionResults/CSVResult.cs @@ -1,7 +1,7 @@ using Fingers10.ExcelExport.Extensions; +using Fingers10.ExcelExport.Models; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -10,6 +10,9 @@ namespace Fingers10.ExcelExport.ActionResults public class CSVResult : IActionResult where T : class { private readonly IEnumerable _data; + public string FileName { get; } + + private List Columns { get; set; } = new List(); public CSVResult(IEnumerable data, string fileName) { @@ -17,23 +20,32 @@ public CSVResult(IEnumerable data, string fileName) FileName = fileName; } - public string FileName { get; } + public CSVResult(IEnumerable data, string fileName, params (string name, string label, int order)[] definitions) + { + _data = data; + FileName = fileName; + + Columns.SetDefinitions(definitions); + } + + public CSVResult(IEnumerable data, string fileName, List definitions) + { + _data = data; + FileName = fileName; + + Columns.SetDefinitions(definitions); + } public async Task ExecuteResultAsync(ActionContext context) { - try - { - var csvBytes = await _data.GenerateCSVForDataAsync(); - WriteExcelFileAsync(context.HttpContext, csvBytes); - - } - catch (Exception e) - { - Console.WriteLine(e); - - var errorBytes = await new List().GenerateCSVForDataAsync(); - WriteExcelFileAsync(context.HttpContext, errorBytes); - } + byte[] csvBytes; + + if (Columns != null && Columns.Count > 0) + csvBytes = await _data.GenerateCSVForDataAsync(Columns); + else + csvBytes = await _data.GenerateCSVForDataAsync(); + + WriteExcelFileAsync(context.HttpContext, csvBytes); } private async void WriteExcelFileAsync(HttpContext context, byte[] bytes) diff --git a/src/ApplicationCore/ActionResults/ExcelResult.cs b/src/ApplicationCore/ActionResults/ExcelResult.cs index b7b5407..f35a374 100644 --- a/src/ApplicationCore/ActionResults/ExcelResult.cs +++ b/src/ApplicationCore/ActionResults/ExcelResult.cs @@ -1,7 +1,7 @@ using Fingers10.ExcelExport.Extensions; +using Fingers10.ExcelExport.Models; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; -using System; using System.Collections.Generic; using System.Threading.Tasks; @@ -10,6 +10,9 @@ namespace Fingers10.ExcelExport.ActionResults public class ExcelResult : IActionResult where T : class { private readonly IEnumerable _data; + public string SheetName { get; } + public string FileName { get; } + private List Columns { get; set; } = new List(); public ExcelResult(IEnumerable data, string sheetName, string fileName) { @@ -18,24 +21,34 @@ public ExcelResult(IEnumerable data, string sheetName, string fileName) FileName = fileName; } - public string SheetName { get; } - public string FileName { get; } + public ExcelResult(IEnumerable data, string sheetName, string fileName, List definitions) + { + _data = data; + SheetName = sheetName; + FileName = fileName; + + Columns.SetDefinitions(definitions); + } + + public ExcelResult(IEnumerable data, string sheetName, string fileName, params (string name, string label, int order)[] definitions) + { + _data = data; + SheetName = sheetName; + FileName = fileName; + + Columns.SetDefinitions(definitions); + } public async Task ExecuteResultAsync(ActionContext context) { - try - { - var excelBytes = await _data.GenerateExcelForDataTableAsync(SheetName); - WriteExcelFileAsync(context.HttpContext, excelBytes); - - } - catch (Exception e) - { - Console.WriteLine(e); - - var errorBytes = await new List().GenerateExcelForDataTableAsync(SheetName); - WriteExcelFileAsync(context.HttpContext, errorBytes); - } + byte[] excelBytes; + + if (Columns != null && Columns.Count > 0) + excelBytes = await _data.GenerateExcelForDataTableAsync(SheetName, Columns); + else + excelBytes = await _data.GenerateExcelForDataTableAsync(SheetName); + + WriteExcelFileAsync(context.HttpContext, excelBytes); } private async void WriteExcelFileAsync(HttpContext context, byte[] bytes) diff --git a/src/ApplicationCore/ApplicationCore.csproj b/src/ApplicationCore/ApplicationCore.csproj index 151adf4..02beda3 100644 --- a/src/ApplicationCore/ApplicationCore.csproj +++ b/src/ApplicationCore/ApplicationCore.csproj @@ -10,16 +10,16 @@ Classes to generate Excel Report in ASP.NET Excel Export, CSV Export https://github.com/fingers10/ExcelExport - https://github.com/fingers10/ExcelExport + https://github.com/haithambasim/ExcelExport Public - 1. Added support for CSV Export -2. Updated Nuget Dependencies -3. Removed Package Icon + LICENSE.md Copyright (c) 2019 Abdul Rahman (@fingers10) - 3.0.0 - 3.0.0.0 + 1.0.3 + 0.0.0.0 + HBH.Fingers10.ExcelExport + HBH.Fingers10.ExcelExport diff --git a/src/ApplicationCore/Attributes/ExcludeFromReportAttribute.cs b/src/ApplicationCore/Attributes/ExcludeFromReportAttribute.cs new file mode 100644 index 0000000..4fe87e1 --- /dev/null +++ b/src/ApplicationCore/Attributes/ExcludeFromReportAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace Fingers10.ExcelExport.Attributes +{ + [AttributeUsage(AttributeTargets.Property)] + public class ExcludeFromReportAttribute : Attribute + { + } +} diff --git a/src/ApplicationCore/Attributes/IncludeAllInReportAttribute.cs b/src/ApplicationCore/Attributes/IncludeAllInReportAttribute.cs new file mode 100644 index 0000000..374a2f5 --- /dev/null +++ b/src/ApplicationCore/Attributes/IncludeAllInReportAttribute.cs @@ -0,0 +1,9 @@ +using System; + +namespace Fingers10.ExcelExport.Attributes +{ + [AttributeUsage(AttributeTargets.Class)] + public class IncludeAllInReportAttribute : Attribute + { + } +} diff --git a/src/ApplicationCore/Extensions/ReportExtensions.cs b/src/ApplicationCore/Extensions/ReportExtensions.cs index df9dc0a..a202c01 100644 --- a/src/ApplicationCore/Extensions/ReportExtensions.cs +++ b/src/ApplicationCore/Extensions/ReportExtensions.cs @@ -16,9 +16,17 @@ public static class ReportExtensions { public static async Task ToDataTableAsync(this IEnumerable data, string name) { - var columns = GetColumnsFromModel(typeof(T)).ToDictionary(x => x.Name, x => x.Value).OrderBy(x => x.Value.Order); + var columns = typeof(T).GetCustomAttributes().Any() + ? GetAllColumnsFromModel(typeof(T)).ToDictionary(x => x.Name, x => x.Value).OrderBy(x => x.Value.Order).ToList() + : GetColumnsFromModel(typeof(T)).ToDictionary(x => x.Name, x => x.Value).OrderBy(x => x.Value.Order).ToList(); + var table = new DataTable(name ?? typeof(T).Name); + if (columns.Count == 0) + { + return table; + } + await Task.Run(() => { foreach (var column in columns) @@ -41,10 +49,69 @@ await Task.Run(() => return table; } + public static async Task ToDataTableAsync(this IEnumerable data, string name, List cols = null) + { + var table = new DataTable(name ?? typeof(T).Name); + + if (cols == null) + return table; + + var properties = typeof(T).GetProperties().ToList(); + + if (cols.Count == 0) + return table; + + await Task.Run(() => + { + foreach (var column in cols) + { + var prop = properties.FirstOrDefault(x => x.Name == column.Name); + table.Columns.Add(column.Label, Nullable.GetUnderlyingType(prop.GetPropertyDescriptor().PropertyType) ?? prop.GetPropertyDescriptor().PropertyType); + } + + foreach (T item in data) + { + var row = table.NewRow(); + + foreach (var prop in cols) + { + row[prop.Label] = PropertyExtensions.GetPropertyValue(item, prop.Name) ?? DBNull.Value; + } + + table.Rows.Add(row); + } + }); + + return table; + } + + public static IEnumerable GetAllColumnsFromModel(Type parentClass, string parentName = null) + { + var properties = parentClass.GetProperties() + .Where(p => !p.GetCustomAttributes().Any()); + + var order = 0; + + foreach (var prop in properties) + { + order++; + yield return new Column + { + Name = prop.GetPropertyDisplayName(), + Value = new ColumnValue + { + Order = order, + Path = string.IsNullOrWhiteSpace(parentName) ? prop.Name : $"{parentName}.{prop.Name}", + PropertyDescriptor = prop.GetPropertyDescriptor() + } + }; + } + } + public static IEnumerable GetColumnsFromModel(Type parentClass, string parentName = null) { var complexReportProperties = parentClass.GetProperties() - .Where(p => p.GetCustomAttributes().Any()); + .Where(p => p.GetCustomAttributes().Any()).ToList(); var properties = parentClass.GetProperties() .Where(p => p.GetCustomAttributes().Any()); @@ -70,7 +137,6 @@ public static IEnumerable GetColumnsFromModel(Type parentClass, string p foreach (var parentProperty in complexReportProperties) { var parentType = parentProperty.PropertyType; - var parentAttribute = parentProperty.GetCustomAttribute(); var complexProperties = GetColumnsFromModel(parentType, string.IsNullOrWhiteSpace(parentName) ? parentProperty.Name : $"{parentName}.{parentProperty.Name}"); @@ -82,19 +148,21 @@ public static IEnumerable GetColumnsFromModel(Type parentClass, string p } } - //public static DataTable ToFastDataTable(this IEnumerable data, string name) - //{ - // //For Excel reference - // //https://stackoverflow.com/questions/564366/convert-generic-list-enumerable-to-datatable - // // To restrict order or specific property - // //ObjectReader.Create(data, "Id", "Name", "Description") - // using (var reader = ObjectReader.Create(data)) - // { - // var table = new DataTable(name ?? typeof(T).Name); - // table.Load(reader); - // return table; - // } - //} + public static async Task GenerateExcelForDataTableAsync(this IEnumerable data, string name, List columns = null) + { + var table = await data.ToDataTableAsync(name, columns); + + using (var wb = new XLWorkbook(XLEventTracking.Disabled)) + { + wb.Worksheets.Add(table).ColumnsUsed().AdjustToContents(); + + using (var stream = new MemoryStream()) + { + wb.SaveAs(stream); + return stream.ToArray(); + } + } + } public static async Task GenerateExcelForDataTableAsync(this IEnumerable data, string name) { @@ -112,14 +180,14 @@ public static async Task GenerateExcelForDataTableAsync(this IEnumera } } - public static async Task GenerateCSVForDataAsync(this IEnumerable data) + public static async Task GenerateCSVForDataAsync(this IEnumerable data) { var builder = new StringBuilder(); var stringWriter = new StringWriter(builder); await Task.Run(() => { - var columns = GetColumnsFromModel(typeof(T)).ToDictionary(x => x.Name, x => x.Value).OrderBy(x => x.Value.Order); + var columns = GetColumnsFromModel(typeof(T)).ToDictionary(x => x.Name, x => x.Value).OrderBy(x => x.Value.Order).ToList(); foreach (var column in columns) { @@ -130,7 +198,6 @@ await Task.Run(() => foreach (T item in data) { - var properties = item.GetType().GetProperties(); foreach (var prop in columns) { stringWriter.Write(PropertyExtensions.GetPropertyValue(item, prop.Value.Path)); @@ -142,5 +209,61 @@ await Task.Run(() => return Encoding.UTF8.GetBytes(builder.ToString()); } + + public static async Task GenerateCSVForDataAsync(this IEnumerable data, List cols = null) + { + var builder = new StringBuilder(); + var stringWriter = new StringWriter(builder); + + await Task.Run(() => + { + cols = cols ?? new List(); + + foreach (var column in cols) + { + stringWriter.Write(column.Label); + stringWriter.Write(", "); + } + stringWriter.WriteLine(); + + foreach (T item in data) + { + foreach (var prop in cols) + { + stringWriter.Write(PropertyExtensions.GetPropertyValue(item, prop.Name)); + stringWriter.Write(", "); + } + stringWriter.WriteLine(); + } + }); + + return Encoding.UTF8.GetBytes(builder.ToString()); + } + + + public static List SetDefinitions(this List columns, List definitions) + { + if (definitions == null || definitions.Count == 0) + { + return new List(); + } + + columns.AddRange(definitions); + + return columns; + } + + public static List SetDefinitions(this List columns, params (string, string, int)[] definitions) + { + if (definitions == null || definitions.Length == 0) + { + return columns; + } + + + columns.AddRange(definitions.OrderBy(x => x.Item3).ToList().Select(item => new ExcelColumnDefinition(item.Item1, item.Item2))); + + return columns; + } } } diff --git a/src/ApplicationCore/Models/ExcelColumnDefinition.cs b/src/ApplicationCore/Models/ExcelColumnDefinition.cs new file mode 100644 index 0000000..95e1528 --- /dev/null +++ b/src/ApplicationCore/Models/ExcelColumnDefinition.cs @@ -0,0 +1,18 @@ +namespace Fingers10.ExcelExport.Models +{ + public class ExcelColumnDefinition + { + public string Name { get; set; } + public string Label { get; set; } + + public ExcelColumnDefinition(string name, string label) + { + Name = name; + Label = label; + } + + protected ExcelColumnDefinition() + { + } + } +} diff --git a/src/ApplicationCore/README.md b/src/ApplicationCore/README.md new file mode 100644 index 0000000..061cd48 --- /dev/null +++ b/src/ApplicationCore/README.md @@ -0,0 +1,215 @@ +[![NuGet Badge](https://buildstats.info/nuget/fingers10.excelexport)](https://www.nuget.org/packages/fingers10.excelexport/) +[![Open Source Love svg1](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)](https://github.com/fingers10/open-source-badges/) + +[![GitHub license](https://img.shields.io/github/license/fingers10/ExcelExport.svg)](https://github.com/fingers10/ExcelExport/blob/master/LICENSE) +[![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://GitHub.com/fingers10/ExcelExport/graphs/commit-activity) +[![Ask Me Anything !](https://img.shields.io/badge/Ask%20me-anything-1abc9c.svg)](https://GitHub.com/fingers10/ExcelExport) +[![HitCount](http://hits.dwyl.io/fingers10/badges.svg)](http://hits.dwyl.io/fingers10/badges) + +[![GitHub forks](https://img.shields.io/github/forks/fingers10/ExcelExport.svg?style=social&label=Fork)](https://GitHub.com/fingers10/ExcelExport/network/) +[![GitHub stars](https://img.shields.io/github/stars/fingers10/ExcelExport.svg?style=social&label=Star)](https://GitHub.com/fingers10/ExcelExport/stargazers/) +[![GitHub followers](https://img.shields.io/github/followers/fingers10.svg?style=social&label=Follow)](https://github.com/fingers10?tab=followers) + +[![GitHub contributors](https://img.shields.io/github/contributors/fingers10/ExcelExport.svg)](https://GitHub.com/fingers10/ExcelExport/graphs/contributors/) +[![GitHub issues](https://img.shields.io/github/issues/fingers10/ExcelExport.svg)](https://GitHub.com/fingers10/ExcelExport/issues/) +[![GitHub issues-closed](https://img.shields.io/github/issues-closed/fingers10/ExcelExport.svg)](https://GitHub.com/fingers10/ExcelExport/issues?q=is%3Aissue+is%3Aclosed) + +# Excel Export +Simple classes to generate Excel/CSV Report in ASP.NET Core. + +To export/download the `IEnumerable` data as an excel file, add action method in your controller as shown below. Return the data as `ExcelResult`/`CSVResult` by passing filtered/ordered data, sheet name and file name. **ExcelResult**/**CSVResult** Action Result that I have added in the Nuget package. This will take care of converting your data as excel/csv file and return it back to browser. + +>**Note: This tutorial contains example for downloading/exporting excel/csv from Asp.Net Core Backend.** + +# Give a Star ⭐️ +If you liked `ExcelExport` project or if it helped you, please give a star ⭐️ for this repository. That will not only help strengthen our .NET community but also improve development skills for .NET developers in around the world. Thank you very much 👍 + +## Columns +### Name +Column names in Excel Export can be configured using the below attributes +* `[Display(Name = "")]` +* `[DisplayName(“”)]` + +### Report Setup +To customize the Excel Column display in your report, use the following attribute +* `[IncludeInReport]` +* `[NestedIncludeInReport]` + +And here are the options, + +|Option |Type |Example |Description| +|-------|--------|-----------------------------------------|-----------| +|Order |`int` |`[IncludeInReport(Order = N)]` |To control the order of columns in Excel Report| + +**Please note:** From **v.2.0.0**, simple properties in your models **with `[IncludeInReport]` attribute** will be displayed in excel report. You can add any level of nesting to your models using **`[NestedIncludeInReport]` attribute**. + +# NuGet: +* [Fingers10.ExcelExport](https://www.nuget.org/packages/Fingers10.ExcelExport/) **v3.0.0** + +## Package Manager: +```c# +PM> Install-Package Fingers10.ExcelExport +``` + +## .NET CLI: +```c# +> dotnet add package Fingers10.ExcelExport +``` + +## Root Model + +```c# +public class DemoExcel +{ + public int Id { get; set; } + + [IncludeInReport(Order = 1)] + public string Name { get; set; } + + [IncludeInReport(Order = 2)] + public string Position { get; set; } + + [Display(Name = "Office")] + [IncludeInReport(Order = 3)] + public string Offices { get; set; } + + [NestedIncludeInReport] + public DemoNestedLevelOne DemoNestedLevelOne { get; set; } +} +``` + +## Nested Level One Model: + +```c# +public class DemoNestedLevelOne +{ + [IncludeInReport(Order = 4)] + public short? Experience { get; set; } + + [DisplayName("Extn")] + [IncludeInReport(Order = 5)] + public int? Extension { get; set; } + + [NestedIncludeInReport] + public DemoNestedLevelTwo DemoNestedLevelTwos { get; set; } +} +``` + +## Nested Level Two Model: + +```c# +public class DemoNestedLevelTwo +{ + [DisplayName("Start Date")] + [IncludeInReport(Order = 6)] + public DateTime? StartDates { get; set; } + + [IncludeInReport(Order = 7)] + public long? Salary { get; set; } +} +``` + +## Action Method + +```c# +public async Task GetExcel() +{ + // Get you IEnumerable data + var results = await _demoService.GetDataAsync(); + return new ExcelResult(results, "Demo Sheet Name", "Fingers10"); +} + +public async Task GetCSV() +{ + // Get you IEnumerable data + var results = await _demoService.GetDataAsync(); + return new CSVResult(results, "Fingers10"); +} +``` + +## Page Handler + +```c# +public async Task OnGetExcelAsync() +{ + // Get you IEnumerable data + var results = await _demoService.GetDataAsync(); + return new ExcelResult(results, "Demo Sheet Name", "Fingers10"); +} + +public async Task OnGetCSVAsync() +{ + // Get you IEnumerable data + var results = await _demoService.GetDataAsync(); + return new CSVResult(results, "Fingers10"); +} +``` + +## Pass columns definitions as a parameter to the constructor + +```c# +public class Employee +{ + public string EmployeeName { get; set; } + public DateTime? StartDate { get; set; } + public long? BasicSalary { get; set; } +} +``` + +* Excel +```c# +public async Task OnGetExcelAsync() +{ + // Get you IEnumerable data + var results = await _demoService.GetDataAsync(); + return new ExcelResult( + data: results, + sheetName: "Fingers10", + fileName: "Fingers10", + ("EmployeeName", "Employee Name", 1), // prop name, label, order + ("StartDate", "Start Date", 2), + ("BasicSalary", "Basic Salary", 3) + ); +} +``` + +* CSV +```c# +public async Task OnGetExcelAsync() +{ + // Get you IEnumerable data + var results = await _demoService.GetDataAsync(); + return new CSVResult( + data: results, + fileName: "Fingers10", + ("EmployeeName", "Employee Name", 1), // prop name, label, order + ("StartDate", "Start Date", 2), + ("BasicSalary", "Basic Salary", 3) + ); +} +``` + +# Target Platform +* .Net Standard 2.0 + +# Tools Used +* Visual Studio Community 2019 + +# Other Nuget Packages Used +* ClosedXML (0.95.0) - For Generating Excel Bytes +* Microsoft.AspNetCore.Mvc.Abstractions (2.2.0) - For using IActionResult +* System.ComponentModel.Annotations (4.7.0) - For Reading Column Names from Annotations + +# Author +* **Abdul Rahman** - Software Developer - from India. Software Consultant, Architect, Freelance Lecturer/Developer and Web Geek. + +# Contributions +Feel free to submit a pull request if you can add additional functionality or find any bugs (to see a list of active issues), visit the Issues section. Please make sure all commits are properly documented. + +# License +ExcelExport is release under the MIT license. You are free to use, modify and distribute this software, as long as the copyright header is left intact. + +Enjoy! + +# Sponsors/Backers +I'm happy to help you with my Nuget Package. Support this project by becoming a sponsor/backer. Your logo will show up here with a link to your website. Sponsor/Back via [![Sponsor via PayPal](https://www.paypalobjects.com/webstatic/mktg/Logo/pp-logo-100px.png)](https://paypal.me/arsmsi) \ No newline at end of file