diff --git a/.editorconfig b/.editorconfig
index 4e52df2..f555214 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -9,6 +9,17 @@ tab_width = 4
indent_size = 4
charset = utf-8
+# Build scripts
+[*.{yml,yaml}]
+indent_style = spaces
+indent_size = 2
+
+# XML project files
+[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
+indent_size = 2
+
+# Code files
+[*.cs]
### Naming rules: ###
@@ -52,8 +63,7 @@ dotnet_naming_rule.static_fields_should_be_pascal_case.style = static_field_styl
dotnet_naming_symbols.static_fields.applicable_kinds = field
dotnet_naming_symbols.static_fields.required_modifiers = static
-dotnet_naming_style.static_field_style.capitalization = camel_case
-dotnet_naming_style.static_field_style.required_prefix = s_
+dotnet_naming_style.static_field_style.capitalization = pascal_case
# Instance fields are camelCase and start with _
dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion
@@ -150,20 +160,6 @@ dotnet_style_readonly_field = true:warning
# New-line preferences
dotnet_style_allow_multiple_blank_lines_experimental = false:warning
dotnet_style_allow_statement_immediately_after_block_experimental = false:warning
-csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = false:warning
-csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = false:warning
-
-# Build scripts
-[*.{yml,yaml}]
-indent_style = spaces
-indent_size = 2
-
-# XML project files
-[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
-indent_size = 2
-
-# Code files
-[*.cs]
## C# style settings:
@@ -195,7 +191,7 @@ csharp_style_expression_bodied_constructors = false:none
csharp_style_expression_bodied_operators = false:none
# Prefer local method constructs to have a block body
-csharp_style_expression_bodied_local_functions = true:suggestion
+csharp_style_expression_bodied_local_functions = when_on_single_line:suggestion
# Prefer property-like constructs to have an expression-body
csharp_style_expression_bodied_properties = true:suggestion
@@ -288,16 +284,17 @@ dotnet_diagnostic.CS1998.severity = none # CS1998: Async method l
dotnet_diagnostic.CS4014.severity = error # CS4014: Because this call is not awaited, execution of the current method continues before the call is completed
dotnet_diagnostic.CA2007.severity = none # CA2007: Consider calling ConfigureAwait on the awaited task
-# Immediate.Handlers relies on nested types
-dotnet_diagnostic.CA1034.severity = none # CA1034: Nested types should not be visible
-
# No need for cryptographically secure anything in this project
dotnet_diagnostic.CA5394.severity = none # CA5394: Random is an insecure random number generator
# Dispose things need disposing
dotnet_diagnostic.CA2000.severity = error # CA2000: Dispose objects before losing scope
+dotnet_diagnostic.CA1034.severity = none # CA1034: Nested types should not be visible
dotnet_diagnostic.CA1515.severity = none # CA1515: Consider making public types internal
+dotnet_diagnostic.CA1708.severity = none # CA1708: Identifiers should differ by more than case
+dotnet_diagnostic.CA1716.severity = none # CA1716: Identifiers should not match keywords
+dotnet_diagnostic.IDE0390.severity = none # IDE0390: Make method synchronous
# Meziantou.Analyzers
MA0053.public_class_should_be_sealed = true
@@ -307,13 +304,24 @@ dotnet_diagnostic.MA0004.severity = none
dotnet_diagnostic.MA0048.severity = none
dotnet_diagnostic.MA0051.severity = none
dotnet_diagnostic.MA0053.severity = warning
+dotnet_diagnostic.MA0190.severity = none
[src/Immediate.Cache.Shared/**.cs]
# XML Documentation
+dotnet_diagnostic.CS0105.severity = error # CS0105: Using directive is unnecessary.
dotnet_diagnostic.CS1573.severity = error # CS1573: Missing XML comment for parameter
dotnet_diagnostic.CS1591.severity = error # CS1591: Missing XML comment for publicly visible type or member
dotnet_diagnostic.CS1712.severity = error # CS1712: Type parameter has no matching typeparam tag in the XML comment (but other type parameters do)
# Async
dotnet_diagnostic.CA2007.severity = error # CA2007: Consider calling ConfigureAwait on the awaited task
+
+[tests/**.cs]
+
+dotnet_diagnostic.CA1707.severity = none # CA1707: Identifiers should not contain underscores
+dotnet_diagnostic.CA1724.severity = none # CA1724: Type names should not match namespaces
+dotnet_diagnostic.CA1822.severity = none # CA1822: Mark members as static
+dotnet_diagnostic.CA1873.severity = none # CA1873: Evaluation of this argument may be expensive and unnecessary if logging is disabled
+
+dotnet_diagnostic.xUnit2029.severity = none # xUnit2029: Do not use Assert.Empty to check if a value does not exist in a collection
diff --git a/Directory.Build.props b/Directory.Build.props
index 97d1a94..49d1b63 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -22,6 +22,14 @@
+ $(Polyfill)|T:System.Index
+ $(Polyfill)|T:System.Range
+ $(Polyfill)|T:System.Diagnostics.CodeAnalysis
+ $(Polyfill)|T:System.Runtime.CompilerServices.CallerArgumentExpressionAttribute
+ $(Polyfill)|T:System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute
+ $(Polyfill)|T:System.Runtime.CompilerServices.IsExternalInit
+ $(Polyfill)|T:System.Runtime.CompilerServices.RequiredMemberAttribute
+ $(Polyfill)|T:System.Runtime.CompilerServices.SkipLocalsInitAttribute
$(Polyfill)|T:System.Threading.Lock
$(Polyfill)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 0721d7a..d0a0f2d 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,6 +4,7 @@
+
@@ -13,6 +14,9 @@
+
+
+
diff --git a/Immediate.Cache.slnx b/Immediate.Cache.slnx
index 9daf63d..0f9cda7 100644
--- a/Immediate.Cache.slnx
+++ b/Immediate.Cache.slnx
@@ -16,6 +16,7 @@
+
diff --git a/readme.md b/readme.md
index 806ff50..79fc349 100644
--- a/readme.md
+++ b/readme.md
@@ -19,7 +19,9 @@ Immediate.Cache is a collection of classes that simplify caching responses from
### Creating a Cache
-Create a subclass of `ApplicationCacheBase`, which will serve as the cache for a particular handler. An example:
+Create a class and apply the `[CacheFor<>]` attribute, targeting a handler. Add a `TransformKey` method to transform a
+request into a cache key. For example:
+
```cs
[Handler]
public static partial class GetValue
@@ -33,13 +35,8 @@ public static partial class GetValue
) => ValueTask.FromResult(new Response(query.Value));
}
-public sealed class GetValueCache(
- IMemoryCache memoryCache,
- Owned> ownedHandler
-) : ApplicationCacheBase(
- memoryCache,
- ownedHandler
-)
+[CacheFor]
+public sealed class GetValueCache
{
protected override string TransformKey(GetValue.Query request) =>
$"GetValue(query: {request.Value})";
@@ -48,24 +45,15 @@ public sealed class GetValueCache(
In this case, the `GetValueCache` class will serve as a cache for the `GetValue` IH handler.
-### Register the Cache with DI
-
-In your `Program.cs` file:
+### Adding generated caches to the `IServiceCollection` collection
-* Ensure that Memory Cache is registered, by calling:
-```cs
-services.AddMemoryCache();
-```
+In your `Program.cs`, add a call to `services.AddXxxCaches()`, where Xxx is the application identifier. By default,
+this is the short form of the assembly name. For example:
-* Register `Owned<>` as a singleton
-```cs
-services.AddSingleton(typeof(Owned<>));
-```
+* For a project named `Web`, it will be `services.AddWebCaches()`
+* For a project named `Application.Web`, it will be `services.AddApplicationWebCaches()`
-* Register your cache service(s) as a singleton(s)
-```cs
-services.AddSingleton();
-```
+However, this name can be overridden using `[assembly: ImmediateAssemblyIdentifierAttribute("SomeIdentifier")]`.
### Retrieve Data From the Cache
diff --git a/src/Common/ITypeSymbolExtensions.cs b/src/Common/ITypeSymbolExtensions.cs
new file mode 100644
index 0000000..328be60
--- /dev/null
+++ b/src/Common/ITypeSymbolExtensions.cs
@@ -0,0 +1,114 @@
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.CodeAnalysis;
+
+namespace Immediate.Cache;
+
+internal static class ITypeSymbolExtensions
+{
+ extension([NotNullWhen(true)] ITypeSymbol? typeSymbol)
+ {
+ public bool IsImmediateAssemblyIdentifierAttribute =>
+ typeSymbol is INamedTypeSymbol
+ {
+ Arity: 0,
+ Name: "ImmediateAssemblyIdentifierAttribute",
+ ContainingNamespace.IsImmediateHandlersShared: true,
+ };
+
+ public bool IsHandlerAttribute =>
+ typeSymbol is INamedTypeSymbol
+ {
+ Name: "HandlerAttribute",
+ ContainingNamespace.IsImmediateHandlersShared: true,
+ };
+ }
+
+ extension(INamedTypeSymbol typeSymbol)
+ {
+ public bool GetValidHandleMethod([NotNullWhen(true)] out ITypeSymbol? requestType, [NotNullWhen(true)] out ITypeSymbol? responseType)
+ {
+ requestType = null;
+ responseType = null;
+
+ if (!typeSymbol.GetAttributes().Any(a => a.AttributeClass.IsHandlerAttribute))
+ return false;
+
+ if (typeSymbol
+ .GetMembers()
+ .OfType()
+ .Where(m => m.Name is "Handle" or "HandleAsync")
+ .Take(2)
+ .ToList() is not [var handleMethod])
+ {
+ return false;
+ }
+
+ // must have request type
+ if (handleMethod.Parameters is not [{ Type: ITypeSymbol parameterType }, ..])
+ return false;
+
+ if (handleMethod.ReturnType is not INamedTypeSymbol
+ {
+ Arity: 1,
+ Name: "ValueTask",
+ ContainingNamespace.IsSystemThreadingTasks: true,
+ TypeArguments: [ITypeSymbol returnType],
+ })
+ {
+ return false;
+ }
+
+ requestType = parameterType;
+ responseType = returnType;
+ return true;
+ }
+ }
+
+ extension(INamespaceSymbol namespaceSymbol)
+ {
+ public bool IsImmediateCacheShared =>
+ namespaceSymbol is
+ {
+ Name: "Shared",
+ ContainingNamespace:
+ {
+ Name: "Cache",
+ ContainingNamespace:
+ {
+ Name: "Immediate",
+ ContainingNamespace.IsGlobalNamespace: true,
+ },
+ },
+ };
+
+ public bool IsImmediateHandlersShared =>
+ namespaceSymbol is
+ {
+ Name: "Shared",
+ ContainingNamespace:
+ {
+ Name: "Handlers",
+ ContainingNamespace:
+ {
+ Name: "Immediate",
+ ContainingNamespace.IsGlobalNamespace: true,
+ },
+ },
+ };
+
+ public bool IsSystemThreadingTasks =>
+ namespaceSymbol is
+ {
+ Name: "Tasks",
+ ContainingNamespace:
+ {
+ Name: "Threading",
+ ContainingNamespace:
+ {
+ Name: "System",
+ ContainingNamespace.IsGlobalNamespace: true,
+ },
+ },
+ };
+ }
+}
diff --git a/src/Common/SyntaxExtensions.cs b/src/Common/SyntaxExtensions.cs
new file mode 100644
index 0000000..9737d42
--- /dev/null
+++ b/src/Common/SyntaxExtensions.cs
@@ -0,0 +1,13 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Immediate.Cache;
+
+internal static class SyntaxExtensions
+{
+ extension(MemberDeclarationSyntax mds)
+ {
+ public bool IsStatic => mds.Modifiers.Any(m => m.IsKind(SyntaxKind.StaticKeyword));
+ }
+}
diff --git a/src/Common/Utility.cs b/src/Common/Utility.cs
new file mode 100644
index 0000000..cd12d38
--- /dev/null
+++ b/src/Common/Utility.cs
@@ -0,0 +1,12 @@
+using Microsoft.CodeAnalysis;
+
+namespace Immediate.Cache;
+
+internal static class Utility
+{
+ public static string? NullIf(this string value, string check) =>
+ value.Equals(check, StringComparison.Ordinal) ? null : value;
+
+ public static IncrementalValuesProvider WhereNotNull(this IncrementalValuesProvider values)
+ where T : class => values.Where(x => x is not null)!;
+}
diff --git a/src/Immediate.Cache.Analyzers/Immediate.Cache.Analyzers.csproj b/src/Immediate.Cache.Analyzers/Immediate.Cache.Analyzers.csproj
index 551c261..18975e9 100644
--- a/src/Immediate.Cache.Analyzers/Immediate.Cache.Analyzers.csproj
+++ b/src/Immediate.Cache.Analyzers/Immediate.Cache.Analyzers.csproj
@@ -1,15 +1,19 @@
- netstandard2.0
- true
- true
+ netstandard2.0
+ true
+ true
-
-
-
+
+
+
+
+
+
+
diff --git a/src/Immediate.Cache.Analyzers/OwnedDisposableScopeSuppressor.cs b/src/Immediate.Cache.Analyzers/OwnedDisposableScopeSuppressor.cs
index 1319bb8..d246793 100644
--- a/src/Immediate.Cache.Analyzers/OwnedDisposableScopeSuppressor.cs
+++ b/src/Immediate.Cache.Analyzers/OwnedDisposableScopeSuppressor.cs
@@ -54,15 +54,7 @@ public override void ReportSuppressions(SuppressionAnalysisContext context)
{
Arity: 1,
Name: "Owned",
- ContainingNamespace:
- {
- Name: "Cache",
- ContainingNamespace:
- {
- Name: "Immediate",
- ContainingNamespace.IsGlobalNamespace: true,
- },
- },
+ ContainingNamespace.IsImmediateCacheShared: true,
})
{
continue;
diff --git a/src/Immediate.Cache.Generators/DisplayNameFormatters.cs b/src/Immediate.Cache.Generators/DisplayNameFormatters.cs
new file mode 100644
index 0000000..6adbaca
--- /dev/null
+++ b/src/Immediate.Cache.Generators/DisplayNameFormatters.cs
@@ -0,0 +1,14 @@
+using Microsoft.CodeAnalysis;
+
+namespace Immediate.Cache.Generators;
+
+internal static class DisplayNameFormatters
+{
+ public static readonly SymbolDisplayFormat FullyQualifiedWithNullableFormat =
+ SymbolDisplayFormat.FullyQualifiedFormat
+ .WithMiscellaneousOptions(
+ SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions
+ | SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier
+ );
+
+}
diff --git a/src/Immediate.Cache.Generators/EquatableReadOnlyList.cs b/src/Immediate.Cache.Generators/EquatableReadOnlyList.cs
new file mode 100644
index 0000000..8774136
--- /dev/null
+++ b/src/Immediate.Cache.Generators/EquatableReadOnlyList.cs
@@ -0,0 +1,53 @@
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Immediate.Cache.Generators;
+
+[ExcludeFromCodeCoverage]
+public static class EquatableReadOnlyList
+{
+ public static EquatableReadOnlyList ToEquatableReadOnlyList(this IEnumerable enumerable)
+ => new(enumerable is IReadOnlyList l ? l : [.. enumerable]);
+}
+
+///
+/// A wrapper for IReadOnlyList that provides value equality support for the wrapped list.
+///
+[ExcludeFromCodeCoverage]
+public readonly struct EquatableReadOnlyList(
+ IReadOnlyList? collection
+) : IEquatable>, IReadOnlyList
+{
+ private IReadOnlyList Collection => collection ?? [];
+
+ public bool Equals(EquatableReadOnlyList other)
+ => this.SequenceEqual(other);
+
+ public override bool Equals(object? obj)
+ => obj is EquatableReadOnlyList other && Equals(other);
+
+ public override int GetHashCode()
+ {
+ var hashCode = new HashCode();
+
+ foreach (var item in Collection)
+ hashCode.Add(item);
+
+ return hashCode.ToHashCode();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ => Collection.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator()
+ => Collection.GetEnumerator();
+
+ public int Count => Collection.Count;
+ public T this[int index] => Collection[index];
+
+ public static bool operator ==(EquatableReadOnlyList left, EquatableReadOnlyList right)
+ => left.Equals(right);
+
+ public static bool operator !=(EquatableReadOnlyList left, EquatableReadOnlyList right)
+ => !left.Equals(right);
+}
diff --git a/src/Immediate.Cache.Generators/Immediate.Cache.Generators.csproj b/src/Immediate.Cache.Generators/Immediate.Cache.Generators.csproj
new file mode 100644
index 0000000..1812740
--- /dev/null
+++ b/src/Immediate.Cache.Generators/Immediate.Cache.Generators.csproj
@@ -0,0 +1,37 @@
+
+
+
+ netstandard2.0
+ true
+ true
+ $(NoWarn);CA1716
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $(GetTargetPathDependsOn);GetDependencyTargetPaths
+
+
+
+
+
+
+
+
+
diff --git a/src/Immediate.Cache.Generators/ImmediateCacheGenerator.Models.cs b/src/Immediate.Cache.Generators/ImmediateCacheGenerator.Models.cs
new file mode 100644
index 0000000..0d8cc53
--- /dev/null
+++ b/src/Immediate.Cache.Generators/ImmediateCacheGenerator.Models.cs
@@ -0,0 +1,22 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Immediate.Cache.Generators;
+
+public sealed partial class ImmediateCacheGenerator
+{
+ [ExcludeFromCodeCoverage]
+ private sealed record AssemblyDefaults
+ {
+ public required string AssemblyName { get; init; }
+ public required string RootNamespace { get; init; }
+ }
+
+ [ExcludeFromCodeCoverage]
+ private sealed record CacheDefinition
+ {
+ public required string? Namespace { get; init; }
+ public required string ClassName { get; init; }
+ public required string RequestType { get; init; }
+ public required string ResponseType { get; init; }
+ }
+}
diff --git a/src/Immediate.Cache.Generators/ImmediateCacheGenerator.Render.cs b/src/Immediate.Cache.Generators/ImmediateCacheGenerator.Render.cs
new file mode 100644
index 0000000..d795ad0
--- /dev/null
+++ b/src/Immediate.Cache.Generators/ImmediateCacheGenerator.Render.cs
@@ -0,0 +1,76 @@
+using System.Reflection;
+using Microsoft.CodeAnalysis;
+using Scriban;
+
+namespace Immediate.Cache.Generators;
+
+public sealed partial class ImmediateCacheGenerator
+{
+ private static readonly Template ServiceCollectionExtensionsTemplate = GetTemplate("ServiceCollectionExtensions");
+ private static readonly Template ApplicationCacheTemplate = GetTemplate("ApplicationCache");
+
+ private static void RenderServiceCollectionExtensions(
+ IncrementalGeneratorInitializationContext context,
+ IncrementalValueProvider assemblyDefaults,
+ IncrementalValuesProvider caches
+ )
+ {
+ context.RegisterSourceOutput(
+ assemblyDefaults.Combine(caches.Collect()),
+ (context, x) =>
+ {
+ var source = ServiceCollectionExtensionsTemplate
+ .Render(new
+ {
+ x.Left.AssemblyName,
+ x.Left.RootNamespace,
+
+ Caches = x.Right,
+
+ Version = ThisAssembly.InformationalVersion,
+ });
+
+ context.CancellationToken.ThrowIfCancellationRequested();
+ context.AddSource("IC.ServiceCollectionExtensions.g.cs", source);
+ }
+ );
+ }
+
+ private static void RenderCaches(
+ IncrementalGeneratorInitializationContext context,
+ IncrementalValuesProvider caches
+ )
+ {
+ context.RegisterSourceOutput(
+ caches,
+ (context, cache) =>
+ {
+ var source = ApplicationCacheTemplate
+ .Render(new
+ {
+ cache.Namespace,
+ cache.ClassName,
+ cache.RequestType,
+ cache.ResponseType,
+
+ Version = ThisAssembly.InformationalVersion,
+ });
+
+ context.CancellationToken.ThrowIfCancellationRequested();
+ context.AddSource($"IC.{cache.Namespace}.{cache.ClassName}.g.cs", source);
+ }
+ );
+ }
+
+ private static Template GetTemplate(string name)
+ {
+ using var stream = Assembly
+ .GetExecutingAssembly()
+ .GetManifestResourceStream(
+ $"Immediate.Cache.Generators.Templates.{name}.sbntxt"
+ );
+
+ using var reader = new StreamReader(stream);
+ return Template.Parse(reader.ReadToEnd());
+ }
+}
diff --git a/src/Immediate.Cache.Generators/ImmediateCacheGenerator.Transform.cs b/src/Immediate.Cache.Generators/ImmediateCacheGenerator.Transform.cs
new file mode 100644
index 0000000..bab15f1
--- /dev/null
+++ b/src/Immediate.Cache.Generators/ImmediateCacheGenerator.Transform.cs
@@ -0,0 +1,31 @@
+using Microsoft.CodeAnalysis;
+
+namespace Immediate.Cache.Generators;
+
+public sealed partial class ImmediateCacheGenerator
+{
+ private static CacheDefinition? TransformCacheDefinition(GeneratorAttributeSyntaxContext context, CancellationToken token)
+ {
+ token.ThrowIfCancellationRequested();
+
+ if (context.TargetSymbol is not INamedTypeSymbol { ContainingType: null } targetSymbol)
+ return null;
+
+ if (context.Attributes is not [{ AttributeClass.TypeArguments: [INamedTypeSymbol { IsStatic: false } handlerSymbol] }])
+ return null;
+
+ var @namespace = targetSymbol.ContainingNamespace.ToString().NullIf("");
+ var name = targetSymbol.Name;
+
+ if (!handlerSymbol.GetValidHandleMethod(out var requestType, out var responseType))
+ return null;
+
+ return new CacheDefinition()
+ {
+ Namespace = @namespace,
+ ClassName = name,
+ RequestType = requestType.ToDisplayString(DisplayNameFormatters.FullyQualifiedWithNullableFormat),
+ ResponseType = responseType.ToDisplayString(DisplayNameFormatters.FullyQualifiedWithNullableFormat),
+ };
+ }
+}
diff --git a/src/Immediate.Cache.Generators/ImmediateCacheGenerator.cs b/src/Immediate.Cache.Generators/ImmediateCacheGenerator.cs
new file mode 100644
index 0000000..453b58d
--- /dev/null
+++ b/src/Immediate.Cache.Generators/ImmediateCacheGenerator.cs
@@ -0,0 +1,81 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Immediate.Cache.Generators;
+
+[Generator]
+public sealed partial class ImmediateCacheGenerator : IIncrementalGenerator
+{
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ var assemblyDefaults = GetAssemblyDefaults(context);
+
+ var caches = ProcessCaches(context);
+
+ RenderServiceCollectionExtensions(context, assemblyDefaults, caches);
+ }
+
+ private static IncrementalValueProvider GetAssemblyDefaults(IncrementalGeneratorInitializationContext context)
+ {
+ var assemblyName = context.CompilationProvider
+ .Select((cp, _) => cp.GetAssemblyIdentifier())
+ .WithTrackingName("AssemblyName");
+
+ var @namespace = context
+ .AnalyzerConfigOptionsProvider
+ .Select(
+ (c, _) => c.GlobalOptions
+ .TryGetValue("build_property.rootnamespace", out var ns)
+ ? ns : ""
+ )
+ .WithTrackingName("RootNamespace");
+
+ var assemblyDefaults = assemblyName
+ .Combine(@namespace)
+ .Select((x, _) => new AssemblyDefaults
+ {
+ AssemblyName = x.Left,
+ RootNamespace = x.Right,
+ })
+ .WithTrackingName("AssemblyDefaults");
+
+ return assemblyDefaults;
+ }
+
+ private static IncrementalValuesProvider ProcessCaches(IncrementalGeneratorInitializationContext context)
+ {
+ var caches = context.SyntaxProvider
+ .ForAttributeWithMetadataName(
+ "Immediate.Cache.Shared.CacheForAttribute`1",
+ (node, _) => node is ClassDeclarationSyntax { IsStatic: false },
+ TransformCacheDefinition
+ )
+ .WhereNotNull()
+ .WithTrackingName("CacheDefinitions");
+
+ RenderCaches(context, caches);
+
+ return caches;
+ }
+}
+
+file static class Extensions
+{
+ public static string GetAssemblyIdentifier(this Compilation compilation)
+ {
+ if (compilation.Assembly.GetAttributes()
+ .FirstOrDefault(a => a.AttributeClass.IsImmediateAssemblyIdentifierAttribute)
+ is { ConstructorArguments: [{ Value: string { Length: >= 1 } identifier }] }
+ && identifier[0] != '@'
+ && SyntaxFacts.IsValidIdentifier(identifier))
+ {
+ return identifier;
+ }
+
+ return (compilation.AssemblyName ?? string.Empty)
+ .Replace(".", string.Empty)
+ .Replace(" ", string.Empty)
+ .Trim();
+ }
+}
diff --git a/src/Immediate.Cache.Generators/Templates/ApplicationCache.sbntxt b/src/Immediate.Cache.Generators/Templates/ApplicationCache.sbntxt
new file mode 100644
index 0000000..8b660b7
--- /dev/null
+++ b/src/Immediate.Cache.Generators/Templates/ApplicationCache.sbntxt
@@ -0,0 +1,27 @@
+//
+#nullable enable
+
+#pragma warning disable CS1591
+
+{{~ if !string.empty namespace ~}}
+namespace {{ namespace }};
+
+{{~ end ~}}
+partial class {{ class_name }} : global::Immediate.Cache.Shared.ApplicationCache<
+ {{ request_type }},
+ {{ response_type }}
+>
+{
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Immediate.Cache", "{{ version }}")]
+ public {{ class_name }}(
+ global::Microsoft.Extensions.Caching.Memory.IMemoryCache memoryCache,
+ global::Immediate.Cache.Shared.Owned<
+ global::Immediate.Handlers.Shared.IHandler<
+ {{ request_type }},
+ {{ response_type }}
+ >
+ > ownedHandler
+ ) : base(memoryCache, ownedHandler)
+ {
+ }
+}
diff --git a/src/Immediate.Cache.Generators/Templates/ServiceCollectionExtensions.sbntxt b/src/Immediate.Cache.Generators/Templates/ServiceCollectionExtensions.sbntxt
new file mode 100644
index 0000000..eb7ba17
--- /dev/null
+++ b/src/Immediate.Cache.Generators/Templates/ServiceCollectionExtensions.sbntxt
@@ -0,0 +1,35 @@
+//
+#nullable enable
+
+#pragma warning disable CS1591
+
+{{~ if !string.empty root_namespace ~}}
+namespace {{ root_namespace }};
+
+{{~ end ~}}
+[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Immediate.Cache", "{{ version }}")]
+public static partial class CacheServiceCollectionExtensions
+{
+ public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection Add{{ assembly_name }}Caches(
+ this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services
+ )
+ {
+ {{~ for c in caches ~}}
+ global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.Add(
+ services,
+ global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor.Singleton(
+ typeof(global::{{~ if !string.empty c.namespace; c.namespace; "."; end }}{{ c.class_name }})
+ )
+ );
+
+ {{~ end ~}}
+ global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.Add(
+ services,
+ global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor.Singleton(
+ typeof(global::Immediate.Cache.Shared.Owned<>)
+ )
+ );
+
+ return services;
+ }
+}
diff --git a/src/Immediate.Cache.Shared/ApplicationCacheBase.cs b/src/Immediate.Cache.Shared/ApplicationCache.cs
similarity index 98%
rename from src/Immediate.Cache.Shared/ApplicationCacheBase.cs
rename to src/Immediate.Cache.Shared/ApplicationCache.cs
index 4cb109b..18ba9e8 100644
--- a/src/Immediate.Cache.Shared/ApplicationCacheBase.cs
+++ b/src/Immediate.Cache.Shared/ApplicationCache.cs
@@ -2,7 +2,7 @@
using Immediate.Handlers.Shared;
using Microsoft.Extensions.Caching.Memory;
-namespace Immediate.Cache;
+namespace Immediate.Cache.Shared;
///
/// Base class for caching the results of an .
@@ -19,7 +19,7 @@ namespace Immediate.Cache;
///
/// The handler from which to cache data
///
-public abstract class ApplicationCacheBase(
+public abstract class ApplicationCache(
IMemoryCache memoryCache,
Owned> handler
)
diff --git a/src/Immediate.Cache.Shared/CacheForAttribute.cs b/src/Immediate.Cache.Shared/CacheForAttribute.cs
new file mode 100644
index 0000000..e519194
--- /dev/null
+++ b/src/Immediate.Cache.Shared/CacheForAttribute.cs
@@ -0,0 +1,14 @@
+using Immediate.Handlers.Shared;
+
+namespace Immediate.Cache.Shared;
+
+///
+/// Apply to a -attributed class to generate
+/// a cache wrapper for the handler.
+///
+///
+/// The handler class for which to generate a cache.
+///
+[AttributeUsage(AttributeTargets.Class)]
+public sealed class CacheForAttribute : Attribute
+ where THandler : class;
diff --git a/src/Immediate.Cache.Shared/Immediate.Cache.Shared.csproj b/src/Immediate.Cache.Shared/Immediate.Cache.Shared.csproj
index 16fe36f..1cb1217 100644
--- a/src/Immediate.Cache.Shared/Immediate.Cache.Shared.csproj
+++ b/src/Immediate.Cache.Shared/Immediate.Cache.Shared.csproj
@@ -1,11 +1,7 @@
-
- Immediate.Cache
-
-
- runtime-async=on
+ runtime-async=on
diff --git a/src/Immediate.Cache.Shared/Owned.cs b/src/Immediate.Cache.Shared/Owned.cs
index a2357d7..6d3b27a 100644
--- a/src/Immediate.Cache.Shared/Owned.cs
+++ b/src/Immediate.Cache.Shared/Owned.cs
@@ -1,6 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
-namespace Immediate.Cache;
+namespace Immediate.Cache.Shared;
///
/// A factory for creating a scope containing a strong-type service as it's root.
diff --git a/src/Immediate.Cache.Shared/OwnedScope.cs b/src/Immediate.Cache.Shared/OwnedScope.cs
index a9eab22..679c50c 100644
--- a/src/Immediate.Cache.Shared/OwnedScope.cs
+++ b/src/Immediate.Cache.Shared/OwnedScope.cs
@@ -1,4 +1,4 @@
-namespace Immediate.Cache;
+namespace Immediate.Cache.Shared;
///
/// Represents a container for a scope and a scoped service that is rooted by the scope.
diff --git a/src/Immediate.Cache/Immediate.Cache.csproj b/src/Immediate.Cache/Immediate.Cache.csproj
index f3196c2..1da280b 100644
--- a/src/Immediate.Cache/Immediate.Cache.csproj
+++ b/src/Immediate.Cache/Immediate.Cache.csproj
@@ -24,26 +24,35 @@
+
+
+
+ Include="$(PkgScriban)/lib/netstandard2.0/Scriban.dll"
+ Pack="true"
+ PackagePath="analyzers/roslyn4.8/dotnet/cs"
+ Condition=" '$(TargetFramework)' == 'net8.0' " />
+ Include="../Immediate.Cache.Shared/bin/$(Configuration)/$(TargetFramework)/Immediate.Cache.Shared.dll"
+ Pack="true"
+ PackagePath="lib/$(TargetFramework)" />
+
+
diff --git a/tests/Immediate.Cache.FunctionalTests/ApplicationCacheTests.cs b/tests/Immediate.Cache.FunctionalTests/ApplicationCacheTests.cs
index da522f7..3bb9f48 100644
--- a/tests/Immediate.Cache.FunctionalTests/ApplicationCacheTests.cs
+++ b/tests/Immediate.Cache.FunctionalTests/ApplicationCacheTests.cs
@@ -1,3 +1,4 @@
+using Immediate.Cache.Shared;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
diff --git a/tests/Immediate.Cache.FunctionalTests/DelayGetValue.cs b/tests/Immediate.Cache.FunctionalTests/DelayGetValue.cs
index 4bd12be..535c5cf 100644
--- a/tests/Immediate.Cache.FunctionalTests/DelayGetValue.cs
+++ b/tests/Immediate.Cache.FunctionalTests/DelayGetValue.cs
@@ -21,7 +21,7 @@ public sealed class Query
public sealed record Response(int Value, bool ExecutedHandler, Guid RandomValue);
- private static readonly Lock s_lock = new();
+ private static readonly Lock Lock = new();
private static async ValueTask HandleAsync(
Query query,
@@ -37,14 +37,14 @@ CancellationToken token
_ = query.WaitForTestToStartExecuting.TrySetResult();
await query.WaitForTestToContinueOperation.Task.WaitAsync(token);
- lock (s_lock)
+ lock (Lock)
query.TimesExecuted++;
return new(query.Value, ExecutedHandler: true, RandomValue: Guid.NewGuid());
}
catch
{
- lock (s_lock)
+ lock (Lock)
query.TimesCancelled++;
throw;
}
diff --git a/tests/Immediate.Cache.FunctionalTests/DelayGetValueCache.cs b/tests/Immediate.Cache.FunctionalTests/DelayGetValueCache.cs
index a3555ca..1bd6632 100644
--- a/tests/Immediate.Cache.FunctionalTests/DelayGetValueCache.cs
+++ b/tests/Immediate.Cache.FunctionalTests/DelayGetValueCache.cs
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
+using Immediate.Cache.Shared;
using Immediate.Handlers.Shared;
using Microsoft.Extensions.Caching.Memory;
@@ -7,7 +8,7 @@ namespace Immediate.Cache.FunctionalTests;
public sealed class DelayGetValueCache(
IMemoryCache memoryCache,
Owned> ownedHandler
-) : ApplicationCacheBase(
+) : ApplicationCache(
memoryCache,
ownedHandler
)
diff --git a/tests/Immediate.Cache.FunctionalTests/GetValueCache.cs b/tests/Immediate.Cache.FunctionalTests/GetValueCache.cs
index 18f7f48..f29d0dd 100644
--- a/tests/Immediate.Cache.FunctionalTests/GetValueCache.cs
+++ b/tests/Immediate.Cache.FunctionalTests/GetValueCache.cs
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
+using Immediate.Cache.Shared;
using Immediate.Handlers.Shared;
using Microsoft.Extensions.Caching.Memory;
@@ -7,7 +8,7 @@ namespace Immediate.Cache.FunctionalTests;
public sealed class GetValueCache(
IMemoryCache memoryCache,
Owned> ownedHandler
-) : ApplicationCacheBase(
+) : ApplicationCache(
memoryCache,
ownedHandler
)
diff --git a/tests/Immediate.Cache.Tests/GeneratorTests/CacheGenerationTests.cs b/tests/Immediate.Cache.Tests/GeneratorTests/CacheGenerationTests.cs
new file mode 100644
index 0000000..1e97e28
--- /dev/null
+++ b/tests/Immediate.Cache.Tests/GeneratorTests/CacheGenerationTests.cs
@@ -0,0 +1,195 @@
+using static Immediate.Cache.Tests.Utility;
+
+namespace Immediate.Cache.Tests.GeneratorTests;
+
+public sealed class CacheGenerationTests
+{
+ [Fact]
+ public async Task ValidCache()
+ {
+ var result = GeneratorTestHelper.RunGenerator(
+ """
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Immediate.Cache.Shared;
+ using Immediate.Handlers.Shared;
+
+ namespace Dummy;
+
+ [CacheFor]
+ public sealed partial class GetUsersQueryCache
+ {
+ protected override string TransformKey(GetUsersQuery.Query request) => "Test";
+ }
+
+ [Handler]
+ public sealed partial class GetUsersQuery
+ {
+ public record Query;
+ public record Response;
+
+ private async ValueTask HandleAsync(
+ Query _,
+ CancellationToken token
+ )
+ {
+ await Task.CompletedTask;
+ return new();
+ }
+ }
+ """
+ );
+
+ Assert.Equal(
+ [
+ "Immediate.Cache.Generators/Immediate.Cache.Generators.ImmediateCacheGenerator/IC.Dummy.GetUsersQueryCache.g.cs",
+ "Immediate.Cache.Generators/Immediate.Cache.Generators.ImmediateCacheGenerator/IC.ServiceCollectionExtensions.g.cs",
+ "Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.Dummy.GetUsersQuery.g.cs",
+ "Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.ServiceCollectionExtensions.g.cs",
+ ],
+ result.GeneratedTrees.Select(t => t.FilePath.Replace('\\', '/'))
+ );
+
+ _ = await VerifyIgnoreImmediateHandlers(result);
+ }
+
+ [Fact]
+ public async Task NotHandler_DoesNotGenerate()
+ {
+ var result = GeneratorTestHelper.RunGenerator(
+ """
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Immediate.Cache.Shared;
+ using Immediate.Handlers.Shared;
+
+ namespace Dummy;
+
+ [CacheFor]
+ public sealed partial class GetUsersQueryCache
+ {
+ }
+
+ public sealed partial class GetUsersQuery
+ {
+ public record Query;
+ public record Response;
+
+ private async ValueTask HandleAsync(
+ Query _,
+ CancellationToken token
+ )
+ {
+ await Task.CompletedTask;
+ return new();
+ }
+ }
+ """,
+ skippedSteps: ["CacheDefinitions"]
+ );
+
+ Assert.Equal(
+ [
+ "Immediate.Cache.Generators/Immediate.Cache.Generators.ImmediateCacheGenerator/IC.ServiceCollectionExtensions.g.cs",
+ ],
+ result.GeneratedTrees.Select(t => t.FilePath.Replace('\\', '/'))
+ );
+
+ _ = await VerifyIgnoreImmediateHandlers(result);
+ }
+
+ [Fact]
+ public async Task StaticHandler_DoesNotGenerate()
+ {
+ var result = GeneratorTestHelper.RunGenerator(
+ """
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Immediate.Cache.Shared;
+ using Immediate.Handlers.Shared;
+
+ namespace Dummy;
+
+ [CacheFor]
+ public sealed partial class GetUsersQueryCache
+ {
+ }
+
+ [Handler]
+ public static partial class GetUsersQuery
+ {
+ public record Query;
+ public record Response;
+
+ private static async ValueTask HandleAsync(
+ Query _,
+ CancellationToken token
+ )
+ {
+ await Task.CompletedTask;
+ return new();
+ }
+ }
+ """,
+ skippedSteps: ["CacheDefinitions"]
+ );
+
+ Assert.Equal(
+ [
+ "Immediate.Cache.Generators/Immediate.Cache.Generators.ImmediateCacheGenerator/IC.ServiceCollectionExtensions.g.cs",
+ "Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.Dummy.GetUsersQuery.g.cs",
+ "Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.ServiceCollectionExtensions.g.cs",
+ ],
+ result.GeneratedTrees.Select(t => t.FilePath.Replace('\\', '/'))
+ );
+
+ _ = await VerifyIgnoreImmediateHandlers(result);
+ }
+
+ [Fact]
+ public async Task InvalidHandlerReturn_DoesNotGenerate()
+ {
+ var result = GeneratorTestHelper.RunGenerator(
+ """
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Immediate.Cache.Shared;
+ using Immediate.Handlers.Shared;
+
+ namespace Dummy;
+
+ [CacheFor]
+ public sealed partial class GetUsersQueryCache
+ {
+ }
+
+ [Handler]
+ public sealed partial class GetUsersQuery
+ {
+ public record Query;
+ public record Response;
+
+ private async ValueTask HandleAsync(
+ Query _,
+ CancellationToken token
+ )
+ {
+ await Task.CompletedTask;
+ }
+ }
+ """,
+ skippedSteps: ["CacheDefinitions"]
+ );
+
+ Assert.Equal(
+ [
+ "Immediate.Cache.Generators/Immediate.Cache.Generators.ImmediateCacheGenerator/IC.ServiceCollectionExtensions.g.cs",
+ "Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.Dummy.GetUsersQuery.g.cs",
+ "Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.ServiceCollectionExtensions.g.cs",
+ ],
+ result.GeneratedTrees.Select(t => t.FilePath.Replace('\\', '/'))
+ );
+
+ _ = await VerifyIgnoreImmediateHandlers(result);
+ }
+}
diff --git a/tests/Immediate.Cache.Tests/GeneratorTests/GeneratorTestHelper.cs b/tests/Immediate.Cache.Tests/GeneratorTests/GeneratorTestHelper.cs
new file mode 100644
index 0000000..d63b73e
--- /dev/null
+++ b/tests/Immediate.Cache.Tests/GeneratorTests/GeneratorTestHelper.cs
@@ -0,0 +1,156 @@
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using Immediate.Cache.Generators;
+using Immediate.Handlers.Generators;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace Immediate.Cache.Tests.GeneratorTests;
+
+public static class GeneratorTestHelper
+{
+ public static GeneratorDriverRunResult RunGenerator(
+ [StringSyntax("c#-test")] string source,
+ params ReadOnlySpan skippedSteps
+ ) => RunGenerator(source, LanguageVersion.CSharp13, skippedSteps);
+
+ public static GeneratorDriverRunResult RunGenerator(
+ [StringSyntax("c#-test")] string source,
+ LanguageVersion languageVersion,
+ params ReadOnlySpan skippedSteps
+ )
+ {
+ var parseOptions = new CSharpParseOptions(languageVersion);
+
+ var syntaxTree = CSharpSyntaxTree.ParseText(
+ source,
+ parseOptions,
+ cancellationToken: TestContext.Current.CancellationToken
+ );
+
+ var compilation = CSharpCompilation.Create(
+ assemblyName: "Tests",
+ syntaxTrees: [syntaxTree],
+ references:
+ [
+ ..Utility.NetCoreAssemblies,
+ ..Utility.GetAdditionalReferences(),
+ ],
+ options: new(
+ outputKind: OutputKind.DynamicallyLinkedLibrary
+ )
+ );
+
+ GeneratorDriver driver = CSharpGeneratorDriver.Create(
+ generators: [new ImmediateCacheGenerator().AsSourceGenerator(), new ImmediateHandlersGenerator().AsSourceGenerator()],
+ parseOptions: parseOptions,
+ driverOptions: new GeneratorDriverOptions(default, trackIncrementalGeneratorSteps: true)
+ );
+
+ driver = RunGenerator(driver, compilation);
+ var result = driver.GetRunResult();
+
+ VerifyIncrementality(driver, compilation, parseOptions, skippedSteps);
+
+ return result;
+ }
+
+ private static GeneratorDriver RunGenerator(
+ GeneratorDriver driver,
+ Compilation compilation
+ )
+ {
+ driver = driver
+ .RunGeneratorsAndUpdateCompilation(
+ compilation,
+ out var outputCompilation,
+ out var diagnostics,
+ TestContext.Current.CancellationToken
+ );
+
+ Assert.Empty(
+ outputCompilation
+ .GetDiagnostics(TestContext.Current.CancellationToken)
+ .Where(d => d.Severity is DiagnosticSeverity.Error or DiagnosticSeverity.Warning)
+ // ignore attempt to use a `static` handler type; making sure we don't generate in invalid scenario
+ .Where(d => d is not { Id: "CS0718" })
+ );
+
+ Assert.Empty(diagnostics);
+ return driver;
+ }
+
+ private static void VerifyIncrementality(
+ GeneratorDriver driver,
+ Compilation compilation,
+ CSharpParseOptions parseOptions,
+ ReadOnlySpan skippedSteps
+ )
+ {
+ var clone = compilation.Clone().AddSyntaxTrees(
+ CSharpSyntaxTree.ParseText(
+ "// dummy",
+ parseOptions,
+ cancellationToken: TestContext.Current.CancellationToken
+ )
+ );
+
+ driver = RunGenerator(driver, clone);
+
+ if (
+ driver.GetRunResult() is not
+ {
+ Results:
+ [
+ {
+ TrackedOutputSteps: { } outputSteps,
+ TrackedSteps: { } trackedSteps,
+ },
+ _
+ ],
+ }
+ )
+ {
+ Assert.Fail("Unable to verify incrementality.");
+ return;
+ }
+
+ foreach (var (_, step) in outputSteps)
+ AssertSteps(step);
+
+ foreach (var step in TrackedSteps)
+ {
+ if (skippedSteps.Contains(step))
+ {
+ if (trackedSteps.ContainsKey(step))
+ Assert.Fail($"Step `{step}` should have been skipped, but is present.");
+ }
+ else
+ {
+ if (!trackedSteps.TryGetValue(step, out var outputs))
+ Assert.Fail($"Step `{step}` expected, but is missing.");
+
+ AssertSteps(outputs);
+ }
+ }
+ }
+
+ private static ReadOnlySpan TrackedSteps =>
+ new string[]
+ {
+ "AssemblyName",
+ "RootNamespace",
+ "AssemblyDefaults",
+
+ "CacheDefinitions",
+ };
+
+ private static void AssertSteps(
+ ImmutableArray steps
+ )
+ {
+ var outputs = steps.SelectMany(o => o.Outputs);
+
+ Assert.All(outputs, o => Assert.True(o.Reason is IncrementalStepRunReason.Unchanged or IncrementalStepRunReason.Cached));
+ }
+}
diff --git a/tests/Immediate.Cache.Tests/GeneratorTests/ImmediateAssemblyIdentifierTests.cs b/tests/Immediate.Cache.Tests/GeneratorTests/ImmediateAssemblyIdentifierTests.cs
new file mode 100644
index 0000000..8ba394b
--- /dev/null
+++ b/tests/Immediate.Cache.Tests/GeneratorTests/ImmediateAssemblyIdentifierTests.cs
@@ -0,0 +1,57 @@
+using static Immediate.Cache.Tests.Utility;
+
+namespace Immediate.Cache.Tests.GeneratorTests;
+
+public sealed class ImmediateAssemblyIdentifierTests
+{
+ [Fact]
+ public async Task ImmediateAssemblyIdentifierOverridesAssemblyName()
+ {
+ var result = GeneratorTestHelper.RunGenerator(
+ """
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Immediate.Cache.Shared;
+ using Immediate.Handlers.Shared;
+
+ [assembly: ImmediateAssemblyIdentifier("Custom")]
+
+ namespace Dummy;
+
+ [CacheFor]
+ public sealed partial class GetUsersQueryCache
+ {
+ protected override string TransformKey(GetUsersQuery.Query request) => "Test";
+ }
+
+ [Handler]
+ public sealed partial class GetUsersQuery
+ {
+ public record Query;
+ public record Response;
+
+ private async ValueTask HandleAsync(
+ Query _,
+ CancellationToken token
+ )
+ {
+ await Task.CompletedTask;
+ return new();
+ }
+ }
+ """
+ );
+
+ Assert.Equal(
+ [
+ "Immediate.Cache.Generators/Immediate.Cache.Generators.ImmediateCacheGenerator/IC.Dummy.GetUsersQueryCache.g.cs",
+ "Immediate.Cache.Generators/Immediate.Cache.Generators.ImmediateCacheGenerator/IC.ServiceCollectionExtensions.g.cs",
+ "Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.Dummy.GetUsersQuery.g.cs",
+ "Immediate.Handlers.Generators/Immediate.Handlers.Generators.ImmediateHandlersGenerator/IH.ServiceCollectionExtensions.g.cs",
+ ],
+ result.GeneratedTrees.Select(t => t.FilePath.Replace('\\', '/'))
+ );
+
+ _ = await VerifyIgnoreImmediateHandlers(result);
+ }
+}
diff --git a/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/CacheGenerationTests.InvalidHandlerReturn_DoesNotGenerate#IC.ServiceCollectionExtensions.g.verified.cs b/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/CacheGenerationTests.InvalidHandlerReturn_DoesNotGenerate#IC.ServiceCollectionExtensions.g.verified.cs
new file mode 100644
index 0000000..6f3ae09
--- /dev/null
+++ b/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/CacheGenerationTests.InvalidHandlerReturn_DoesNotGenerate#IC.ServiceCollectionExtensions.g.verified.cs
@@ -0,0 +1,22 @@
+//HintName: IC.ServiceCollectionExtensions.g.cs
+//
+#nullable enable
+
+#pragma warning disable CS1591
+
+public static partial class CacheServiceCollectionExtensions
+{
+ public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestsCaches(
+ this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services
+ )
+ {
+ global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.Add(
+ services,
+ global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor.Singleton(
+ typeof(global::Immediate.Cache.Shared.Owned<>)
+ )
+ );
+
+ return services;
+ }
+}
diff --git a/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/CacheGenerationTests.NotHandler_DoesNotGenerate#IC.ServiceCollectionExtensions.g.verified.cs b/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/CacheGenerationTests.NotHandler_DoesNotGenerate#IC.ServiceCollectionExtensions.g.verified.cs
new file mode 100644
index 0000000..6f3ae09
--- /dev/null
+++ b/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/CacheGenerationTests.NotHandler_DoesNotGenerate#IC.ServiceCollectionExtensions.g.verified.cs
@@ -0,0 +1,22 @@
+//HintName: IC.ServiceCollectionExtensions.g.cs
+//
+#nullable enable
+
+#pragma warning disable CS1591
+
+public static partial class CacheServiceCollectionExtensions
+{
+ public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestsCaches(
+ this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services
+ )
+ {
+ global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.Add(
+ services,
+ global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor.Singleton(
+ typeof(global::Immediate.Cache.Shared.Owned<>)
+ )
+ );
+
+ return services;
+ }
+}
diff --git a/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/CacheGenerationTests.StaticHandler_DoesNotGenerate#IC.ServiceCollectionExtensions.g.verified.cs b/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/CacheGenerationTests.StaticHandler_DoesNotGenerate#IC.ServiceCollectionExtensions.g.verified.cs
new file mode 100644
index 0000000..6f3ae09
--- /dev/null
+++ b/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/CacheGenerationTests.StaticHandler_DoesNotGenerate#IC.ServiceCollectionExtensions.g.verified.cs
@@ -0,0 +1,22 @@
+//HintName: IC.ServiceCollectionExtensions.g.cs
+//
+#nullable enable
+
+#pragma warning disable CS1591
+
+public static partial class CacheServiceCollectionExtensions
+{
+ public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestsCaches(
+ this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services
+ )
+ {
+ global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.Add(
+ services,
+ global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor.Singleton(
+ typeof(global::Immediate.Cache.Shared.Owned<>)
+ )
+ );
+
+ return services;
+ }
+}
diff --git a/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/CacheGenerationTests.ValidCache#IC.Dummy.GetUsersQueryCache.g.verified.cs b/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/CacheGenerationTests.ValidCache#IC.Dummy.GetUsersQueryCache.g.verified.cs
new file mode 100644
index 0000000..bf568f1
--- /dev/null
+++ b/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/CacheGenerationTests.ValidCache#IC.Dummy.GetUsersQueryCache.g.verified.cs
@@ -0,0 +1,25 @@
+//HintName: IC.Dummy.GetUsersQueryCache.g.cs
+//
+#nullable enable
+
+#pragma warning disable CS1591
+
+namespace Dummy;
+
+partial class GetUsersQueryCache : global::Immediate.Cache.Shared.ApplicationCache<
+ global::Dummy.GetUsersQuery.Query,
+ global::Dummy.GetUsersQuery.Response
+>
+{
+ public GetUsersQueryCache(
+ global::Microsoft.Extensions.Caching.Memory.IMemoryCache memoryCache,
+ global::Immediate.Cache.Shared.Owned<
+ global::Immediate.Handlers.Shared.IHandler<
+ global::Dummy.GetUsersQuery.Query,
+ global::Dummy.GetUsersQuery.Response
+ >
+ > ownedHandler
+ ) : base(memoryCache, ownedHandler)
+ {
+ }
+}
diff --git a/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/CacheGenerationTests.ValidCache#IC.ServiceCollectionExtensions.g.verified.cs b/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/CacheGenerationTests.ValidCache#IC.ServiceCollectionExtensions.g.verified.cs
new file mode 100644
index 0000000..3fe4091
--- /dev/null
+++ b/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/CacheGenerationTests.ValidCache#IC.ServiceCollectionExtensions.g.verified.cs
@@ -0,0 +1,29 @@
+//HintName: IC.ServiceCollectionExtensions.g.cs
+//
+#nullable enable
+
+#pragma warning disable CS1591
+
+public static partial class CacheServiceCollectionExtensions
+{
+ public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddTestsCaches(
+ this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services
+ )
+ {
+ global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.Add(
+ services,
+ global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor.Singleton(
+ typeof(global::Dummy.GetUsersQueryCache)
+ )
+ );
+
+ global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.Add(
+ services,
+ global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor.Singleton(
+ typeof(global::Immediate.Cache.Shared.Owned<>)
+ )
+ );
+
+ return services;
+ }
+}
diff --git a/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/ImmediateAssemblyIdentifierTests.ImmediateAssemblyIdentifierOverridesAssemblyName#IC.Dummy.GetUsersQueryCache.g.verified.cs b/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/ImmediateAssemblyIdentifierTests.ImmediateAssemblyIdentifierOverridesAssemblyName#IC.Dummy.GetUsersQueryCache.g.verified.cs
new file mode 100644
index 0000000..bf568f1
--- /dev/null
+++ b/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/ImmediateAssemblyIdentifierTests.ImmediateAssemblyIdentifierOverridesAssemblyName#IC.Dummy.GetUsersQueryCache.g.verified.cs
@@ -0,0 +1,25 @@
+//HintName: IC.Dummy.GetUsersQueryCache.g.cs
+//
+#nullable enable
+
+#pragma warning disable CS1591
+
+namespace Dummy;
+
+partial class GetUsersQueryCache : global::Immediate.Cache.Shared.ApplicationCache<
+ global::Dummy.GetUsersQuery.Query,
+ global::Dummy.GetUsersQuery.Response
+>
+{
+ public GetUsersQueryCache(
+ global::Microsoft.Extensions.Caching.Memory.IMemoryCache memoryCache,
+ global::Immediate.Cache.Shared.Owned<
+ global::Immediate.Handlers.Shared.IHandler<
+ global::Dummy.GetUsersQuery.Query,
+ global::Dummy.GetUsersQuery.Response
+ >
+ > ownedHandler
+ ) : base(memoryCache, ownedHandler)
+ {
+ }
+}
diff --git a/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/ImmediateAssemblyIdentifierTests.ImmediateAssemblyIdentifierOverridesAssemblyName#IC.ServiceCollectionExtensions.g.verified.cs b/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/ImmediateAssemblyIdentifierTests.ImmediateAssemblyIdentifierOverridesAssemblyName#IC.ServiceCollectionExtensions.g.verified.cs
new file mode 100644
index 0000000..fb3c9d2
--- /dev/null
+++ b/tests/Immediate.Cache.Tests/GeneratorTests/Snapshots/ImmediateAssemblyIdentifierTests.ImmediateAssemblyIdentifierOverridesAssemblyName#IC.ServiceCollectionExtensions.g.verified.cs
@@ -0,0 +1,29 @@
+//HintName: IC.ServiceCollectionExtensions.g.cs
+//
+#nullable enable
+
+#pragma warning disable CS1591
+
+public static partial class CacheServiceCollectionExtensions
+{
+ public static global::Microsoft.Extensions.DependencyInjection.IServiceCollection AddCustomCaches(
+ this global::Microsoft.Extensions.DependencyInjection.IServiceCollection services
+ )
+ {
+ global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.Add(
+ services,
+ global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor.Singleton(
+ typeof(global::Dummy.GetUsersQueryCache)
+ )
+ );
+
+ global::Microsoft.Extensions.DependencyInjection.Extensions.ServiceCollectionDescriptorExtensions.Add(
+ services,
+ global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor.Singleton(
+ typeof(global::Immediate.Cache.Shared.Owned<>)
+ )
+ );
+
+ return services;
+ }
+}
diff --git a/tests/Immediate.Cache.Tests/Immediate.Cache.Tests.csproj b/tests/Immediate.Cache.Tests/Immediate.Cache.Tests.csproj
index 30468ea..f587110 100644
--- a/tests/Immediate.Cache.Tests/Immediate.Cache.Tests.csproj
+++ b/tests/Immediate.Cache.Tests/Immediate.Cache.Tests.csproj
@@ -1,7 +1,7 @@
- Exe
+ Exe
<_SkipUpgradeNetAnalyzersNuGetWarning>true
@@ -10,39 +10,45 @@
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
+
+
+
-
+
-
+
-
+
-
+
+
+
-
+
diff --git a/tests/Immediate.Cache.Tests/ModuleInitializer.cs b/tests/Immediate.Cache.Tests/ModuleInitializer.cs
new file mode 100644
index 0000000..916198f
--- /dev/null
+++ b/tests/Immediate.Cache.Tests/ModuleInitializer.cs
@@ -0,0 +1,16 @@
+using System.Runtime.CompilerServices;
+
+namespace Immediate.Cache.Tests;
+
+public static class ModuleInitializer
+{
+ [ModuleInitializer]
+ public static void Init()
+ {
+ VerifierSettings.AutoVerify(includeBuildServer: false);
+ VerifierSettings.ScrubLinesContaining("cs", comparison: StringComparison.Ordinal, "GeneratedCodeAttribute");
+ UseSourceFileRelativeDirectory("Snapshots");
+
+ VerifySourceGenerators.Initialize();
+ }
+}
diff --git a/tests/Immediate.Cache.Tests/SuppressorTests/OwnedDisposableScopeSuppressorTests.cs b/tests/Immediate.Cache.Tests/SuppressorTests/OwnedDisposableScopeSuppressorTests.cs
index af844fd..01aa43e 100644
--- a/tests/Immediate.Cache.Tests/SuppressorTests/OwnedDisposableScopeSuppressorTests.cs
+++ b/tests/Immediate.Cache.Tests/SuppressorTests/OwnedDisposableScopeSuppressorTests.cs
@@ -18,7 +18,7 @@ await SuppressorTestHelpers
using System;
using System.Threading.Tasks;
- using Immediate.Cache;
+ using Immediate.Cache.Shared;
public sealed class Disposable : IDisposable, IAsyncDisposable
{
@@ -50,7 +50,7 @@ await SuppressorTestHelpers
using System;
using System.Threading.Tasks;
- using Immediate.Cache;
+ using Immediate.Cache.Shared;
public sealed class Disposable : IDisposable, IAsyncDisposable
{
@@ -84,7 +84,7 @@ await SuppressorTestHelpers
using System;
using System.Threading.Tasks;
- using Immediate.Cache;
+ using Immediate.Cache.Shared;
public sealed class Disposable : IDisposable, IAsyncDisposable
{
@@ -118,7 +118,7 @@ await SuppressorTestHelpers
using System;
using System.Threading.Tasks;
- using Immediate.Cache;
+ using Immediate.Cache.Shared;
public sealed class Disposable : IDisposable, IAsyncDisposable
{
@@ -151,7 +151,7 @@ await SuppressorTestHelpers
using System;
using System.Threading.Tasks;
- using Immediate.Cache;
+ using Immediate.Cache.Shared;
public sealed class Disposable : IDisposable, IAsyncDisposable
{
@@ -189,7 +189,7 @@ await SuppressorTestHelpers
using System;
using System.Threading.Tasks;
- using Immediate.Cache;
+ using Immediate.Cache.Shared;
public sealed class Disposable : IDisposable, IAsyncDisposable
{
@@ -226,7 +226,7 @@ await SuppressorTestHelpers
using System;
using System.Threading.Tasks;
- using Immediate.Cache;
+ using Immediate.Cache.Shared;
public sealed class Disposable : IDisposable, IAsyncDisposable
{
diff --git a/tests/Immediate.Cache.Tests/SuppressorTests/SuppressorTestHelpers.cs b/tests/Immediate.Cache.Tests/SuppressorTests/SuppressorTestHelpers.cs
index 3bdb75c..e7960c6 100644
--- a/tests/Immediate.Cache.Tests/SuppressorTests/SuppressorTestHelpers.cs
+++ b/tests/Immediate.Cache.Tests/SuppressorTests/SuppressorTestHelpers.cs
@@ -97,7 +97,7 @@ public static CSharpSuppressorTest CreateSuppresso
},
};
- test.TestState.AdditionalReferences.AddRange(Utility.GetMetadataReferences());
+ test.TestState.AdditionalReferences.AddRange(Utility.GetAdditionalReferences());
return test;
}
diff --git a/tests/Immediate.Cache.Tests/Utility.cs b/tests/Immediate.Cache.Tests/Utility.cs
index cafa8cc..340b363 100644
--- a/tests/Immediate.Cache.Tests/Utility.cs
+++ b/tests/Immediate.Cache.Tests/Utility.cs
@@ -1,9 +1,15 @@
+using System.Runtime.CompilerServices;
+using System.Text.RegularExpressions;
+using Immediate.Cache.Shared;
+using Immediate.Handlers.Shared;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Testing;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.DependencyInjection;
namespace Immediate.Cache.Tests;
-internal static class Utility
+internal static partial class Utility
{
#if NET8_0
public static ReferenceAssemblies ReferenceAssemblies => ReferenceAssemblies.Net.Net80;
@@ -28,8 +34,19 @@ internal static class Utility
#error .net version not yet implemented
#endif
- public static IEnumerable GetMetadataReferences() =>
+ public static IEnumerable GetAdditionalReferences() =>
[
MetadataReference.CreateFromFile(typeof(Owned<>).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(HandlerAttribute).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(ServiceCollection).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(IServiceCollection).Assembly.Location),
+ MetadataReference.CreateFromFile(typeof(IMemoryCache).Assembly.Location),
];
+
+ public static SettingsTask VerifyIgnoreImmediateHandlers(GeneratorDriverRunResult result, [CallerFilePath] string sourceFile = "") =>
+ Verify(result, sourceFile: sourceFile)
+ .IgnoreGeneratedResult(gsr => ImmediateHandlersHintName().IsMatch(Path.GetFileName(gsr.HintName)));
+
+ [GeneratedRegex(@"IH\..*\.g\.cs", RegexOptions.ExplicitCapture, matchTimeoutMilliseconds: 100)]
+ private static partial Regex ImmediateHandlersHintName();
}