Servicestack + Miniprofiler + dotnetcore + Angular2+

I’m a big fan of Servicestack framework, and recently was trying to implement the MiniProfiler profiling framework so that I could diagnose some poor-performing pages remotely on a staging server that I had limited access to.

Unfortunately, the existing ServiceStack.Miniprofiler is available for .NET Framework, not dotnetcore, so I have been playing around to try to get it working, inspired largely by:

https://stackoverflow.com/questions/38249465/miniprofiler-how-do-i-profile-an-angularjs-webapi-app

and

https://blog.dangl.me/archive/using-the-stackexchange-miniprofiler-with-an-angular-single-page-application/

I’m happy to announce that I’ve gotten this working, and my poor performance issues solved!

 

For anyone interested, to get MiniProfiler.Core working in ServiceStack, follow these steps:

Install-Package MiniProfiler.AspNetCore.Mvc -IncludePrerelease

To configure Miniprofiler to serve the required include.js, include.tmpl etc files via MVC, in my Startup.cs file, I made a private property in the Startup class (which defines the name of the Cors policy that MVC will use and which will allow all headers, such as the X-Miniprofiler-Ids headers – you can play around with these settings to make your policy more restrictive as required):

 /// <summary>
 /// Label to identify a particularly open and permissive CORS policy, which came into play
 /// during implementation of the MiniProfiler debugging tools
 /// </summary>
 private readonly string VeryPermissiveCorsPolicyName = "AllowAllCors";

Then I add the following section to Startup.ConfigureServices(IServiceCollection services):

#region MiniProfiler Configuration

if (MyAppConfig.IsProfilingEnabled)
{
	// To enable the miniprofiler, not used by SS
	services.AddMemoryCache();

	// Note .AddMiniProfiler() returns a IMiniProfilerBuilder for easy intellisense
	services.AddMiniProfiler(options =>
	{
		// All of this is optional. You can simply call .AddMiniProfiler() for all defaults

		// (Optional) Path to use for profiler URLs, default is /mini-profiler-resources
		//options.RouteBasePath = "/profiler";

		// (Optional) Control storage
		// (default is 30 minutes in MemoryCacheStorage)
		(options.Storage as MemoryCacheStorage).CacheDuration = TimeSpan.FromMinutes(60);

		// (Optional) Control which SQL formatter to use, InlineFormatter is the default
		options.SqlFormatter = new StackExchange.Profiling.SqlFormatters.InlineFormatter();

                // (Optional) To control authorization, you can use the Func<HttpRequest, bool> options: 
                // (default is everyone can access profilers) 
                options.ResultsAuthorize = IsUserAllowedToSeeMiniProfilerUI; 
                options.ResultsListAuthorize = IsUserAllowedToSeeMiniProfilerUI;
		/*
		// (Optional)  To control which requests are profiled, use the Func<HttpRequest, bool> option:
		// (default is everything should be profiled)
		options.ShouldProfile = request => MyShouldThisBeProfiledFunction(request);

		// (Optional) Profiles are stored under a user ID, function to get it:
		// (default is null, since above methods don't use it by default)
		options.UserIdProvider = request => MyGetUserIdFunction(request);

		// (Optional) Swap out the entire profiler provider, if you want
		// (default handles async and works fine for almost all appliations)
		options.ProfilerProvider = new MyProfilerProvider();

		// (Optional) You can disable "Connection Open()", "Connection Close()" (and async variant) tracking.
		// (defaults to true, and connection opening/closing is tracked)
		options.TrackConnectionOpenClose = true;
		*/
	});

	services.AddCors(options =>
	{
		options.AddPolicy(VeryPermissiveCorsPolicyName,
			builder => builder.AllowAnyOrigin()
				.AllowAnyMethod()
				.AllowAnyHeader()
				.AllowCredentials());
	});
}

#endregion MiniProfiler Configuration

Add the method to perform authchecks to make sure that we only show miniprofiler info to admin-type accounts:

