Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ICacheSpecificationBuilder #161

Merged
merged 1 commit into from
Sep 28, 2021

Conversation

vittorelli
Copy link
Contributor

@vittorelli vittorelli commented Sep 25, 2021

A small change to the SpecificationBuilderExtensions.EnableCache builder method in order to allow developers to create their own extension methods for customized caching implementations.

I've been using Spefications throughout several different projects and am a huge fan. We've recently added support for caching in our repositories and I've refactored the code so it integrates with Specification. A requirement in our project was to have some expiration date on the cache, which isn't possible with the current method signatures. I saw two possibilities:

  1. Create additional methods/overloads/... in Ardalis.Specification that allow for adding information on how the caching should behave.
  2. Minimal intrusion in Ardalis.Specification by changing the return type of SpecificationBuilderExtensions.EnableCache.

The liked the first option better, but soon realised that my requirements wouldn't nessecarily match those of other developers, and I didn't like the idea of creating too many new methods to support different needs in caching behaviour (absolute expiration, sliding windows, tokens, ....).

So I've gone with the second possibility.
The main idea of this pull request is to constrain the usage of extension methods one may create to facilitate custom caching needs in projects that consume Ardalis.Specification. This idea might also be extended to other builder methods.

What do you guys think?

Example on how I'm using the ICacheSpecificationBuilder

    public static class CachedSpecificationExtensions
    {
        private static ConcurrentDictionary<string, TimeSpan> _expirationTimeSpans = new();

        /// <summary>
        /// Extension method which registers cache TTL values for the <see cref="Ardalis.Specification"/> on
        /// which this method is called.
        /// </summary>
        public static ISpecificationBuilder<T> WithCacheExpiration<T>(this ICacheSpecificationBuilder<T> @this, TimeSpan span)
             where T : class
        {
            _expirationTimeSpans[@this.Specification.CacheKey] = span;
            return @this;
        }

        /// <summary>
        /// Utility method which can be used in repositories to read registered TTL values.
        /// </summary>
        /// <param name="key"></param>
        public static TimeSpan GetTimeSpan(string key)
        {
            return _expirationTimeSpans.ContainsKey(key)
                ? _expirationTimeSpans[key]
                : TimeSpan.MaxValue;
        }
    }

    /// <summary>
    /// Example specification using <see cref="SpecificationExtentions.WithCacheExpiration{T}(ICacheSpecificationBuilder{T}, TimeSpan)"/>.
    /// </summary>
    public class CachedEntityListSpecification : Specification<Entity>
    {
        public CachedEntityListSpecification(string color)
        {
            Query
		.EnableCache(nameof(CachedEntityListSpecification), "filter_color", color)
			.WithCacheExpiration(TimeSpan.FromHours(12))
		.Where(x => x.Color == color);
        }
    }

    /// <summary>
    /// Example Entity class.
    /// </summary>
    public class Entity
    {
        public string Name { get; set; }
        public string Color { get; set; }
    }

    /// <summary>
    /// Example Repository
    /// </summary>
    public class Repository<T>
    {
        private DbContext _ctx;
        private MemoryCache _cache;

        public List<T> List(ISpecification<T> spec)
        {
            var specificationResult = SpecificationEvaluator.Default.GetQuery(_ctx.Set<T>().AsQueryable(), spec);

            if (spec.CacheEnabled)
            {
                var ttl = CachedSpecificationExtensions.GetTimeSpan(spec.CacheKey);
                _cache.GetOrCreate(spec.CacheKey, ce =>
                {
                    ce.AbsoluteExpiration = DateTime.Now.Add(ttl);
                    return specificationResult.ToList();
                });
            }
            else
            {
                return specificationResult.ToList();
            }
        }
    }

@fiseni
Copy link
Collaborator

fiseni commented Sep 25, 2021

Hi @vittorelli,

Thank you. I do like this. We're not including anything specific, but we're providing a mechanism for users to handle it by themselves.

Your usage is nice too. To be honest, keeping app-wide mutable shared state is always fishy :) Yea, the concurrent dictionary is considered thread-safe, but not all methods. Having your own base specification which will include the state might be a better solution, but it requires a bit more work.

@ardalis if you don't have any comments, let's merge this.

@vittorelli
Copy link
Contributor Author

vittorelli commented Sep 25, 2021

Having your own base specification which will include the state might be a better solution, but it requires a bit more work.

Don't know why I didn't think of that. Way better, thanks! :-)

@ardalis
Copy link
Owner

ardalis commented Sep 25, 2021

I love it; let's do it! Any chance we can get a docs PR with some usage examples, too?

@vittorelli
Copy link
Contributor Author

I'll see what I can do about the docs :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants