SCGen Update

With some client Sitecore work coming up, I've had to think about how to get rid of TDS for that specific client. I haven't had to do much Sitecore template work on that project at all, so i've been able to exclude TDS projects from the solution pretty much from the beginning.
However, the TDS code generation within that project created a Model.cs file that is nearly 53,000 lines. 2.4 MB!! It is pretty monumentally important that scgen can generate pretty much the same code, but without the enormous overhead of TDS. However, not the same code, as much of the code in that Model.cs is repeated, like full repeated "using" statements for every single type... (ugh).  I can probably get it down to half the size, or even better!
The TDS code generator was generating things that weren't covered by scgen, like field wrapper properties along with field wrapper value getters.  It was generating solr index attributes. Index field attributes would use the name of the property but as all lowercase and underscore separated, as well as the C# style property name. 
The project Model.cs needed a lot of new things that just isn't covered by scgen, and I didn't want to add dedicated properties to scgen that only this project needs.
So, my solution...
Long story short, Go allows you to deserialize json to a concrete structure much like Newtonsoft.Json, and it allows a field type to be json.RawMessage. So the FieldType type looks like this now
type FieldType struct {
	TypeName      string          `json:"typeName"`
	CodeType      string          `json:"codeType"`
	Suffix        string          `json:"suffix"`
	PropertiesRaw json.RawMessage `json:"properties"`
	Properties    FieldTypePropertyMap

type FieldTypePropertyMap map[string]string

To define these properties, you can just add a "properties" json property to the field type. Here's an example for the "checkbox" field type:

        { "typeName": "Checkbox", "codeType": "bool", "suffix": "", "properties": {"SpecialProperty": "SpecialPropertyValue"} },

The process for including this and processing the text template is pretty verbose. It's not so bad if you can always depend on that property being there. But first, if you need to check for the property, you can use Go's Text Template "index" method, like so:

{{ if ne (index $field.Properties "propertyName") "" }}   The property exists, use it here.  {{ end }}

If the map doesn't include that property, it will just return a blank string. And to write it out in the template, simply do this, again with the index method:

{{ index $field.Properties "propertyName" }}

Feel free to browse the code at  There was also an update to the configuration helper that I use for nearly every project I create in Go, located at

The Mind of a 20+ Year Developer

I'm not sure how long I've been coding. Let me see... I started with GWBasic in my senior year of high school, so approximately Fall of 1996, so 22 years fully, but I didn't know jack then. I don't know jack still, so by that measure, I've not been developing at all :)

But computer science started in college in Spring of 1998, so it's been over 20 years since I actually started to learn some things. Those were exciting times! Anyway...

As a developer of 20 years, really into learning everything I could, having dealt with many different languages and coding paradigms, frameworks, ideas, projects, protocols, etc. at some point, a lot of solutions started to seem obvious to me. For a recent example, I've been working on my Go Sitecore API. Sitecore is a hierarchical system for storing content. Content is a broad term in the Sitecore world. As they say, "Sitecore is built on Sitecore". It stores all of your data structures (templates), content, ways to render the content (layout and renderings for all intents and purposes), system data, etc. Everything is a node. A node is an instance of a "Template".  The "Template" node is a node. Etc. And it's all tree based.

So in writing the Sitecore API, you don't necessarily want to deal with the entire tree. Also being a tree, the nodes have a "Path" property, like /sitecore/templates/User Defined/My Template. I wrote a simple way to build the tree from the database, then filter out the tree by a set of paths ( paths []string). This would simply go through the tree and the result would be a map of Guid -> Item (a node), where the nodes returned would all reside within the path specified. You could provide a path like "-/sitecore/system" (beginning with a hyphen) to say that you want to exclude those items. That code is here. So I found myself needing the opposite.

Give me all nodes in these paths, then give me all nodes NOT in these paths. You could write a set operation, an XOR or something like that. But I needed to do it by path. Knowing I had the path operations like "-/sitecore" (starting with hyphen) to exclude items, I quickly said to myself, "why not use the same paths, and where it starts with -, remove it, and if it doesn't start with -, then prepend it, and use those paths?"  So that's what I did. You can see that code here.

Of course, now I'm thinking the XOR operation might be a better idea! Give me all of the nodes in those paths, then loop through the base tree and add any nodes where the ID is not in the resulting filtered tree... that might be a little bit better, I think... although it does result in two loops through the entire tree contents, my original idea may actually be the better one.

So you can see how the mind of a 20 year developer works. Also I'm not afraid to say "Oh, yeah, that can be done much better, I'm going to rewrite it."

An Uncanny Memory

Another thing that I've noticed is that I know how pretty much everything I wrote works. For every project, if someone asked me, "Hey, there's a component in this project you worked on 5 years ago, I have to make an update to it, can you tell me how you wrote it?"  "Sure! It works like this" and then I'd spout off all the things it does. And any thing to watch out for. Sometimes I'm amazed at how I have retained that information through all I've worked on in the past 20 years.

I might benchmark those two methods and see which one is faster. Happy Coding!

Sitecore Doc

Sitecore solutions can become large and unweildy. Recently I was tasked with the following: Find out which page each of these service methods (40-50) are called. With how .NET and Sitecore applications (generally all good applications) are written, a service method call would be written within a component, but that component could be put on any page!

Luckily these components manifest themselves in Sitecore as "renderings". They can have data sources and parameters. And code, which is what leads us to this mess in the first place ;)

