Thursday, May 24, 2018

Combining Two PDFs Using .Net Core and a Free Library

I recently wrote a four-part series of posts describing how to display SSRS reports inside an Angular application. Once we delivered the requested features, there were a few more stories that were added, among them was the ability to attach multiple PDFs to each other before displaying them to the user. Since we already had everything in place to retrieve and display a single PDF at a time, the (obviously) tricky part of this task was to figure out how to merge multiple PDFs into a single document on the fly.

I think my first step was probably the same as most other developers: I Googled it. I kept coming across iTextSharp as the most common solution others had used so I dug into it and it looked like it was perfect. Unfortunately, it wasn't free for our scenario (the license allows certain free usages, but we didn't fall under any of them) so it wasn't really an option. I found a couple others way to do what I needed, but none were as good or as fast. I ultimately came across a Stack Overflow answer where someone mentioned a port to .Net Core (did I mention this was in .Net Core 2.0?) of the last version of iTextSharp that was under the LGPL (read: essentially free) license. Perfect! All I had to do was figure out how to make it work, which lead me to this post.

The NuGet package is iTextSharp.LGPLv2.Core (there is also iTextSharp.LGPLv2.Core.Fix, but I'm not sure what the difference(s) is/are). Once I had that installed it was a simple matter of writing a method to merge multiple PDFs together. Of course I wanted the method to be reusable and injectable so I created a basic interface with a single method that accepts an array (of undetermined length using the params keyword) of byte arrays representing the PDFs to merge together.
   1:  public interface IProvidePdfMerging
   2:  {
   3:      byte[] Merge(params byte[][] originals);
   4:  }

And then I created the implementing method.
   1:  public class PdfMerger : IProvidePdfMerging
   2:  {
   3:      public byte[] Merge(params byte[][] originals);
   4:      {
   5:          var files = originals.ToList();
   6:  
   7:          using (var stream = new MemoryStream())
   8:          {
   9:              var doc = new Document();
  10:              var pdf = new PdfCopy(doc, stream);
  11:              doc.Open();
  12:  
  13:              PdfReader reader;
  14:              PdfImportedPage page;
  15:  
  16:              files.ForEach(file =>
  17:              {
  18:                  reader = new PdfReader(file);
  19:                  for (var i = 0; i < reader.NumberOfPages; i++)
  20:                  {
  21:                      page = pdf.GetImportedPage(reader, i + 1);
  22:                      pdf.AddPage(page);
  23:                  }
  24:  
  25:                  pdf.FreeReader(reader);
  26:                  reader.Close();
  27:              });
  28:  
  29:              doc.Close();
  30:  
  31:              return stream.ToArray();
  32:          }
  33:      }
  34:  }

And that's pretty much it. I pass in the byte arrays representing the PDFs I want to merge, in the order I want to merge them, and then the resulting byte array is my new PDF. It's clean, it's fast, it's reusable, it's injectable. I covered all the bases pretty easily here.

Saturday, May 19, 2018

Angular Material Full Size Dialog on Small Devices

I restarted a project recently and decided to use Angular Material to build it. So far it's been working out pretty well. The latest version of Material (6.0.1) works nicely. The documentation leaves a lot to be desired, but it's not that difficult to muck around in the source and find what I need.

Today I started working on the sign-in/sign-out dialog for my site. I want the dialog to open as a full-screen dialog when the user is on a small device, and open as some other value when the user is on a regular computer (laptop, desktop, whatever). This didn't end up being very hard, but I had to mash together a couple of solutions I found so I wanted to put this post up for future reference.

If you Google something like "angular material full screen dialog mobile" you'll get a bunch of responses where people are looking for this exact feature.I started with this comment on one of the Github issues that was opened and then tweaked it a little bit using the BreakpointObserver that comes with Material (at least in version 6 it does). I haven't finalized what I want my sign-in/sign-out page/dialog to look like yet so for now let's just say we want the dialog to open at half the width and half the height of the window on larger devices and full screen on smaller devices.

Here's the code, and I'll explain it a bit more afterward.
   1:  import { Component } from '@angular/core';
   2:  import { BreakpointObserver, Breakpoints, BreakpointState } from '@angular/cdk/layout';
   3:  import { Observable } from 'rxjs';
   4:  import { MatDialog, MatDialogRef } from '@angular/material';
   5:  import { SignInComponent } from '../sign-in/sign-in.component';
...snip...
  12:  export class SidenavComponent {
  13:    isExtraSmall: Observable<BreakpointState> = this.breakpointObserver.observe(Breakpoints.XSmall);
  14:  
  15:    constructor(private breakpointObserver: BreakpointObserver, private dialog: MatDialog) {}
  16:  
  17:    openSignInDialog(): void {
  18:      const signInDialogRef = this.dialog.open(SignInComponent, {
  19:        width: '50%',
  20:        height: '50%',
  21:        maxWidth: '100vw',
  22:        maxHeight: '100vh',
  23:      });
  24:  
  25:      const smallDialogSubscription = this.isExtraSmall.subscribe(result =< {
  26:        if (size.matches) {
  27:          signInDialogRef.updateSize('100%', '100%');
  28:        } else {
  29:          signInDialogRef.updateSize('50%', '50%');
  30:        }
  31:      });
  32:  
  33:      signInDialogRef.afterClosed().subcsribe(result =< {
  34:        smallDialogSubscription.unsubscribe();
  35:      });
  36:    }
  37:  }

The only real issue I have with this code is that I have to specify the exact height and width I want to use when the dialog is displayed on a large device. All things considered, that's not really a big deal to me.

This is also pretty self-explanatory (I think).

On line 17 we open the dialog with a size that is half the height and width of the current window. We also set the max height and max width of the dialog explicitly to match the height and width of the viewport (the device, essentially). If you leave that part off you'll end up with your dialog being pulled over to the left of the screen and not taking up the full width.

On line 25 we subscribe to the observable that's watching the devices size to see if it drops below the predefined XSmall breakpoint. There are other breakpoints we could have used just by changing out the definition on line 13. When we subscribe we immediately get the last value from the observable. On devices that fall under the XSmall breakpoint, our result variable has a matches property that is set to true. On other devices, matches is set to false. All we have to do is invoke the updateSize function on the dialog ref we received back when we opened the dialog.

Finally, on line 33 we make sure to unsubscribe from the observable. This is just a clean-up precaution to make sure we avoid memory leaks.

Wednesday, May 16, 2018

SSRS Reports in an Angular App (Part 4 - Downloading the Report)

I recently wrote the firstsecond, and third parts of this series on displaying SSRS reports in an Angular application. At the end of the third post I had a report, in PDF format, being displayed via a 3rd party component in my Angular application and the user was able to preview the report for printing. Let's pick up from there.

In addition to printing (or print preview as it turned out) we also wanted users to be able to download the report with a name they provide. It was fairly straightforward to get this done, and just required combining two pieces.

The first thing we wanted to do was allow the user to provide a name. Since this is a pretty common feature (and the how-to is going to vary wildly from one project to the next) I'm going to skip it here. The key piece was using the file-saver (via npm package) to save the file to the user's computer.

To better facilitate unit testing and abstraction I created a FileSaver service that I could inject into my components when necessary:
   1:  import { Injectable } from '@angular/core';
   2:  import * as FileSaver from 'file-saver';
   3:  
   4:  @Injectable()
   5:  export class FileSaverService {
   6:    saveAs(blob: any, name: string): void {
   7:      const document = new Blob([blob], {
   8:        type: 'application/pdf'
   9:      });
  10:      FileSaver.saveAs(document, name);
  11:    }
  12:  }

Once I had that service created I just needed to inject it and use it like this:
   1:  save(): void {
   2:    this.fileSaverService.saveAs(new Blob([this.reportBytes.buffer]), this.downloadName);
   3:  }

That turned out to be pretty much all I needed to do to download my reports. I love the easy parts.
  1. Display a list of all reports in SSRS
  2. Allow the user to click on a single report to view the report
  3. Allow the user to print the report
  4. Allow the user to download the report
Update: All four parts of this series are complete now.
  1. SSRS Reports in an Angular App (Part 1 - Showing the Report)
  2. SSRS Reports in an Angular App (Part 2 - Showing the Report (correctly))
  3. SSRS Reports in an Angular App (Part 3 - Printing the Report)
  4. SSRS Reports in an Angular App (Part 4 - Downloading the Report)(this post)

Tuesday, May 15, 2018

SSRS Reports in an Angular App (Part 3 - Printing the Report)

I recently wrote the first and second parts of this series on displaying SSRS reports in an Angular application. At the end of the second post I had a report, in PDF format, being displayed via a 3rd party component in my Angular application and I was ready to allow the user to print the report. Let's pick up from there.

Printing a PDF seems pretty straight forward at first: I should just be able to run the window.print JavaScript function and be all set. Of course, it isn't actually that simple because then I wouldn't be writing this post.

Our project has a requirement to support Chrome, Firefox, Safari, and Edge so I needed a solution that would work in all browsers. After a bit of digging I found that I'd need to do a little bit of dancing around Edge (big shocker there), but the other three would readily support the method I wanted to use. The route that made the most sense was to use the built-in (mostly) createObjectUrl function to build a URL from a Blob created from the PDF bytes, then open that URL in a new window. That was easy enough:
   1:  print(): void {
   2:    const url = URL.createObjectUrl(new Blob([this.reportBytes], { type: 'application/pdf' }));
   3:    window.open(url);
   4:  }

As I wrote before, this worked fine for most of the browsers, but Edge wasn't happy so I had to modify it to work:
   1:  print(): void {
   2:    if (window.navigator && window.navigator.msSaveOrOpenBlob) {
   3:      window.navigator.msSaveOrOpenBlobBlob([this.reportBytes]));
   4:    } else {
   5:      const url = URL.createObjectUrl(new Blob([this.reportBytes], { type: 'application/pdf' }));
   6:      window.open(url);
   7:    }
   4:  }