/// <summary>
/// Always prevent non-admin users from seeing the Miniprofiler results, 
/// including the list view at ~/mini-profiler-resources/results.
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private bool IsUserAllowedToSeeMiniProfilerUI(HttpRequest request)
{
	var sessionId = request.Cookies[Keywords.SessionId];
	if (string.IsNullOrEmpty(sessionId)) return false;
	var sessionKey = SessionFeature.GetSessionKey(sessionId);
	var cache = HostContext.TryResolve<ICacheClient>();
	var userSession = cache.Get<IAuthSession>(sessionKey);
	if (userSession == null || !userSession.IsAuthenticated) return false;
	if (userSession.Roles.Contains(SystemRolesEnum.ADMIN.ToString())) return true;
	if (userSession.Roles.Contains(SystemRolesEnum.SYSADMIN.ToString())) return true;
	return false;
}

Then I add MiniProfiler config to the public void Configure(IApplicationBuilder app, IHostingEnvironment env) method:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // do some stuff

    if (MyAppConfig.IsProfilingEnabled)
    {
        // Global CORS policy - we had to set this here to get the MVC/miniprofiler headers implemented. Can possibly tidy up in the long term.
        app.UseCors(VeryPermissiveCorsPolicyName);
        app.UseMiniProfiler();
    }

    // do any other stuff
}

 

I configure my DB connection to be profiled:

container.Register<IDbConnectionFactory>(
    new OrmLiteConnectionFactory(connectionString, SqlServer2012OrmLiteDialectProvider.Instance)
 {
    ConnectionFilter = x => new ProfiledDbConnection(x as DbConnection, MiniProfiler.Current)
 });

 

Then, moving to the front-end, I need to do two things. Because there is not yet currently an Angular component to represent the Miniprofiler script, and because I haven’t had time to attempt this yet, I add the content (to the index.html file before the Angular2 app tag is placed, as the profiler must be attached to the window object to work properly at runtime) that would normally be rendered out by the MVC RenderHelpers… (note the src points to where your backend MVC route serving MiniProfiler,  as this must match the value if customized, e.g. options.RouteBasePath = “/profiler”; above) :

<script async type="text/javascript" id="mini-profiler" 
    src="./mini-profiler-resources/includes.js?" 
    data-version="" 
    data-path="./mini-profiler-resources/"
    data-current-id=""
    data-ids=""
    data-position="right"
    data-trivial="true"
    data-children="true"
    data-max-traces="35"
    data-controls="true"
    data-authorized="true"
    data-toggle-shortcut="Alt+P"
    data-start-hidden="false"
    data-trivial-milliseconds="5">
</script>

 

Finally, because our solution uses the Servicestack JsonServiceClient, not the default Angular $http tools, I use a service client and define it with a ResponseFilter set up to look for the existance of MiniProfilerId headers:

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { environment } from '../../environments/environment';
import { JsonServiceClient, IReturn } from '@servicestack/client';

@Injectable()
export class ServiceStackService {
  
  public client: JsonServiceClient;
  
  // Where to find ids of profile information in headers
  private miniProfilerHeadersKey = 'X-MiniProfiler-Ids';

  // Name of the MiniProfiler object that gets attached to the window
  private miniProfiler = window['MiniProfiler'];

  constructor(
  ) {
    this.client = new JsonServiceClient(environment.apiUrl);
    if (environment.showMiniProfilerResults) {
      if (this.miniProfiler) {
        this.client.responseFilter = this.showMiniProfiler;    
      }
      else {
        console.warn('No miniprofiler detected! We expect it to be attached to the window by a script');
      }
    }
  }

  /**
   * Query the api for debugging diagnostics information about how long each operation is taking
   */
  private showMiniProfiler(response: Response) {

    if (response.headers.has(this.miniProfilerHeadersKey)) {

      const miniprofilerIdString: string = response.headers.get(this.miniProfilerHeadersKey);
      const miniprofilerIds = JSON.parse(miniprofilerIdString) as string[];

      if (this.miniProfiler) {
        this.miniProfiler.fetchResults(miniprofilerIds);
      }
    }
  }
}

dale.holborow

Leave a Reply

Your email address will not be published. Required fields are marked *