First we'd need a way to map these renderings to service calls. I came up with a generic "info" data field to do that in a JSON file which defines all of the renderings we're interested in. On a side note, I only provide those that we're interested in, this current project would yield a 4-5 MB result file which would be ridiculous if it included everything. That JSON looks like this:


    "includeUndefined": false,
    "renderings": [
            "id": "6A8BB729-E186-45E7-A72E-E752FDEC2F48",
            "name": "AccountBalancePurgeSublayout",
            "info": [
                "CustomerInformationService.GetCardStatusV4(authHeader, accountId)",
                "CustomerInformationService.GetPurgeInfo(authHeader, accountID)"

Using my (recently updated to accommodate this request) Go Sitecore API, I was able to take that information and map it against every page renderings (or standard values renderings) and produce a file that is filled with every page and their (eventual) calls into service methods. These aren't directly called within the page code (usually), and there's heavy caching going on as well. Here's what the output looks like:


    Name:     booking-calendar
    ID:       f1837270-6aca-4115-94bc-08d1a4ed43ad
    Path:     /sitecore/content/REDACTED/booking-calendar
                    Path:         /layouts/REDACTED2013/REDACTED/SubLayouts/Booking/Calendar/CalendarBooking.ascx
                    Placeholder:  content 
                                  ReservationService.GetCashCalendarV3(GetAuthHeader(),promoCode,startDate,endDate,isHearingAccess,isMobilityAccess, isWeb)
                                  ReservationService.GetCashCalendarWithArrivalV3(GetAuthHeader(), promoCode, roomType, arrivalDt, numNights, isWeb)
                    Path:         /layouts/REDACTED2013/REDACTEDMobile/SubLayouts/Booking/Calendar/CalendarBookingMobile.ascx
                    Placeholder:  content 
                                  ReservationService.GetCashCalendarV3(GetAuthHeader(),promoCode,startDate,endDate,isHearingAccess,isMobilityAccess, isWeb)
                                  ReservationService.GetCashCalendarWithArrivalV3(GetAuthHeader(), promoCode, roomType, arrivalDt, numNights, isWeb)

This was very useful for this specific task, however it's written in a way that will be very useful going forward, to provide insights into our Sitecore implementations and how the content is structured.

This app will see updates (sorry the code isn't available for now) so that it will show usages among different renderings, unused ones or broken (exists in a renderings field but not as an actual item in sitecore [was deleted or not imported]), and other stuff that I can think of. This binary is named "scdoc" as I like to keep my names short :)  The Sitecore Code Generation tool I wrote is simply "scgen".

Check out that Go Sitecore API though if you want to easily query your Sitecore database!  Happy Coding :)

Go and Sitecore, Part 3

In parts 1 and 2, so far, we've covered code generation with Go against a Sitecore template tree, and serializing items from the database to disk. Part 3 takes that serialized form and updates the database with items and fields that are missing or different, then clears out any items or fields that were orphaned in the process.

It probably doesn't do the clearing orphaned fields completely correctly, as I will only clear fields where the item doesn't exist anymore. It won't clear fields that no longer belong to the new template if the item's template changed. That'll probably be an easy change though, as it could probably be done with a single (albeit, advanced) query.

Deserializing involves the following steps.

  1. Load all items (already done at the beginning every time the program runs)
  2. Load all field values. This happens if you are serializing or deserializing.
  3. Read the contents from disk, mapping serialized items with items in the database.
  4. Compare items and fields.
    1. If an item exists on disk but not in the database, it needs an insert
    2. If an item exists on the database but not on disk, it needs a delete (and all fields and children and children's fields, all the way down its lineage)
    3. #2 works if an item was moved because delete happens after moves.
    4. Do the same thing for fields... update if it changed, delete if in the db but not on disk, insert if on disk but not in the db.
  5. This can, in some cases, cause thousands of inserts or updates, so we'll do batch updates concurrently.

Deserialization code just involves 2 regular expressions, and filepath.Walk to get all serialized files. Read the files, build the list, map them to items where applicable, decide whether to insert / update / delete / ignore, and pass the whole list of updates to the data access layer to run the updates.

I love the path and filepath packages. Here's my filepath.Walk method.

func getItemsForDeserialization(cfg conf.Configuration) []data.DeserializedItem {
	list := []data.DeserializedItem{}
	filepath.Walk(cfg.SerializationPath, func(path string, info os.FileInfo, err error) error {
		if strings.HasSuffix(path, "."+cfg.SerializationExtension) {
			bytes, _ := ioutil.ReadFile(path)
			contents := string(bytes)
			if itemmatches := itemregex.FindAllStringSubmatch(contents, -1); len(itemmatches) == 1 {
				m := itemmatches[0]
				id := m[1]
				name := m[2]
				template := m[3]
				parent := m[4]
				master := m[5]

				item := data.DeserializedItem{ID: id, TemplateID: template, ParentID: parent, Name: name, MasterID: master, Fields: []data.DeserializedField{}}

				if fieldmatches := fieldregex.FindAllStringSubmatch(contents, -1); len(fieldmatches) > 0 {
					for _, m := range fieldmatches {
						id := m[1]
						name := m[2]
						version, _ := strconv.ParseInt(m[3], 10, 64)
						language := m[4]
						source := m[5]
						value := m[6]

						item.Fields = append(item.Fields, data.DeserializedField{ID: id, Name: name, Version: version, Language: language, Source: source, Value: value})
				list = append(list, item)

		return nil

	return list

I did a quick and crude "kick off a bunch of update processes to cut the time down" method.

func update(cfg conf.Configuration, items []data.UpdateItem, fields []data.UpdateField) int64 {
	var updated int64 = 0
	var wg sync.WaitGroup
	itemGroupSize := len(items)/2 + 1
	fieldGroupSize := len(fields)/4 + 1

	// items - 2 processes
	for i := 0; i < 2; i++ {
		grp := items[i*itemGroupSize : (i+1)*itemGroupSize]
		go func() {
			updated += updateItems(cfg, grp)

	// fields - 4 processes
	for i := 0; i < 4; i++ {
		grp := fields[i*fieldGroupSize : (i+1)*fieldGroupSize]
		go func() {
			updated += updateFields(cfg, grp)


	return updated

Very unclever. Take all of the update items and fields, break them into a set number of chunks, kick off six processes, allocating twice as many for fields than for items. Each call to the respective update methods opens its own connection to SQL Server. This can be done much better but it does accomplish what I set out to accomplish. Utilize Go's coroutines (goroutines) and where something can be done concurrently, do it concurrently to try to cut down the time required. This is the only process that uses Go's concurrent constructs.

That's it for part 3!  Part 4 will come more quickly than part 3 did. I had some things going on, a year anniversary with my girlfriend, lots of stuff :)

Part 1 - Generation
Part 2 - Serialization
Part 3 - Deserialization

Go and Sitecore, Part 2

In part 1, I covered how I'm now generating code from Sitecore templates, to a limited degree. I won't share the whole process and the whole program until the end, but just going over touch points until then.

For part 2, we'll cover Sitecore serialization. For the terminology, I'm not sure what TDS or other similar tools would refer to them as, but I will refer to these acts as serialization (writing Sitecore contents to disk) and deserialization (reading Sitecore contents from disk and writing to the database)

For Sitecore serialization, I would say step 1 is to decide which fields you DON'T want to bring over. In the past, I've had loads of issues with serializing things like Workflow state. And locks. So my approach is to ignore the existence of certain fields. Essentially, find out all of the fields on "Standard template", and decide which ones are essential or useful. Remove those from a global list of "ignored fields" list. Then get your data. For the data, from part 1 we use the same tree of items. When we build the tree, it gets a root node tree and an item map  (map[string]*data.Item). For serialization we need the item map. The root is only useful for building paths, after that we could most likely toss it. With the item map in hand, and a list of ignored fields, we can get the data.

        with FieldValues (ValueID, ItemID, FieldID, Value, Version, Language, Source)
                ID, ItemId, FieldId, Value, 1, 'en', 'SharedFields'
            from SharedFields
                ID, ItemId, FieldId, Value, Version, Language, 'VersionedFields'
            from VersionedFields
                ID, ItemId, FieldId, Value, 1, Language, 'UnversionedFields'
            from UnversionedFields

        select cast(fv.ValueID as varchar(100)) as ValueID, cast(fv.ItemID as varchar(100)) as ItemID, f.Name as FieldName, cast(fv.FieldID as varchar(100)) as FieldID, fv.Value, fv.Version, fv.Language, fv.Source
                    FieldValues fv
                        join Items f
                            on fv.FieldID = f.ID
                    f.Name not in (%[1]v)
                order by f.Name;

With SQL Server, we're able to do common table expressions (CTEs) which makes this a single query and pretty easy to read. We're getting all field values except for those ignored. We get version and language no matter what, and we get the source, which table the value comes from. ValueID is just the Fields table ID which could be useful as a unique identifier, but it's not actually used right now.  We simply pull all of these values into another list of serialize items, matching their ItemID with the item map to produce a new "serialized item" type, which will be serialized. SerializedItem only has a pointer to the Item, and a list of field values. Field values have Field ID and Name, the Value, the version, the language, and the source (VersionedFields, UnversionedFields, SharedFields).

The item map is also trimmed down to items in paths that you specify, so you're not writing the entire tree. In SQL Server with the current database (12K items), the field value query with no field name filter takes 3 seconds and returns 190K values. That's a bit high for my liking, but when you're dealing with loads of data you have to be accepting of some longer load times.

The serialized file format is hard coded, versus being a text template. However I feel I could do the text template since I've found out how to remove surrounding whitespace (e.g.  {{- end }}, that left hyphen says remove whitespace to the left). However, putting it in a text template, as with code generation, implies that the format can be configured. But, this needs to be able to be read back in through deserialization, so should be less configurable, 100% predictable.

func serializeItems(cfg conf.Configuration, list []*data.SerializedItem) error {
	sepstart := "__VALUESTART__"
	sepend := "___VALUEEND___"

	for _, item := range list {
		path := item.Item.Path
		path = strings.Replace(path, "/", "\\", -1)
		dir := filepath.Join(cfg.SerializationPath, path)

		if err := os.MkdirAll(dir, os.ModePerm); err == nil {
			d := fmt.Sprintf("ID: %v\r\nName: %v\r\nTemplateID: %v\r\nParentID: %v\r\nMasterID: %v\r\n\r\n", item.Item.ID, item.Item.Name, item.Item.TemplateID, item.Item.ParentID, item.Item.MasterID)
			for _, f := range item.Fields {
				d += fmt.Sprintf("__FIELD__\r\nID: %v\r\nName: %v\r\nVersion: %v\r\nLanguage: %v\r\nSource: %v\r\n%v\r\n%v\r\n%v\r\n\r\n", f.FieldID, f.Name, f.Version, f.Language, f.Source, sepstart, f.Value, sepend)

			filename := filepath.Join(dir, item.Item.ID+"."+cfg.SerializationExtension)
			ioutil.WriteFile(filename, []byte(d), os.ModePerm)

	return nil

If you've looked into the TDS file format, you've noticed it adds the length of the value so that parsing the field value is "easier???" or something. However, it makes for git conflicts on occasion. Additionally, you can't just go in there and update the text and deserialize it.  For instance, if you had to bulk update a path that would end up in the value for each item, like a domain name or url in an external link field which is the value for many fields, with the TDS method you can't just do a find replace (unless the length of the value doesn't change!). Without the length you could find/replace across the whole path of serialized objects. There are other future benefits to this. Imagine you need to generate a tree but you don't want to use Sitecore API. You could generate this file structure and have it deserialize to Sitecore. The length doesn't help that scenario though, it just makes it a tiny less painful.

The idea for this was first, "common sense", but second, it's been working for HTTP and form posts for YEARS!! HTTP multipart forms just use the boundary property. My boundary isn't dynamic, it's just a marker. If that text were to show up in a Sitecore field, this program doesn't work. Most likely I'd replace underscores with some other value. I could generate a boundary at the start of serialization, and put it in a file in the root of serialization, like ".sersettings" with "boundary: __FIELDVALUE90210__" which would be determined at the start of serialization to be unique and having no occurrences in sitecore field values. Anyway, I've gone on too long about this :)

Also, the path and path/filepath packages in Go are the best. So helpful.

In this format, here is what the "sitecore" root node looks like serialized.

ID: 11111111-1111-1111-1111-111111111111
Name: sitecore
TemplateID: C6576836-910C-4A3D-BA03-C277DBD3B827
ParentID: 00000000-0000-0000-0000-000000000000
MasterID: 00000000-0000-0000-0000-000000000000

ID: 56776EDF-261C-4ABC-9FE7-70C618795239
Name: __Help link
Version: 1
Language: en
Source: SharedFields


ID: 577F1689-7DE4-4AD2-A15F-7FDC1759285F
Name: __Long description
Version: 1
Language: en
Source: UnversionedFields
This is the root of the Sitecore content tree.

ID: 9541E67D-CE8C-4225-803D-33F7F29F09EF
Name: __Short description
Version: 1
Language: en
Source: UnversionedFields
This is the root of the Sitecore content tree.

In part 3, we'll be looking into deserializing these items.


Part 1 - Generation
Part 2 - Serialization
Part 3 - Deserialization

Go and Sitecore, Part 1

I will use this post to sell you on the merits of Go, since I am in love with it. :)  In our dev shop, a C# MVC.NET etc shop, we've been using Hedgehog's Team Development for Sitecore. While this product does what it says it does, it's a few things. Slow, expensive, difficult to learn, does way too much, requires a website to be running, "clunky" (official term), and a few other things that make it undesirable. Fidgetty, if that's a word. Sometimes unreliable. My boss has decided to try to head away from that direction and that product.

However. There are a few good things that it does provide. They are useful if the rest of the product has its flaws. (Of course there are other products out there as well, like Unicorn). Those features that I find most useful as a developer, and a developer on a team (hence the T in TDS), are

  1. Code generation from templates
  2. Serializing Sitecore content (templates, layouts, and other items) and committing those to git.
  3. Deserializing Sitecore content which others have committed to git  (or which you've serialized earlier and messed up)

Those features, if they could be separated out, are desirable to keep around.

Over the past month or two, I've had to do a lot of work dealing directly with the sitecore database. You have Items, VersionedFields, UnversionedFields, and SharedFields. That's your data. Unless you are worried about the Media Library, then you might need to deal with the Blobs table. I haven't researched that fully, so I'm not sure of which tables are required for media. So I felt comfortable getting in there and giving code generation a shot with my new-found knowledge of the Sitecore table. Now I know them even more intimately.

Go and Sitecore

Go and Sitecore are a natural fit. First thing you need is the sql server library from "". That thing works great. You just have to change your "data source" parameter to "server" in your connection string. Very mild annoyance :) In building this thing, it's probably best to just select all items. Items table is a small set of data. The database I'm working on is 12,000+ items, which are all returned in milliseconds.

Select * from Items. In this case, though, I did a left joint to the SharedFields table two times, one to get the base templates (__Base templates) and one to get the field type (Type). If they are a Template, they'll have base templates sometimes, if they are a field, they'll have a field type. I just hard coded those field ids in there for now.

            cast(Items.ID as varchar(100)) ID, Name, replace(replace(Name, ' ', ''), '-', '') as NameNoSpaces, cast(TemplateID as varchar(100)) TemplateID, cast(ParentID as varchar(100)) ParentID, cast(MasterID as varchar(100)) as MasterID, Items.Created, Items.Updated, isnull(sf.Value, '') as Type, isnull(Replace(Replace(b.Value, '}',''), '{', ''), '') as BaseTemplates
                left join SharedFields sf
                    on Items.ID = sf.ItemId
                        and sf.FieldId = 'AB162CC0-DC80-4ABF-8871-998EE5D7BA32'
                left join SharedFields b
                    on Items.ID = b.ItemID
                        and b.FieldId = '12C33F3F-86C5-43A5-AEB4-5598CEC45116'

The next part is to use that item data to rebuild the sitecore tree in memory.

func buildTree(items []*data.Item) (root *data.Item, itemMap map[string]*data.Item, err error) {
	itemMap = make(map[string]*data.Item)
	for _, item := range items {
		itemMap[item.ID] = item

	root = nil
	for _, item := range itemMap {
		if p, ok := itemMap[item.ParentID]; ok {
			p.Children = append(p.Children, item)
			item.Parent = p
		} else if item.ParentID == "00000000-0000-0000-0000-000000000000" {
			root = item

	if root != nil {
		root.Path = "/" + root.Name
	return root, itemMap, nil

Since I select all Items, I know the "sitecore" item will be in there, with ParentID equal to all 0s. This is the root. After the tree is built, I assign paths based on the tree. And then assign all of the base templates. Base templates are of course crucial if you're generating code. You will want to provide implemented interfaces to each interface when you generate Glass Mapper interfaces, for instance.

assignPaths of course couldn't be any easier. For each item in the children, set the path equal to the root path + / + the item name. Then recursively set it for all of that item's children. That process takes no time in Go, even across 12000 items. Now your tree is built.

The path is important for determining a namespace for your interfaces. If your path is at /sitecore/templates/User Defined/MySite and there's a template under MySite with the relative path of "Components/CalendarData", you'd want the namespace to be some configured base namespace  (Site.Data) plus the relative path to that template, to get something like Site.Data.Components   and then your class or interface would be e.g. ICalendarData.

So after you determine all of that for every item, you just have to filter the itemMap by templates and their children (template section, template field), create a list of templates  ( Template struct is another type in my Go code, to better map templates, namespaces, fields, field types, etc), and run that list of templates against a "text/template" Template in Go. It's only a list of templates at this time, vs keeping the hierarchy, because you don't need hierarchy once you've determined the path. And Templates just have fields, once you break it down. They aren't template sections and then fields. They are just Template, Field, and Base Templates.

My text template is this:

{{ range $index, $template := .Templates }}
namespace {{ .Namespace }} {
    public class {{ $template.Item.CleanName }}Constants {
        public const string TemplateIdString = "{{ $template.Item.ID }}";
        public static readonly ID TemplateId = new ID(TemplateIdString);

    [SitecoreType(TemplateId={{ $template.Item.CleanName }}Constants.TemplateIdString)]
    public partial interface I{{ $template.Item.CleanName }} : ISitecoreItem{{ if (len $template.BaseTemplates) gt 0 }}{{ range $bindex, $baseTemplate := $template.BaseTemplates }}, global::{{ $baseTemplate.Namespace}}.I{{ $baseTemplate.Item.CleanName}}{{end}}{{end}} {
        {{ if (len $template.Fields) gt 0 }}
        {{ range $findex, $field := $template.Fields }}
            {{ $field.CodeType}} {{ $field.CleanName }}{{ $field.Suffix }} { get; set; }{{end}}{{end}}

A few other notes. In configuration, I provide a CodeType mapping. For each field type, you can specify what the type of that property should be. This is of course useful if you have a custom type for "Treelist" that you want to use, and also if you're generating code that's not in C#. I didn't want to hard code those values. Also useful if you provide custom field types in Sitecore.

Field suffix is for something like, if you have a Droptree, you can select one item, and that value would be a uniqueidentifier in the database. This is represented in C# as a "Guid" type. For these fields, I like to add an "ID" to the end of the property name to signify this. Then in the accompanying partial class that isn't overwritten every time you generate, provide a field for the actual item that it would reference, like  IReferencedItem ReferencedItem {get;set;}  where the generated code would yield a Guid ReferencedItemID {get;set;}  You get the picture ;)

For each field, template, and base template, you have a pointer to the Item from which it was created, so you have access to all of the item data. Like CleanName, ID, Created and Update times if you need them for some reason. The path. Everything.

The best part of this tool is that it does all of those things that I require. Code generation, serialization, and deserialization. And it does it in a neat way. Currently I'm not doing any concurrency so it might take 20 seconds to do all of it, but that's still way faster than TDS!

This is Part 1 of this small series. In the future I'll go over how I did, and improved, item serialization and deserialization, as well as some neat things I did to accomplish not running all of those processes each time you run the tool. So you can run it to just generate, or just deserialize, or generate and serialize. Not many permutations on that, but the important thing is you don't have to wait 8 seconds for it to also serialize if all you want to do is generate, which takes less than a second. 800 ms to be exact.

800 ms!!

Tune in for Part 2 soon! Serialization.


Part 1 - Generation
Part 2 - Serialization
Part 3 - Deserialization