This isn't a perfect solution because the report doesn't actually print right away when you click the Print button. It doesn't even invoke the native print function of the browser. It does, however, open the PDF in a new tab and allow the user to print it if they wish. We had to go that route because Edge had some issues launching the print dialog properly with a PDF (which goes back to not being able to use the createObjectUrl function). Not perfect, but good enough.
  1. Display a list of all reports in SSRS
  2. Allow the user to click on a single report to view the report
  3. Allow the user to print the report
  4. Allow the user to download the report
Update: All four parts of this series are complete now.
  1. SSRS Reports in an Angular App (Part 1 - Showing the Report)
  2. SSRS Reports in an Angular App (Part 2 - Showing the Report (correctly))
  3. SSRS Reports in an Angular App (Part 3 - Printing the Report)(this post)
  4. SSRS Reports in an Angular App (Part 4 - Downloading the Report)

Monday, May 14, 2018

Squashing Commit Messages in Git

A couple of times in the past week or two I've wanted to squash my merges into a single merge before pushing up to the remote server. I know that rebase can do that, but for whatever reason, rebase scares the tar out of me so I've never used it. Someday I'll have to learn it, but until then I found this neat answer on Stack Overflow.

tl;dr:

git commit -m "My most recent changes"
git reset --soft HEAD~3
git commit -m "All changes from last 3 commits"

