diff --git a/README_DERIVATIVE.md b/README_DERIVATIVE.md index 51fd0463..9a728be9 100644 --- a/README_DERIVATIVE.md +++ b/README_DERIVATIVE.md @@ -9,10 +9,14 @@ You will need the following development tools to build, run, and test this project: * Windows or MacOS. -* Jetbrains Rider or Visual Studio (There is most support for JetBrains dotUltimate) +* JetBrains Rider (recommended) or Visual Studio + * Note: Using JetBrains Rider is recommended since the codebase includes more comprehensive customized tooling (coding standards, live templates, etc.) + * Note: If using Visual Studio, you will need to install the additional component `.NET Compiler Platform SDK` in order to run the built-in Roslyn source generators. + * Note: if using Visual Studio, the built-in Roslyn analyzers will not work (due to .netstandard2.0 [restrictions between Visual Studio and Roslyn](https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview)) + * Install the .NET7.0 SDK (specifically version 7.0.14). Available for [download here](https://dotnet.microsoft.com/en-us/download/dotnet/7.0) -> We have ensured that you wont need any other infrastructure running on your local machine (i.e. SQLServer database), unless you want to run infrastructure specific integration tests. +> We have ensured that you won't need any other infrastructure running on your local machine (i.e., a Microsoft SQLServer database) unless you want to run infrastructure-specific integration tests. # Setup Environment diff --git a/src/Infrastructure.Web.Api.Interfaces/RouteAttribute.cs b/src/Infrastructure.Web.Api.Interfaces/RouteAttribute.cs index 3af33618..066ab9f0 100644 --- a/src/Infrastructure.Web.Api.Interfaces/RouteAttribute.cs +++ b/src/Infrastructure.Web.Api.Interfaces/RouteAttribute.cs @@ -1,15 +1,21 @@ using System.ComponentModel; +#if !NETSTANDARD2_0 using System.Diagnostics.CodeAnalysis; +#endif namespace Infrastructure.Web.Api.Interfaces; /// -/// Provides a declarative way to define a REST route and service operation +/// Provides a declarative way to define a REST route service operation, and configuration /// [AttributeUsage(AttributeTargets.Class, Inherited = false)] public class RouteAttribute : Attribute { - public RouteAttribute([StringSyntax("Route")] string routeTemplate, ServiceOperation operation, + public RouteAttribute( +#if !NETSTANDARD2_0 + [StringSyntax("Route")] +#endif + string routeTemplate, ServiceOperation operation, AccessType access = AccessType.Anonymous, bool isTestingOnly = false) { if (!Enum.IsDefined(typeof(ServiceOperation), operation)) @@ -23,10 +29,10 @@ public RouteAttribute([StringSyntax("Route")] string routeTemplate, ServiceOpera IsTestingOnly = isTestingOnly; } - public bool IsTestingOnly { get; } - public AccessType Access { get; } + public bool IsTestingOnly { get; } + public ServiceOperation Operation { get; } public string RouteTemplate { get; } diff --git a/src/Tools.Generators.WebApi.UnitTests/MinimalApiMediatRGeneratorSpec.cs b/src/Tools.Generators.WebApi.UnitTests/MinimalApiMediatRGeneratorSpec.cs index 451668f3..73e5d31a 100644 --- a/src/Tools.Generators.WebApi.UnitTests/MinimalApiMediatRGeneratorSpec.cs +++ b/src/Tools.Generators.WebApi.UnitTests/MinimalApiMediatRGeneratorSpec.cs @@ -5,42 +5,47 @@ using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Xunit; -using WebApi_MinimalApiMediatRGenerator = Generators::Tools.Generators.WebApi.MinimalApiMediatRGenerator; +using MinimalApiMediatRGenerator = Generators::Tools.Generators.WebApi.MinimalApiMediatRGenerator; namespace Tools.Generators.WebApi.UnitTests; [UsedImplicitly] public class MinimalApiMediatRGeneratorSpec { + private static readonly string[] + AdditionalCompilationAssemblies = + { "System.Runtime.dll", "netstandard.dll" }; //HACK: required to analyze custom attributes + private static CSharpCompilation CreateCompilation(string sourceCode) { var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location)!; + var references = new List + { + MetadataReference.CreateFromFile(typeof(MinimalApiMediatRGenerator).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location) + }; + AdditionalCompilationAssemblies.ToList() + .ForEach(item => references.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, item)))); var compilation = CSharpCompilation.Create("compilation", new[] { CSharpSyntaxTree.ParseText(sourceCode) }, - new[] - { - MetadataReference.CreateFromFile(typeof(WebApi_MinimalApiMediatRGenerator).Assembly.Location), - MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location), - MetadataReference.CreateFromFile(Path.Combine(assemblyPath, - "System.Runtime.dll")) //HACK: this is required to make custom attributes work - }, + references, new CSharpCompilationOptions(OutputKind.ConsoleApplication)); return compilation; } [Trait("Category", "Unit")] - public class GivenAServiceCLass + public class GivenAServiceClass { private GeneratorDriver _driver; - public GivenAServiceCLass() + public GivenAServiceClass() { - var generator = new WebApi_MinimalApiMediatRGenerator(); + var generator = new MinimalApiMediatRGenerator(); _driver = CSharpGeneratorDriver.Create(generator); } diff --git a/src/Tools.Generators.WebApi.UnitTests/WebApiAssemblyVisitorSpec.cs b/src/Tools.Generators.WebApi.UnitTests/WebApiAssemblyVisitorSpec.cs index c648ba27..394c1567 100644 --- a/src/Tools.Generators.WebApi.UnitTests/WebApiAssemblyVisitorSpec.cs +++ b/src/Tools.Generators.WebApi.UnitTests/WebApiAssemblyVisitorSpec.cs @@ -16,23 +16,26 @@ namespace Tools.Generators.WebApi.UnitTests; public class WebApiAssemblyVisitorSpec { private const string CompilationSourceCode = ""; + private static readonly string[] + AdditionalCompilationAssemblies = + { "System.Runtime.dll", "netstandard.dll" }; //HACK: required to analyze use custom attributes private static CSharpCompilation CreateCompilation(string sourceCode) { var assemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location)!; - + var references = new List + { + MetadataReference.CreateFromFile(typeof(WebApiAssemblyVisitor).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location) + }; + AdditionalCompilationAssemblies.ToList() + .ForEach(item => references.Add(MetadataReference.CreateFromFile(Path.Combine(assemblyPath, item)))); var compilation = CSharpCompilation.Create("compilation", new[] { CSharpSyntaxTree.ParseText(sourceCode) }, - new[] - { - MetadataReference.CreateFromFile(typeof(WebApiAssemblyVisitor).Assembly.Location), - MetadataReference.CreateFromFile(typeof(Binder).GetTypeInfo().Assembly.Location), - MetadataReference.CreateFromFile(Path.Combine(assemblyPath, - "System.Runtime.dll")) //HACK: this is required to make custom attributes work - }, + references, new CSharpCompilationOptions(OutputKind.ConsoleApplication)); return compilation; @@ -371,7 +374,7 @@ namespace ANamespace; public class AResponse : IWebResponse { } - [Route("aroute", ServiceOperation.Get)] + [Infrastructure.Web.Api.Interfaces.RouteAttribute("aroute", ServiceOperation.Get)] public class ARequest : IWebRequest { } diff --git a/src/Tools.Generators.WebApi/Extensions/StringExtensions.cs b/src/Tools.Generators.WebApi/Extensions/StringExtensions.cs index 6d403682..88e46b00 100644 --- a/src/Tools.Generators.WebApi/Extensions/StringExtensions.cs +++ b/src/Tools.Generators.WebApi/Extensions/StringExtensions.cs @@ -5,7 +5,7 @@ public static class StringExtensions /// /// Whether the string value contains no value: it is either: null, empty or only whitespaces /// - public static bool HasNoValue(this string? value) + public static bool HasNoValue(this string value) { return string.IsNullOrEmpty(value) || string.IsNullOrWhiteSpace(value); } @@ -13,7 +13,7 @@ public static bool HasNoValue(this string? value) /// /// Whether the string value contains any value except: null, empty or only whitespaces /// - public static bool HasValue(this string? value) + public static bool HasValue(this string value) { return !string.IsNullOrEmpty(value) && !string.IsNullOrWhiteSpace(value); } diff --git a/src/Tools.Generators.WebApi/Extensions/SymbolExtensions.cs b/src/Tools.Generators.WebApi/Extensions/SymbolExtensions.cs index a241c562..5dce9af1 100644 --- a/src/Tools.Generators.WebApi/Extensions/SymbolExtensions.cs +++ b/src/Tools.Generators.WebApi/Extensions/SymbolExtensions.cs @@ -5,13 +5,13 @@ namespace Tools.Generators.WebApi.Extensions; public static class SymbolExtensions { - public static AttributeData? GetAttribute(this ISymbol symbol, INamedTypeSymbol attributeType) + public static AttributeData GetAttribute(this ISymbol symbol, INamedTypeSymbol attributeType) { return symbol.GetAttributes() .FirstOrDefault(attribute => attribute.AttributeClass!.IsOfType(attributeType)); } - public static INamedTypeSymbol? GetBaseType(this ITypeSymbol symbol, INamedTypeSymbol baseType) + public static INamedTypeSymbol GetBaseType(this ITypeSymbol symbol, INamedTypeSymbol baseType) { return symbol.AllInterfaces.FirstOrDefault(@interface => @interface.IsOfType(baseType)); } @@ -45,7 +45,7 @@ public static IEnumerable GetUsingNamespaces(this INamedTypeSymbol symbo return usingSyntaxes.Select(us => us.Name!.ToString()) .Distinct() - .OrderDescending() + .OrderByDescending(s => s) .ToList(); } @@ -61,7 +61,6 @@ public static bool IsConcreteInstanceClass(this INamedTypeSymbol symbol) public static bool IsDerivedFrom(this ITypeSymbol symbol, INamedTypeSymbol baseType) { - ArgumentNullException.ThrowIfNull(baseType); return symbol.AllInterfaces.Any(@interface => @interface.IsOfType(baseType)); } diff --git a/src/Tools.Generators.WebApi/MinimalApiMediatRGenerator.cs b/src/Tools.Generators.WebApi/MinimalApiMediatRGenerator.cs index 5792d31b..83031411 100644 --- a/src/Tools.Generators.WebApi/MinimalApiMediatRGenerator.cs +++ b/src/Tools.Generators.WebApi/MinimalApiMediatRGenerator.cs @@ -54,7 +54,7 @@ public void Execute(GeneratorExecutionContext context) return; - static string BuildFile(string? assemblyNamespace, string allUsingNamespaces, string allEndpointRegistrations, + static string BuildFile(string assemblyNamespace, string allUsingNamespaces, string allEndpointRegistrations, string allHandlerClasses) { return $@"// @@ -221,7 +221,7 @@ private static void BuildHandlerClasses( handlerClasses.AppendLine(); } - private static string BuildInjectorConstructorAndFields(string? handlerClassName, + private static string BuildInjectorConstructorAndFields(string handlerClassName, List constructors) { var handlerClassConstructorAndFields = new StringBuilder(); diff --git a/src/Tools.Generators.WebApi/Tools.Generators.WebApi.csproj b/src/Tools.Generators.WebApi/Tools.Generators.WebApi.csproj index b33ec330..c18c0328 100644 --- a/src/Tools.Generators.WebApi/Tools.Generators.WebApi.csproj +++ b/src/Tools.Generators.WebApi/Tools.Generators.WebApi.csproj @@ -1,7 +1,9 @@ - net7.0 + netstandard2.0 + latest + disable true true true diff --git a/src/Tools.Generators.WebApi/WebApiAssemblyVisitor.cs b/src/Tools.Generators.WebApi/WebApiAssemblyVisitor.cs index cc093ab7..faa2f1ce 100644 --- a/src/Tools.Generators.WebApi/WebApiAssemblyVisitor.cs +++ b/src/Tools.Generators.WebApi/WebApiAssemblyVisitor.cs @@ -21,7 +21,7 @@ namespace Tools.Generators.WebApi; public class WebApiAssemblyVisitor : SymbolVisitor { internal static readonly string[] IgnoredNamespaces = - { "System", "Microsoft", "MediatR", "MessagePack", "NerdBank*" }; + ["System", "Microsoft", "MediatR", "MessagePack", "NerdBank*"]; private readonly CancellationToken _cancellationToken; private readonly INamedTypeSymbol _cancellationTokenSymbol; @@ -205,24 +205,24 @@ TypeName GetServiceName() return new TypeName(symbol.ContainingNamespace.ToDisplayString(), symbol.Name); } - static ServiceOperation FromOperationVerb(string? operation) + static ServiceOperation FromOperationVerb(string operation) { if (operation is null) { return ServiceOperation.Get; } - return Enum.Parse(operation, true); + return (ServiceOperation)Enum.Parse(typeof(ServiceOperation), operation, true); } - static AccessType FromAccessType(string? access) + static AccessType FromAccessType(string access) { if (access is null) { return AccessType.Anonymous; } - return Enum.Parse(access, true); + return (AccessType)Enum.Parse(typeof(AccessType), access, true); } // We assume that the request type derives from IWebRequest @@ -336,7 +336,7 @@ bool HasWrongSetOfParameters(IMethodSymbol method) } // We assume that the request DTO it is decorated with a RouteAttribute - bool HasRouteAttribute(IMethodSymbol method, out AttributeData? routeAttribute) + bool HasRouteAttribute(IMethodSymbol method, out AttributeData routeAttribute) { var parameters = method.Parameters; if (parameters.Length == 0) @@ -353,35 +353,33 @@ bool HasRouteAttribute(IMethodSymbol method, out AttributeData? routeAttribute) public record ServiceOperationRegistration { - public required ApiServiceClassRegistration Class { get; init; } + public ApiServiceClassRegistration Class { get; set; } - public required bool HasCancellationToken { get; init; } + public bool HasCancellationToken { get; set; } - public required bool IsAsync { get; init; } + public bool IsAsync { get; set; } - public required bool IsTestingOnly { get; init; } + public bool IsTestingOnly { get; set; } - public string? MethodBody { get; set; } + public string MethodBody { get; set; } - public required string MethodName { get; init; } + public string MethodName { get; set; } - public required AccessType OperationAccess { get; init; } + public AccessType OperationAccess { get; set; } - public required ServiceOperation OperationType { get; init; } + public ServiceOperation OperationType { get; set; } - public required TypeName RequestDtoType { get; init; } + public TypeName RequestDtoType { get; set; } - public required TypeName ResponseDtoType { get; init; } + public TypeName ResponseDtoType { get; set; } - public required string RoutePath { get; init; } + public string RoutePath { get; set; } } public record TypeName { public TypeName(string @namespace, string name) { - ArgumentException.ThrowIfNullOrEmpty(@namespace); - ArgumentException.ThrowIfNullOrEmpty(name); Namespace = @namespace; Name = name; } @@ -392,7 +390,7 @@ public TypeName(string @namespace, string name) public string Namespace { get; } - public virtual bool Equals(TypeName? other) + public virtual bool Equals(TypeName other) { if (ReferenceEquals(null, other)) { @@ -409,32 +407,39 @@ public virtual bool Equals(TypeName? other) public override int GetHashCode() { +#if NETSTANDARD2_0 + var hash = 17; + hash = hash * 23 + Namespace.GetHashCode(); + hash = hash * 23 + Name.GetHashCode(); + return hash; +#else return HashCode.Combine(Namespace, Name); +#endif } } public record ApiServiceClassRegistration { - public IEnumerable Constructors { get; init; } = new List(); + public IEnumerable Constructors { get; set; } = new List(); - public required TypeName TypeName { get; init; } + public TypeName TypeName { get; set; } - public IEnumerable UsingNamespaces { get; init; } = new List(); + public IEnumerable UsingNamespaces { get; set; } = new List(); } public record Constructor { - public IEnumerable CtorParameters { get; init; } = new List(); + public IEnumerable CtorParameters { get; set; } = new List(); - public required bool IsInjectionCtor { get; init; } + public bool IsInjectionCtor { get; set; } - public string? MethodBody { get; set; } + public string MethodBody { get; set; } } public record ConstructorParameter { - public required TypeName TypeName { get; init; } + public TypeName TypeName { get; set; } - public required string VariableName { get; init; } + public string VariableName { get; set; } } } \ No newline at end of file