Adds a
Diagnostic
that finds a MEF
ImportingConstructor
with content that’s not entirely wrapped in a try/catch statement.
Provides a
Code Fix
to handle the problem.
The
Code Fix
will:
Add a try/catch statement around the entire content
Add
ErrorNotificationLogger.LogErrorWithoutShowingErrorNotificationUI(“Error in MEF ctor”, e);
inside the catch.
Add a
using
statement that for the static class
ErrorNotificationLogger
The analyzer code is available on
GitHub
, but if you’re interested in the explanation, we’ll see how this sort of analyzer can be created.
Getting Started
If you never created Roslyn Analyzers before, you might want to read the getting started
tutorial
first. If you never worked with
Roslyn
before, I suggest first starting with Josh Varty’s
tutorials
Start with the regular Roslyn Analyzer template in File | New Project | C# | Extensibility | Analyzer with Code Fix (NuGet + VSIX) template.
Each analyzer consists of a
Diagnostic
and a
CodeFix
.
The
Diagnostic
in our case will find constructors with the
ImportingConstructor
attribute and mark them as Error.
The
CodeFix
will wrap the code in the constructor with try/catch.
The Diagnostic
[DiagnosticAnalyzer(LanguageNames.CSharp)]publicclassMefImportExceptionAnalyzerAnalyzer : DiagnosticAnalyzer
publicconststring DiagnosticId = "MefImportExceptionAnalyzer";
privatestaticreadonly LocalizableString Title = "MEF Import exception Logger";
privatestaticreadonly LocalizableString MessageFormat = "There's a MEF ImportingConstructor without a try..catch block .";
privatestaticreadonly LocalizableString Description = "All MEF ImportingConstructor should have a try..catch on entire content.";
privateconststring Category = "MEF";
privatestatic DiagnosticDescriptor Rule =
new DiagnosticDescriptor(DiagnosticId, Title, MessageFormat,
Category, DiagnosticSeverity.Error, isEnabledByDefault: true,
description: Description);
publicoverride ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{ get { return ImmutableArray.Create(Rule); } }
publicoverridevoid Initialize(AnalysisContext context)
context.RegisterSyntaxNodeAction(AnalyzeConstructor,
ImmutableArray.Create(SyntaxKind.ConstructorDeclaration));
privatevoid AnalyzeConstructor(SyntaxNodeAnalysisContext context)
var ctor = (ConstructorDeclarationSyntax)context.Node;
bool isDiagnosticNeeded = IsDiagNeeded(ctor);
if (isDiagnosticNeeded)
var diag = Diagnostic.Create(Rule, ctor.GetLocation());
context.ReportDiagnostic(diag);
privatebool IsDiagNeeded(ConstructorDeclarationSyntax ctor)
bool isAttributeExists = IsImportingAttributeExists(ctor);
if (!isAttributeExists)
returnfalse;
bool isWhiteSpaceOnly = IsWhiteSpaceOnly(ctor);
if (isWhiteSpaceOnly)
returnfalse;
bool tryCatchOnAllExists = IsTryCatchStatementOnly(ctor);
if (tryCatchOnAllExists)
returnfalse;
returntrue;
privatestaticbool IsTryCatchStatementOnly(
ConstructorDeclarationSyntax ctor)
var statements = ctor.Body.Statements;
return statements.Count == 1
&& statements[0] is TryStatementSyntax;
privatestaticbool IsWhiteSpaceOnly(ConstructorDeclarationSyntax ctor)
return ctor.Body.Statements.Count == 0;
privatestaticbool IsImportingAttributeExists(
ConstructorDeclarationSyntax ctor)
var attrs = ctor.AttributeLists.SelectMany(list => list.Attributes);
return attrs.Any(attr => attr.Name.ToString() == "ImportingConstructor");
Here’s what happens here:
In
Initialize
method we register an Action on any
ConstructorDeclaration
.
In AnalyzeConstructor we check 3 conditions:
if the constructor has the
ImportingConstructor
attribute.
should be true
If the body of the constructor is white space only .
should be false
If the entire body is wrapped in a single try/catch statement.
should be false
If the 3 conditions are met, then we call
ReportDiagnostic
.
The Code Fix
The Code Fix is a bit more complicated. We’ll see it in parts (the entire file can be seen
here
on GitHub)
The code fix starts with:
[Microsoft.CodeAnalysis.CodeFixes.ExportCodeFixProvider(
Microsoft.CodeAnalysis.LanguageNames.CSharp,
Name = nameof(MefImportExceptionAnalyzerCodeFixProvider)), Shared]publicclassMefImportExceptionAnalyzerCodeFixProvider : CodeFixProvider
privateconststring title = "Add try.. catch inside";
privateconststring ERROR_NOTIFICATION_NAMESPACE = "DebuggerShared.Services.ErrorNotification";
privateconststring SYSTEM_NAMESPACE = "System";
publicsealedoverride ImmutableArray<string> FixableDiagnosticIds
get { return
ImmutableArray.Create(MefImportExceptionAnalyzerAnalyzer.DiagnosticId);
publicsealedoverride FixAllProvider GetFixAllProvider()
return WellKnownFixAllProviders.BatchFixer;
publicsealedoverrideasync Task RegisterCodeFixesAsync(
CodeFixContext context)
var root =
await context.Document.GetSyntaxRootAsync(context.CancellationToken)
.ConfigureAwait(false);
var diagnostic = context.Diagnostics.First();
var diagnosticSpan = diagnostic.Location.SourceSpan;
var initialToken = root.FindToken(diagnosticSpan.Start);
var ctor =
FindAncestorOfType<ConstructorDeclarationSyntax>(initialToken.Parent);
context.RegisterCodeFix(
CodeAction.Create(title, c => ChangeBlock(context.Document, ctor, c),
equivalenceKey: title),
diagnostic);
private T FindAncestorOfType<T>(SyntaxNode node) where T : SyntaxNode
if (node == null)
returnnull;
if (node is T)
return node as T;
return FindAncestorOfType<T>(node.Parent);
Explanation:
According to
FixableDiagnosticIds
, the code fix will run only for our specific Diagnostic.
RegisterCodeFixesAsync
will run for each found Diagnostic. It will:
Find the diagnostic span
Register a code fix with a createChangedDocument function
c => ChangeBlock(context.Document, ctor, c)
where ‘c’ is a CancellationToken
This is it for the
boilerplate
part of the analyzer. The next part is code manipulation with Roslyn, where we will transform any constructor to be wrapped in a
try/catch
statement. This will be done in the
ChangeBlock
method next.
privateasync Task<Document> ChangeBlock(Document document, ConstructorDeclarationSyntax originalCtor, CancellationToken c)
ConstructorDeclarationSyntax newCtor = CreateConstructorWithTryCatch(originalCtor);
var root = await GetRootWithNormalizedConstructor(document, originalCtor, newCtor).ConfigureAwait(false);
root = AddNamespaceIfMissing(root, ERROR_NOTIFICATION_NAMESPACE);
root = AddNamespaceIfMissing(root, SYSTEM_NAMESPACE);
return document.WithSyntaxRoot(root);
privatestatic ConstructorDeclarationSyntax CreateConstructorWithTryCatch(ConstructorDeclarationSyntax originalCtor)
var originalBlock = originalCtor.Body;
var newCtor = originalCtor.WithBody(
Block(
TryStatement(
SingletonList<CatchClauseSyntax>(
CatchClause()
.WithDeclaration(
CatchDeclaration(
IdentifierName("Exception"))
.WithIdentifier(
Identifier("e")))
.WithBlock(
Block(
SingletonList<StatementSyntax>(
ExpressionStatement(
InvocationExpression(
MemberAccessExpression(
SyntaxKind.SimpleMemberAccessExpression,
IdentifierName("ErrorNotificationLogger"),
IdentifierName("LogErrorWithoutShowingErrorNotificationUI")))
.WithArgumentList(
ArgumentList(
SeparatedList<ArgumentSyntax>(
new SyntaxNodeOrToken[]{
Argument(
LiteralExpression(
SyntaxKind.StringLiteralExpression,
Literal("Error in MEF ctor"))),
Token(SyntaxKind.CommaToken),
Argument(
IdentifierName("e"))})))))))))
.WithBlock(originalBlock))).NormalizeWhitespace();
return newCtor;
privatestaticasync Task<CompilationUnitSyntax> GetRootWithNormalizedConstructor(Document document, ConstructorDeclarationSyntax originalCtor, ConstructorDeclarationSyntax newCtor)
var tree = await document.GetSyntaxTreeAsync().ConfigureAwait(false);
CompilationUnitSyntax root = await tree.GetRootAsync() as CompilationUnitSyntax;
root = root.ReplaceNode(originalCtor, newCtor);
var entirelyNormalizedRoot = root.NormalizeWhitespace();
ConstructorDeclarationSyntax ctorInEntirelyNormalized = FindSpecificConstructor(originalCtor.ParameterList, originalCtor.Identifier.Text, entirelyNormalizedRoot);
var ctorInOrig2 = FindSpecificConstructor(originalCtor.ParameterList, originalCtor.Identifier.Text, root);
ctorInEntirelyNormalized = ctorInEntirelyNormalized.WithParameterList(originalCtor.ParameterList);
ctorInEntirelyNormalized = ctorInEntirelyNormalized.WithAttributeLists(originalCtor.AttributeLists);
var newRoot = root.ReplaceNode(ctorInOrig2, ctorInEntirelyNormalized);
return newRoot;
privatestatic ConstructorDeclarationSyntax FindSpecificConstructor(ParameterListSyntax paramList, string identifierText, CompilationUnitSyntax parentNode)
var res = parentNode.DescendantNodes().
OfType<ConstructorDeclarationSyntax>()
.SingleOrDefault(c => c.Identifier.Text == identifierText
&& IsParamListEqual(c.ParameterList, paramList)
&& !c.Modifiers.Any(x => x.IsKind(SyntaxKind.StaticKeyword)));
return res;
privatestaticbool IsParamListEqual(
ParameterListSyntax paramsA, ParameterListSyntax paramsB)
if (paramsA == null || paramsB == null)
returnfalse;
var parametersA = paramsA.Parameters;
var parametersB = paramsB.Parameters;
if (parametersA == null
|| parametersB == null
|| parametersA.Count != parametersB.Count)
returnfalse;
for (int i = 0; i < parametersA.Count; i++)
var a = Regex.Replace(parametersA[i].ToString(), @"\s+", "");
var b = Regex.Replace(parametersB[i].ToString(), @"\s+", "");
if (a != b)
returnfalse;
returntrue;
private CompilationUnitSyntax AddNamespaceIfMissing(
CompilationUnitSyntax root, string namespaceIdentifyer)
var ns = root.DescendantNodesAndSelf()
.OfType<UsingDirectiveSyntax>()
.FirstOrDefault(elem => elem.Name.ToString() == namespaceIdentifyer);
if (ns != null)
return root;
var usingDirective =
SyntaxFactory.UsingDirective(SyntaxFactory.IdentifierName(namespaceIdentifyer))
.WithTrailingTrivia(SyntaxFactory.EndOfLine("\r\n"));
var lastUsing = root.DescendantNodesAndSelf()
.OfType<UsingDirectiveSyntax>().Last();
root = root.InsertNodesAfter(lastUsing, new[] { usingDirective });
return root;
Explanation:
We create a new constructor with the desired
try/catch
statement. We can see in the bottom of
CreateConstructorWithTryCatch
that the Body of
try
is the body of the previous constructor.
You can easily generate such code with Kiril Osenkov’s
Roslyn Quoter
We replace the old constructor with the new constructor with
var newRoot = root.ReplaceNode(ctorInOrig2, ctorInEntirelyNormalized);
The entire following code is to add using statements. This was needed because for
Sysetm.Exception
and for my own static class
ErrorNotificationLogger
.
We basically find the last
using
statement, and insert a new
using
statement afterward.
For more tutorials on
Roslyn
, you can start with Josh Varty’s
tutorials
Welcome to my blog! I’m a software developer, C# enthusiast, author, and a blogger. I write about C#, .NET, memory management, and performance. Working at Microsoft, but all opinions in this blog are my own.
More about me →
Check out my book
Practical Debugging for .NET Developers
to become an expert problem solver