Let's say we want all of our current work to be committed and squashed together with the previous two commits we made locally.
  1. Commit everything so the index is clean (that means that when you run git status you don't see any pending changes)
  2. Do a soft rewind (that's my term, I don't think it's a proper git term, though) to 3 commits ago (remember that we're squashing together our previous two commits with the commit we made in step 1 so we want to go back 3 now) using git reset --soft HEAD~3
  3. All changes from the previous 3 commits should now be staged for commitment, which you can verify by running git status
  4. Commit the staged changes with the -m flag and provide a single commit message/comment for this now-single commit that combines all 3
This way makes a lot of sense to me and I've found myself using it repeatedly. As always, I wanted to make a note of it here so I could find it later.

SSRS Reports in an Angular App (Part 2 - Showing the Report (correctly))

I recently wrote the first part of this series on displaying SSRS reports in an Angular application. At the end of that post I had a report, in HTML format, bound to an iframe and properly sized to display the full report without scrolling inside the iframe. However, I had a problem that some of the reports looked bad. Let's pick up from there.

I did some digging into the markup provided by SSRS and found that there's a bunch of jQuery that's used to do simple things like resize areas after they're rendered. When I bound that markup to the iframe, the jQuery wasn't able to run. That's actually the intended function. Since I was writing the markup into the iframe as opposed to using the src attribute of the iframe, there didn't seem to be much I could do to get that to work so we went back to the drawing board.

We ended up deciding to pull a PDF from SSRS and throw that up in our Angular application directly. Since we needed the ability to print and download the report anyway, using a PDF made plenty of sense. The only concern we really had was that any interactivity in the report would be lost when SSRS rendered it into a PDF (i.e. we'd lose sub-reports and click-throughs). This new direction, of course, introduced a new issue: how do we display a PDF report inside an Angular application?

The good news is that there's already an open-source component specifically for this. All we needed to do was get the report bytes from the server and give them over to the component. I setup my API to return a FileContentResult containing the PDF bytes, with a content type of 'application/pdf':

   1:  [HttpGet("/api/report")]
   2:  public async Task<IActionResult> GetReport(int id)
   3:  {
   4:      var client = new ReportExecutionServiceSoapClient(
   5:          ReportExecutionServiceSoapClient.EndpointConfiguration.ReportExecutionServiceSoap,
   6:          "http://<ssrs-server>/reportserver/reportexecution2005.asmx");
   7:  
   8:      LoadReportResponse loadedReport = await client.LoadReportAsync(new TrustedUserHeader(), "path/to/report", null);
   9:      ParameterValue parameter = new ParameterValue {Name = "ReportID", Value = id};
  10:      await client.SetExecutionParametersAsync(loadedReport.ExecutionHeader, new TrustedUserHeader(), new[] {parameter}, "en-us");
  11:      RenderRequest renderRequest = new RenderRequest(loadedReport.ExecutionHeader, new TrustedUserHeader(),
  12:          "PDF", @"<DeviceInfo><Toolbar>False</Toolbar></DeviceInfo>");
  13:      RenderResponse renderResponse = await client.RenderAsync(renderRequest);
  14:  
  15:      return new FileContentResult(renderResponse.Result, "application/pdf");
  16:  }

Once my API was returning the bytes of the PDF I just needed to get them in my Angular application and provide them to the component. I wrote about a little bit of difficulty doing this before so check out that (really short) post if you're confused. Here's the Angular code I ended up with (this is in a service):
   1:  getReport(id: number) {
   2:    const headers = {
   3:      'Content-Type': 'application/json',
   4:      'Accept': 'application/pdf'
   5:    };
   6:  
   7:    return this.httpClient.get(`http://<api-endpoint>/api/report/{id}`, {headers, responseType: arrayBuffer});
   5:  }

Now I had a way to get the bytes from the API into my angular application so all I had left to do was throw those bytes into the component. In the ngOnInit function of my component, I used the service to retrieve the report and bound the resulting ArrayBuffer to a property my component:
   1:  ngOnInit(): void {
   2:    this.reportService.getReport.subscribe(b => this.reportBytes = new Uint8Array(b));
   3:  }

Obviously, I needed to create a property called reportBytes on my component before this would work:
   1:  public reportBytes: Uint8Array;

Finally, I needed to use the 3rd party component in my markup. There are several options you can specify on this particular component. For the most part our implementation was simple and it actually worked just fine when we only provided the PDF bytes to display:
<pdf-viewer [src]="reportBytes"></pdf-viewer>

That was the last piece I had to put in place to render a report - in PDF format - in my Angular application. Of course, this would have worked just as well with any PDF so it's a nice reusable solution. With this last piece I can solidly check another item off my list.
  1. Display a list of all reports in SSRS
  2. Allow the user to click on a single report to view the report
  3. Allow the user to print the report
  4. Allow the user to download the report
Update: All four parts of this series are complete now.
  1. SSRS Reports in an Angular App (Part 1 - Showing the Report)
  2. SSRS Reports in an Angular App (Part 2 - Showing the Report (correctly))(this post)
  3. SSRS Reports in an Angular App (Part 3 - Printing the Report)
  4. SSRS Reports in an Angular App (Part 4 - Downloading the Report)

Friday, May 11, 2018

SSRS Reports in an Angular App (Part 1 - Showing the Report)

I started a new job a few weeks ago as a senior C# developer. Although I prefer to work in the full stack (front-end all the way back to the database), I liked the opportunity overall. Within two days, however, my company's needs shifted and I was back doing full stack development with Angular 5 (YAY!). Unfortunately, the first project I was assigned to involves displaying some SSRS reports inside of an Angular application. I don't have to write the reports, but we had some pretty non-negotiable requirements put on this project and finding the right solution has been... interesting.

First, the requirements.

  • Display some reports from an internal SSRS instance without exposing the SSRS instance itself to the public
  • Print the report using the native browser dialog
  • Download a PDF of the report directly from the page
    • When downloading the PDF, provide an option to name the PDF something unique
  • Any interactivity (drilldowns, tooltips, etc.) on the original report should still be available in on the page
So this already seems pretty daunting. Like, how-badly-do-I-want-this-job daunting. Because we use Scrum and this is pretty obviously an epic, we broke it down into smaller, more doable pieces.
  1. Display a list of all reports in SSRS
  2. Allow the user to click on a single report to view the report
  3. Allow the user to print the report
  4. Allow the user to download the report
Now this looks easier. The first item (display a list of all reports in SSRS) ended up being more of a design task than much coding. That's because the team responsible for building the reports (which luckily did not include me or I really might have just quit right then) provided us a stored procedure that returns the information we needed about the reports that were available.
  1. Display a list of all reports in SSRS
  2. Allow the user to click on a single report to view the report
  3. Allow the user to print the report
  4. Allow the user to download the report
The second item was always going to be the trickiest piece. How do we show a report from SSRS without exposing the SSRS server to the public? Well, it turns out that SSRS offers a nifty web service we'd be able to access to get the data we want. You can read more about that in the official docs here. Great, we were going to access a SOAP-based web service... from .Net Core 2.0. Oops. .Net Core is great (so great), but it didn't initially have support for accessing SOAP services. In another lucky break they added that feature recently so I was able to - rather simply - right-click on Dependencies in my project and choose Add Connected Service. This auto-generated the reference.cs file I needed to get the data from the service. Awesome. I tried several different approaches (including this really cool, but not-quite-what-I-needed open source project from a guy named Alan Juden).

OK, so we're going to access the SSRS Web Service from an intermediary RESTful Web API, then access the Web API from Angular to display... what? Oh, we need to figure out how to get the actual report (not the data, but the actual report itself) from the web service. That's possible, but it's not intuitive and it caused me a significant amount of heartburn over the course of a week. Here's the breakdown (including code).

First, we have to instantiate the service client:
   1:  HttpBindingBase httpBinding = new HttpBindingBase
   2:  {
   3:      MaxReceivedMessageSize = int.MaxValue,
   4:      Security = 
   5:          {
   6:              Mode = BasicHttpSecurityMode.TransportCredentialOnly,
   7:              Transport = 
   8:              {
   9:                  ClientCredentialType = HttpClientCredentialType.Ntlm
   10:             }
   11:         }
   12:  };
   13:  EndpointAddress endpointAddress = new EndpointAddress("http://<ssrs-server>/reportserver/reportexecution2005.asmx");
   14:  ReportExecutionServiceSoapClient client = new ReportExecutionServiceClient(httpBinding, endpointAddress);

Once we have the service client, we need to load the report. Even though this method is called LoadReport, it doesn't actually give us the report back yet. We'll do that later.
   15:  LoadReportResponse loadedReport = await client.LoadReportAsync(new TrustedUserHeader(), "path/to/report", null);

Now that we've loaded the report we can assign parameters to it. Let's say the report has a parameter called Name (because I want to keep this simple). We'll first create an instance of ParameterValue, then add that instance to our call.
   16:  ParameterValue parameter = new ParameterValue {Name = "Name", Value = "Mickey"};
   17:  await client.SetExecutionParametersAsync(loadedReport.ExecutionHeader, new TrustedUserHeader(), new[] {parameter}, "en-us");

You can add more parameters if you need to, but I'm trying to keep this simple. You can also skip adding parameters altogether if your report doesn't have any or if you just want to use whatever defaults you've setup.
Now that we've added parameters (or not, depending on the usage) we need to render the report. But it's not as simple as just calling a method named Render. No, we first have to create a RenderRequest.
   18:  RenderRequest renderRequest = new RenderRequest(loadedReport.ExecutionHeader, new TrustedUserHeader(),
         "HTML5", @"<DeviceInfo><Toolbar>False</Toolbar></DeviceInfo>");

Now that we have the RenderRequest built we can finally render the report.
   19:  RenderResponse renderResponse = await client.RenderAsync(renderRequest);

The RenderResult object has a Result property on it that is a byte array. If you wanted to get the report as a PDF for display or download or something then the byte array is probably what you want. If you retrieved the report as HTML then you probably want to convert the byte array into a string. The byte array is UTF8 encoded so you might do something like this:
   20:  byte[] bytes = renderResult.Result;
   21:  string content = Encoding.UTF8.GetString(bytes);

With all of that done, we've finally retrieved our report from the SSRS Web Service in HTML5 format (YAY!!!).

Before we move on to showing the report in Angular, I want to mention a few things about the RenderRequest parameters we used. You can get a list of available formats to request from here. You can see all the options you can specify in the DeviceInfo string here.

Now that I have the report contents in HTML I need to actually display the report to my user. The "best" (I use that term loosely here) way to do this was to put the markup into an iframe. This is where I learned that an iframe can be used without a src attribute (at least in Chrome, I'm not sure about other browsers) by setting the content explicitly. So that's what I did. It looked like this:
   1:  bindIframe(content: string) {
   2:    this.reportContent = content;
   3:    const iframe = <HTMLIFrameElement>document.getElementById('report');
   4:    iframe.contentDocument.write(content);
   5:  }

That bound the markup returned from the service into the iframe, which is exactly what I wanted, but the iframe was tiny. By default, an iframe will size itself to something like 200px by 200px. I could have set the size of the iframe in CSS, but each report had a different height that I didn't know ahead of time. With new reports being added all the time, the only approach that made sense was to resize the iframe after I bound the contents. Here's how I did that:
   1:  bindIframe(content: string) {
   2:    this.reportContent = content;
   3:    const iframe = <HTMLIFrameElement>document.getElementById('report');
   4:    iframe.contentDocument.write(content);
   5:    iframe.height = iframe.contentDocumentbody.scrollHeight.toString();
   6:    iframe.width = iframe.contentDocumentbody.scrollWidth.toString();
   5:  }

This is exactly what I needed to do! Now I'm able to display the HTML from SSRS inside an iframe in my Angular app.
  1. Display a list of all reports in SSRS
  2. Allow the user to click on a single report to view the report
  3. Allow the user to print the report
  4. Allow the user to download the report

I started clicking through the reports to view each of them and I noticed that some of them looked really... wrong. There were lines that should have been below graphs and charts that were sitting on top of those graphs and charts. Images weren't large enough. Some of the reports just looked really bad and I had no idea why.
I'll dig into what I found (and how I fixed it) in the next post. I honestly don't know when I'll get to writing it, but it's on my list.

Update: All four parts of this series are complete now.
  1. SSRS Reports in an Angular App (Part 1 - Showing the Report) (this post)
  2. SSRS Reports in an Angular App (Part 2 - Showing the Report (correctly))
  3. SSRS Reports in an Angular App (Part 3 - Printing the Report)
  4. SSRS Reports in an Angular App (Part 4 - Downloading the Report)

Thursday, May 10, 2018

Object Casting, Generic Arguments, and XUnit Comparers

A few years ago I wrote two separate posts on object casting and generic type parameters. It's hard to know for sure, but I suspect I was dealing with an issue then that came up again this week. And while those old posts did help me eventually get to the right answer, I had to do quite a bit more digging and toying to get what I needed this time. I figured it was time for an update and a more robust code sample that actually shows the whole solution.

Let me start by saying that I ultimately used this SO answer from Mariusz Pawelski to put everything together.

I also came upon this based on a previous answer I had provided on SO for equality checking in XUnit. When I wrote that answer I was only concerned with comparing the values of properties of single instances of objects. Take the Person class:
    1:  public class Person
    2:  {
    3:      public string FirstName { get; set; }
    4:  
    5:      public string LastName { get; set; }
    6:  }
I created a comparer that would evaluate whether two instances of Person had the same values for FirstName and LastName, and that worked just fine. What I needed recently was the ability to compare two lists to each other. Using the same comparer seemed to work, except that it didn't actually work. What was happening was that my comparer checked for properties, didn't find any, so returned true. I needed a way to iterate through a list of objects and compare the value of each property in each position in the first list to the value of the same property on the object in the same position of another list. That led me to this:
    1:  public class CustomEnumerableComparer<T> : IEqualityComparer<IEnumerable<T>>
    2:  {
    3:      public bool Equals(IEnumerable<T> expectedEnumerable, IEnumerable<T> actualEnumerable)
    4:      {
    5:          var expected = expectedEnumerable.ToList();
    6:          var actual = actualEnumerable.ToList();
    7:  
    8:          if (expected.Count != actual.Count)
    9:          {
   10:              throw new EqualException($"{expected.Count} items, $"{actual.Count} items");
   11:          }
   12:  
   13:          if (expected.Count == 0)
   14:          {
   15:              return true;
   16:          }
   17:  
   18:          var props = typeof(T).GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance);
   19:  
   20:          for (var i = 0; i < expected.Count; i++)
   21:          {
   22:              foreach (var prop in props)
   23:              {
   24:                  var expectedValue = prop.GetValue(expected[i], null);
   25:                  var actualValue = prop.GetValue(actual[i], null);
   26:  
   27:                  if (expectedValue == null)
   28:                  {
   29:                      if (actualValue == null)
   30:                      {
   31:                          continue;
   32:                      }
   33:  
   34:                      throw new EqualException($"A value of null for property \"{prop.Name}\"",
   35:                          $"A value of \"{actualValue}\" for property \"{prop.Name}\"");
   36:                  }
   37:  
   38:                  if (actualValue == null)
   39:                  {
   40:                      throw new EqualException($"A value of \"{expectedValue}\" for property \"{prop.Name}\"",
   41:                          $"A value of null for property \"{prop.Name}\"");
   42:                  }
   43:  
   44:                  if (!expectedValue.Equals(actualValue))
   45:                  {
   46:                      throw new EqualException($"A value of \"{expectedValue}\" for property \"{prop.Name}\"",
   47:                          $"A value of \"{actualValue}\" for property \"{prop.Name}\"");
   48:                  }
   49:              }
   50:          }
   51:  
   52:          return true;
   53:      }
   54:  
   55:      public int GetHashCode(IEnumerable<T> parameterValue)
   56:      {
   57:          return Tuple.Create(parameterValue).GetHashCode();
   58:      }
   59:  }

This works perfectly! It works perfectly for a shallow equality check between two lists, that is. So if I was just comparing two lists of Person objects I'd be fine. However, if the Person class was changed to look like this:
    1:  public class Person
    2:  {
    3:      public List<Address> Addresses { get; set; }
    4:  
    5:      public string FirstName { get; set; }
    6:  
    6:      public string LastName { get; set; }
    7:  }

where the Address class looks like this:
    1:  public class Address
    2:  {
    3:      public string City { get; set; }
    4:  
    5:      public string Street1 { get; set; }
    6:  
    6:      public string Street2 { get; set; }
    7:  }

You can see that my comparer wouldn't work as I expect (again). That's because when it encountered the Addresses property, it'd do the same comparison it did before - it would check for properties on the List<> property, find none, and return true. I needed a way to recursively check equality on nested objects. That led me to the aforementioned SO answer as well as my own previous posts.

The first thing I did was move all of the code out of Equals into a private method called AreEqual with nearly the same method signature. Here's the signature only. I'll post all of the code at the very end.
    1:  private bool AreEqual<TNested>(IEnumerable<TNested> expectedEnumerable, IEnumerable<TNested> actualEnumerable)

Now that I have that separated out, I can recursively call it (that means call the AreEqual method from within the AreEqual method). The only thing I had left to do was determine when to do a recursive call and when to do a straight equality check. Using this other answer I found on SO (I spend a lot of time there some days) I was able to include some useful extension methods to determine whether my property was an enumerable.

At this point, I can do a deep equality check on a list and recursively do deep equality checks on properties of those objects in the list that are also lists themselves, but I still have one problem: How do I invoke the AreEqual argument from within itself given that the types of the nested lists aren't likely to be the same as their containing objects? Since AreEqual accepts a generic type parameter, I can invoke it like AreEqual when I know the type at compile-time, but it's a bit trickier when I won't know the type until run-time. That's where the last bit finally comes into play. This is the piece I added right in the middle of AreEqual to recursively call AreEqual with a dynamic generic type (again, I'll post the full comparer at the end).
   44:                  if(prop.IsNonStringEnumerable())
   45:                  {
   46:                      // get the type of the object in the enumerable
   47:                      var objectType = prop.PropertyType.GetGenericArguments()[0];
   48:                      // create a List
   49:                      var genericListType = typeof(List<>).MakeGenericType(objectType);
   50:                      // instantiate lists with the values in the enumerables
   51:                      var expectedList = (IList)Activator.CreateInstance(genericListType, expectedValue);
   52:                      var actualList = (IList)Activator.CreateInstance(genericListType, actualValue);
   53:                      AreEqual((dynamic)expectedList, (dynamic)actualList);
   54:                  }

We're not using the return value of AreEqual when we call it recursively because we've nested the throw statements in there so if we encounter a situation where two values (regardless of their depth) are not equal and they should be, processing will stop immediately.

OK, that was a long way to go to learn something you probably didn't even mean to read this post about (the post kinda turned into how to compare two values in XUnit). Thanks for sticking with me. Here's the full CustomEnumerableComparer class:
    1:  public class Person
    2:  {
    3:      public string FirstName { get; set; }
    4:  
    5:      public string LastName { get; set; }
    6:  }
I created a comparer that would evaluate whether two instances of Person had the same values for FirstName and LastName, and that worked just fine. What I needed recently was the ability to compare two lists to each other. Using the same comparer seemed to work, except that it didn't actually work. What was happening was that my comparer checked for properties, didn't find any, so returned true. I needed a way to iterate through a list of objects and compare the value of each property in each position in the first list to the value of the same property on the object in the same position of another list. That led me to this:
    1:  public class CustomEnumerableComparer<T> : IEqualityComparer<IEnumerable<T>>
    2:  {
    3:      public bool Equals(IEnumerable<T> expectedEnumerable, IEnumerable<T> actualEnumerable)
    4:      {
    5:          return AreEqual(expectedEnumerable, actualEnumerable);
    6:      }
    7:  
    8:      public int GetHashCode(IEnumerable<T> parameterValue)
    9:      {
   10:          return Tuple.Create(parameterValue).GetHashCode();
   11:      }
   12:  
   13:      private bool AreEqual<TNested>(IEnumerable<TNested> expectedEnumerable, IEnumerable<TNested> actualEnumerable)
   14:      {
   15:          var expected = expectedEnumerable.ToList();
   16:          var actual = actualEnumerable.ToList();
   17:  
   18:          if (expected.Count != actual.Count)
   19:          {
   20:              throw new EqualException($"{expected.Count} items, $"{actual.Count} items");
   21:          }
   22:  
   23:          if (expected.Count == 0)
   24:          {
   25:              return true;
   26:          }
   27:  
   28:          var props = typeof(T).GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance);
   29:  
   30:          for (var i = 0; i < expected.Count; i++)
   31:          {
   32:              foreach (var prop in props)
   33:              {
   34:                  var expectedValue = prop.GetValue(expected[i], null);
   35:                  var actualValue = prop.GetValue(actual[i], null);
   36:  
   37:                  if (expectedValue == null)
   38:                  {
   39:                      if (actualValue == null)
   40:                      {
   41:                          continue;
   42:                      }
   43:  
   44:                      throw new EqualException($"A value of null for property \"{prop.Name}\"",
   45:                          $"A value of \"{actualValue}\" for property \"{prop.Name}\"");
   46:                  }
   47:  
   48:                  if (actualValue == null)
   49:                  {
   50:                      throw new EqualException($"A value of \"{expectedValue}\" for property \"{prop.Name}\"",
   51:                          $"A value of null for property \"{prop.Name}\"");
   52:                  }
   53:  
   54:                  if (prop.IsNonStringEnumerable())
   55:                  {
   56:                      // get the type of the object in the enumerable
   57:                      var objectType = prop.PropertyType.GetGenericArguments()[0];
   58:                      // create a List
   59:                      var genericListType = typeof(List<>).MakeGenericType(objectType);
   60:                      // instantiate lists with the values in the enumerables
   61:                      var expectedList = (IList)Activator.CreateInstance(genericListType, expectedValue);
   62:                      var actualList = (IList)Activator.CreateInstance(genericListType, actualValue);
   63:                      AreEqual((dynamic)expectedList, (dynamic)actualList);
   64:                  }
   65:                  else if (!expectedValue.Equals(actualValue))
   66:                  {
   67:                      throw new EqualException($"A value of \"{expectedValue}\" for property \"{prop.Name}\"",
   68:                          $"A value of \"{actualValue}\" for property \"{prop.Name}\"");
   69:                  }
   70:              }
   71:          }
   72:  
   73:          return true;
   74:      }
   75:  }