Our next app-idea is the build the CSV to JSON Converter. This allows us to go back from a CSV definition to a JSON file format (the inverse of App-Idea 6). We allow users to input free-from text in CSV format or load the data from a CSV file. Then provide a conversion function to output the results in JSON format.
Because this app is the inverse of App-Idea 6 (JSON to CSV Converter), we are going to add this functionality to the existing app rather than creating a whole duplicate app from scratch. We are going to refactor the TextConverter
model class to better support different conversions, add the ConvertToJson
operation to our view model, and expose another button to the converter page to do this second conversion.
You can find the source for the Blazor.AppIdeas.Converters on GitHub. And the running sample of the Blazor app online.
So, let’s start by loading Blazor.AppIdeas.Converters solution from App-Idea 6 – Part 3 into Visual Studio.
Refactor TextConverter
In the initial version of TextConverter
, we placed a lot of JSON to CSV parsing code there. That was fine for the initial functionality, but now if we were to add CSV to JSON conversion code to this class it would become large and unwieldy. Also it would mix JSON to CSV methods with CSV to JSON methods and would soon become very confusing as to what the code is responsible for.
And this code would also break the design principles of doing one thing per class. So, this is a good opportunity to refactor this class and make it more modular.
We are going to keep the TextConverter
class as the public entry point for any conversions, but we are going to use the Strategy design pattern to provide separate classes for JsonToCsv and CsvToJson implementations. That way each class is responsible for one direction of conversion. And the TextConverter
class itself doesn’t have any internal knowledge of how either conversion is done.
First, let’s define the ITextConvertStrategy
interface (in the Blazor.AppIdeas.Converters project and Models folder) that provides the definition for all of our strategy classes.
namespace Blazor.AppIdeas.Converters.Models
{
public interface ITextConvertStrategy
{
string Convert(string source);
}
}
Our strategy interface defines one simple method that takes the source text and returns the converted text.
Then, let’s move all of the JSON to CSV conversion logic from TextConverter
into the JsonToCsvConverter
class – in the Blazor.AppIdeas.Converters project and Models folder.
using System;
using System.Linq;
using System.Text;
using System.Text.Json;
namespace Blazor.AppIdeas.Converters.Models
{
internal class JsonCsvConverter : ITextConvertStrategy
{
private string _sourceText;
public string Convert(string source)
{
_sourceText = source;
var result = new StringBuilder();
var element = GetJsonRootArray();
result.AppendLine(ParseJsonPropertyNames(element));
result.Append(ParseJsonElementArray(element));
return result.ToString();
}
private JsonElement GetJsonRootArray()
{
var doc = JsonDocument.Parse(_sourceText);
if (doc.RootElement.ValueKind == JsonValueKind.Object)
{
doc = JsonDocument.Parse($"[{_sourceText}]");
}
if (doc.RootElement.ValueKind != JsonValueKind.Array)
{
throw new NotSupportedException("Unsupported JSON root type.");
}
return doc.RootElement;
}
private string ParseJsonPropertyNames(JsonElement root)
{
var result = new StringBuilder();
var firstElement = root.EnumerateArray().First();
foreach (var property in firstElement.EnumerateObject())
{
result.Append($"{property.Name}, ");
}
return result.ToString().TrimEnd(',', ' ');
}
private string ParseJsonElementArray(JsonElement element)
{
StringBuilder result = new StringBuilder();
foreach (var e in element.EnumerateArray())
{
result.AppendLine(ParseJsonElement(e));
}
return result.ToString();
}
private string ParseJsonElement(JsonElement element)
{
var result = new StringBuilder();
foreach (var property in element.EnumerateObject())
{
result.Append($"{property.Value}, ");
}
return result.ToString().TrimEnd(',', ' ');
}
}
}
- We made the strategy implementation class internal because it shouldn’t be accessed directly.
- We moved the helper methods (
GetJsonRootArray
,ParseJsonPropertyNames
,ParseJsonElementArray
, andParseJsonElement
) directly fromTextConverter
to this class, but their logic is largely unchanged. - We implemented the
Convert
method (lines #12-22) to:- Place the specified text into the _sourceText member variable.
- Get the root element from the parsed
JsonDocument
. - Then, get all of the property names from the root element.
- Next, get the parsed version of all of the elements in the Json array.
- Finally, return the string that represents the completed conversion.
Next, let’s create the CsvToJsonConverter
to implement the reverse logic – in the Blazor.AppIdeas.Converters project and Models folder.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Blazor.AppIdeas.Converters.Models
{
internal class CsvJsonConverter : ITextConvertStrategy
{
private static readonly char[] _csvSeparators = new char[] { ',' };
private const string _jsonSeparator = ", ";
public string Convert(string source)
{
StringBuilder builder = new StringBuilder();
builder.AppendLine("[");
var lines = GetCsvLines(source);
var lineCount = lines.Count();
var properties = ParseCsvPropertyNames(lines.First());
for (int ctr = 1; ctr < lineCount; ctr++)
{
var json = ParseCsvLine(lines.ElementAt(ctr), properties);
var suffix = (ctr != lineCount - 1) ? _jsonSeparator : string.Empty;
builder.AppendLine(json + suffix);
}
builder.AppendLine("]");
return builder.ToString();
}
private IEnumerable<string> GetCsvLines(string text) =>
text.Split(System.Environment.NewLine,
StringSplitOptions.RemoveEmptyEntries);
private IEnumerable<string> ParseCsvPropertyNames(string line) =>
line.Split(_csvSeparators,
StringSplitOptions.RemoveEmptyEntries);
private string ParseCsvLine(string line, IEnumerable<string> properties)
{
var values = line.Split(_csvSeparators,
StringSplitOptions.RemoveEmptyEntries);
if (values.Length < properties.Count())
throw new FormatException();
string jsonLine = string.Empty;
for (int ctr = 0; ctr < properties.Count(); ctr++)
{
jsonLine += FormatJsonProperty(properties.ElementAt(ctr).Trim(),
values.ElementAt(ctr).Trim());
}
return FormatJsonLine(jsonLine);
}
private string FormatJsonProperty(string property, string value) =>
@$"""{property}"" : ""{value}"", ";
private string FormatJsonLine(string jsonLine) =>
" { " + jsonLine.TrimEnd(',', ' ') + " }";
}
}
This class contains the new logic to read CSV and produce JSON, so let’s look at the methods in detail.
- The
Convert
method (lines #13-32), as the main entry point to this class:- Produces the initiator of a JSON array.
- Separates all of the CSV lines to an array.
- Gets the array of the CSV properties from the first line of the source text.
- Then, loops through the remaining lines calling
ParseCsvLine
to transform it into a JSON line. - And it appends the converted line to the result.
- At the end, it closes off the JSON array.
- Finally, it returns the resulting converted string.
- The
GetCsvLines
method (lines #34-36) parses each new line to a separate row in the CSV and returns an array with each line. - The
ParseCsvPropertyNames
method (lines #38-40) separates out each property name in a line (based on the comma separator), and returns an array of property names. - The
ParseCsvLine
method (lines #42-58) takes a line of CSV and the list of properties.- It splits the CSV line into its property values (also by the comma separator).
- It loops through the defined properties and formats the JSON result with the CSV property name and value.
- Concatenating the JSON properties along the way.
- Finally it returns the converted JSON row with the appropriate start and end tokens and property list.
- The
FormatJsonProperty
method (lines #60-61) simply knows how to format a JSON property name-value pair from its parameters. - The
FormatJsonLine
method (lines #63-64) formats the appropriate start and end tokens to the JSON property list.
Finally, let’s update the TextConverter
class to use the right conversion strategy class depending on its context.
using Blazor.AppIdeas.Converters.Services;
using System;
namespace Blazor.AppIdeas.Converters.Models
{
public class TextConverter
{
public TextConverter(string text)
{
SourceText = text ?? throw new ArgumentNullException(nameof(text));
}
public string SourceText { get; }
public string JsonToCsv() =>
PerformConversionTo(IBrowserFileAdapter.FileType.CSV);
public string CsvToJson() =>
PerformConversionTo(IBrowserFileAdapter.FileType.JSON);
private string PerformConversionTo(IBrowserFileAdapter.FileType type)
{
if (string.IsNullOrEmpty(SourceText)) return string.Empty;
ITextConvertStrategy converter = (type == IBrowserFileAdapter.FileType.CSV) ?
new JsonCsvConverter() :
new CsvJsonConverter();
return converter.Convert(SourceText);
}
}
}
- First, we deleted all of the code in
JsonToCsv
method and replaced it with a call to thePerformConversionTo
helper method. - Then, we add the
CsvToJson
method that also calls thePerformConversionTo
method with the appropriate context parameter. - We defined separate entry points for each, so the callers would not need to know about the
FileType
enum required for the conversion. - Finally, the
PerformConversionTo
method (lines #21-29) takes which format to convert to as its context.- It verifies we have a valid
SourceText
property. - Uses the
FileType
context to decide which converter strategy class to use (creating the appropriate instance). - Then, calls the
Convert
method with theSourceText
property as input… return that result from this method.
- It verifies we have a valid
Update JsonCsvConverterViewModel
With the model classes refactored, our changes to the view model are trivial. Let’s make these changes to the JsonCsvConverterViewModel
class:
using Blazor.AppIdeas.Converters.Models;
using Blazor.AppIdeas.Converters.Services;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.JSInterop;
using System;
using System.Threading.Tasks;
namespace Blazor.AppIdeas.Converters.ViewModels
{
public class JsonCsvConverterViewModel
{
private const string _jsCopyTextMethod = "clipboardCopy.copyText";
private const string _convertedTextFilename = "ConvertedText";
private readonly IJSRuntime _jsRuntime;
private readonly IBrowserFileAdapter _browserFileAdapter;
public JsonCsvConverterViewModel(
IJSRuntime jsRuntime, IBrowserFileAdapter fileAdapter)
{
_jsRuntime = jsRuntime ??
throw new ArgumentNullException(nameof(jsRuntime));
_browserFileAdapter = fileAdapter ??
throw new ArgumentNullException(nameof(fileAdapter));
}
public string SourceText { get; set; }
public string ConvertedText { get; set; }
public bool IsConvertedTextEmpty => string.IsNullOrEmpty(ConvertedText);
public IBrowserFileAdapter.FileType ConvertedType { get; set; }
public string ErrorMessage { get; private set; }
public bool HasError => !string.IsNullOrEmpty(ErrorMessage);
public void ConvertToCsv()
{
try
{
ErrorMessage = null;
var converter = new TextConverter(SourceText);
ConvertedText = converter.JsonToCsv();
ConvertedType = IBrowserFileAdapter.FileType.CSV;
}
catch (Exception ex)
{
ErrorMessage = $"Cannot parse source text. {ex.Message}";
ConvertedText = null;
}
}
public void ConvertToJson()
{
try
{
ErrorMessage = null;
var converter = new TextConverter(SourceText);
ConvertedText = converter.CsvToJson();
ConvertedType = IBrowserFileAdapter.FileType.JSON;
}
catch (Exception ex)
{
ErrorMessage = $"Cannot parse source text. {ex.Message}";
ConvertedText = null;
}
}
public void ClearAll()
{
SourceText = null;
ConvertedText = null;
ErrorMessage = null;
}
public async Task Copy() => await _jsRuntime.InvokeVoidAsync(
_jsCopyTextMethod,
ConvertedText);
public async Task OpenInputFile(InputFileChangeEventArgs e)
{
try
{
ErrorMessage = null;
_ = e ?? throw new ArgumentNullException(nameof(e));
if (e.FileCount > 1)
{
ErrorMessage = "Application does not support multiple file selection.";
}
else
{
SourceText = await _browserFileAdapter.ReadTextAsync(e.File)
.ConfigureAwait(false);
}
}
catch (Exception ex)
{
ErrorMessage = $"Cannot read file '{e?.File.Name}'. {ex.Message}";
}
}
public async Task DownloadConvertedText()
{
try
{
ErrorMessage = null;
await _browserFileAdapter.SaveTextAsAsync(
_jsRuntime,
_convertedTextFilename,
ConvertedType,
ConvertedText)
.ConfigureAwait(false);
}
catch (Exception ex)
{
ErrorMessage = $"Cannot download converted text file. {ex.Message}";
}
}
}
}
We made two changes to the view model:
- We added the
ConvertedType
property (line #33) to track theConvertedText
‘s format type. Then, we updated theConvertToCsv
method (line #47) to set that property to CSV type when it was processed. And, in theDownloadConvertedText
method (line #116), we the currentConvertedType
property in the call toIBrowserFileAdapter.SaveTextAsAsync
, so that the appropriate file extension is used during the download. - We added the
ConvertToJson
operation (lines #56-71) which:- Creates the
TextConverter
class with the currentSourceText
. - Updates
ConvertedText
by calling theTextConverter.CsvToJson
method. - Sets the
ConvertedType
to JSON. - And handle any exceptions throughout the process by providing an appropriate
ErrorMessage
.
- Creates the
That was a bit of a trek in refactoring our model classes to be more flexible and more loosely coupled. But the work was worth it, because we simplified the TextConverter
class and provided a great place for extending our conversion types with the ITextConverterStrategy
. If we had a new requirement to convert JSON to XML (for example), it would be trivial to add a new converter strategy class to handle that functionality.
And, since we have lots of tests in place with these projects, we feel comfortable doing this refactoring and knowing that we can validate the new code still works as we expect.
Add JSON Button to JsonCsvConverter Page
Next, we need to make minor UI changes to the page:
@page "/json-csv-convert"
@inject JsonCsvConverterViewModel vm
<div class="col-12 mt-3 pb-2 mb-3 container">
<h3 class="mt-3">JSON-CSV Converter</h3>
<hr />
<EditForm class="form-row" Model=@vm>
<div class="form-group col-lg-5 col-md-12">
<InputTextArea id="convert-from-value" class="form-control"
placeholder="Type JSON or CSV..." rows="8"
@bind-Value=vm.SourceText />
<div class="button-wrap">
<label class="btn btn-secondary" for="convert-open-file">
<span class="oi oi-cloud-upload" aria-hidden="true" /> Load File
</label>
<InputFile id="convert-open-file" style="width: 100%"
OnChange="vm.OpenInputFile"/>
</div>
</div>
<div class="col-lg-2 col-md-12">
<button id="btn-convert-csv"
class="btn btn-primary col-lg-10 offset-lg-1 col-4 mb-lg-1 mb-3"
@onclick="vm.ConvertToCsv">
To CSV
</button>
<button id="btn-convert-json"
class="btn btn-primary col-lg-10 offset-lg-1 col-4 mb-lg-1 mb-3"
@onclick="vm.ConvertToJson">
To JSON
</button>
<button id="btn-clear"
class="btn btn-primary col-lg-10 offset-lg-1 col-3 mb-lg-1 mb-3"
@onclick="vm.ClearAll">
Clear
</button>
</div>
<div class="form-group col-lg-5 col-12">
<InputTextArea id="converted-value" class="form-control" rows="8"
readonly @bind-Value=vm.ConvertedText />
<div>
<button id="download-file" class="btn btn-outline-secondary text-nowrap"
style="width: 52%" disabled="@vm.IsConvertedTextEmpty"
@onclick="vm.DownloadConvertedText">
<span class="oi oi-cloud-download" aria-hidden="true" />
Download File
</button>
<button id="clipboard-copy" class="btn btn-outline-secondary"
style="width: 46.5%; margin-left: -2px"
disabled="@vm.IsConvertedTextEmpty" @onclick="vm.Copy">
<span class="oi oi-clipboard" aria-hidden="true" />
Copy
</button>
</div>
</div>
@if (vm.HasError)
{
<div class="alert alert-danger col-12 ml-1">
<strong>Error:</strong> @vm.ErrorMessage
</div>
}
</EditForm>
</div>
@code {
}
First, we update buttons btn-convert-csv
and btn-clear
to take up less space in portrait mode (lines #22 & 32).
And, we add a new button element (named: To JSON) for the user to perform a CSV->JSON conversion (lines #26-30). This button’s onclick
event is bound to the JsonCsvConverterViewModel.ConvertToJson
method.
With all of our code changes in place, we can build and run (Ctrl + F5) our application locally again. The app visually is exactly the same except for the new ‘To JSON’ button. We can validate the new functionality by editing or loading some valid CSV content and converting it to the corresponding JSON format.


In conclusion, we have built the reverse conversion to go from CSV format to JSON format. In this lesson, we have learned:
- To define and implement the Strategy design pattern to better encapsulate our code.
- To use the appropriate conversion strategy class based on context.
- How to do a large code refactoring to enable the Strategy pattern.