-
Notifications
You must be signed in to change notification settings - Fork 1.7k
#741 Add filtering options and logic to caching #2337
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
base: develop
Are you sure you want to change the base?
#741 Add filtering options and logic to caching #2337
Conversation
|
Hello, Cliff!
Well... correct, OutputCacheMiddlewareTests contains our unit tests for the
Please avoid using
Sure, our team will provide guidance, but please be patient as I'm currently swamped with the hot v24.1 release we're aiming to finish this week. Also, please include acceptance tests as they are an important part of the development process alongside integration/unit tests, and I'll arrange for the team to carry out code reviews. Good luck with the delivery! 😉 |
|
Raman, |
|
We overlooked an important detail: the global configuration, which was improved in the latest PR #2331. The current design adds a "CacheOptions": {
"TtlSeconds": 0,
"Region": "",
"StatusCodes": [400, 404, 405, 410] // excluded or included?
}I think this currently only covers the excluding scenario. We should reconsider your initial implementation to handle the including scenario as well. We also need to implement the excluding scenario explicitly. I suggest introducing groups of codes like 2xx, 3xx, 4xx, and 5xx, where having a single 200 code in the array would represent all 2xx statuses. Finally, we should add a management Boolean option to enable or disable the status codes feature on the fly, so that global status checks can be turned off for specific routes, and vice versa, re-enabled for certain routes even when globally disabled. More technical details will follow. |
|
Raman, |
Add ability to specify blacklisting or whitelisting. Default is Whitelisting. Add ability to specify a range like "2xx", "3xx", "4xx", "5xx" Filter type is generic to be able to use for other filtering, should need arise.
Add Acceptance Tests to filter based on HTTP Response Status Code
|
Raman, public enum FilterType
{
Whitelist,
Blacklist,
}The filter is added to CacheOptions = {
TtlSeconds = 100,
StatusCodeFilter = new HttpStatusCodeFilter(FilterType.Whitelist, new HttpStatusCode[] { HttpStatusCode.OK, HttpStatusCode.Forbidden }),
};You can specify the codes in the filter with strings, including the 2xx, 3xx, etc. ranges CacheOptions = {
TtlSeconds = 100,
StatusCodeFilter = new HttpStatusCodeFilter(FilterType.Whitelist, new string[] { "200", "4xx" }),
};I have added some pretty exhaustive tests. Would you like it extended to allow the items in the list as |
I'm not in favor of using whitelisting and blacklisting, as it feels a bit awkward. I'm not in favor of using custom enumerations, as they involve writing extensive documentation, and parsing string values can hurt the gateway's performance when route artifacts are not cached. Please revert both commits! Is this code and design the result of vibe coding with AI help? It feels overcomplicated and impractical. It is fine to use AI to help write code, but only for an approved feature design❗As contributors, professional C# developers are prohibited from using AI for coding, and a few "contributors" have been banned for doing so. I usually don't allow vibe coding in our repository. Since you are returning to the software development industry without extensive .NET experience, you're allowed to use AI under these restrictions. Also, could you share a link to your LinkedIn profile please? In the Software Development Life Cycle, the first stages are requirements, planning, BI, design, and creating the specification (three phases), followed by the 4th stage, coding or implementation. However, the design was implemented without prior discussion. The issue with this design is that all filtering is handled through C# code, while the main purpose of Ocelot configuration is to use the JSON file ( The old "CacheOptions": {
"TtlSeconds": 888,
"StatusCodes": [200, 301, 302, 303], // included codes, caching enabled
"StatusCodesExcluded": [500, 404, 405, 410] // excluded codes, no caching
}Possible names for the exclusion option could be Idea 1I have an idea to combine both inclusion and exclusion scenarios by using negative values in an array to represent the exclusion (no caching) cases. For example: "CacheOptions": {
"TtlSeconds": 888,
"StatusCodes": [200, 301, 302, 303, -500, -404, -405, -410] // mix of positive and negative codes
}Here is the breakdown:
Idea 2Another sub-feature is the addition of default StatusCodes by Ocelot, based on common best practices for reverse proxies when global configuration is absent. This should be discussed as well. Cliff, P.S. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some great tests have been added, but we are moving forward with redesigning the feature.
Consider this comment as the updated specification, serving as a revision of the #741 requirements. This isn't the final version, as I'm open to new ideas and discussions.
| /// <value><see langword="true"/> if content hashing is enabled; otherwise, <see langword="false"/>.</value> | ||
| public bool? EnableContentHashing { get; set; } | ||
|
|
||
| public HttpStatusCodeFilter StatusCodeFilter { get; set; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$\color{red}{Critical\ issue\ with\ the\ property\ type!}$
| public HttpStatusCodeFilter StatusCodeFilter { get; set; } | |
| public int[] StatusCodes { get; set; } |
Note that the type is not the old HttpStatusCode[] but rather int[], an array of integers. If an Ocelot user is not a C# developer, they might not be familiar with the HttpStatusCode enumeration and its values!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've just pushed a new commit 9013937 with the fix, but I assume the HttpStatusCode enumeration is parsed without issues by the JSON deserializer from the System.Text.Json namespace. I am not so sure about the Newtonsoft.Json deserializer, which is currently the default parser in Ocelot, though we plan to replace Newtonsoft.Json with System.Text.Json soon. If Newtonsoft handles C# enumerations well, hopefully, it might be worth rethinking the proposed property. Either way, the int[] StatusCodes property will remain the main one. I just commented out the line so we can go over this again.
| var ttl = TimeSpan.FromSeconds(options.TtlSeconds); | ||
| _outputCache.Add(downStreamRequestCacheKey, cached, options.Region, ttl); | ||
| Logger.LogDebug(() => $"Finished response added to cache for the '{downstreamUrlKey}' key."); | ||
| if (options.StatusCodeFilter == null || options.StatusCodeFilter.PassesFilter(cached.StatusCode)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- The
PassesFilter(cached.StatusCode)method could serve as a small helper in theCacheOptionsbusiness object, though it depends on the final design. - The expression
options.StatusCodeFilter.PassesFiltergives the impression that an additional filter has been injected into the logic, but that doesn’t seem to be the case. Since we’re not varying filters, we’ll just have a single filter.
| if (options.StatusCodeFilter == null || options.StatusCodeFilter.PassesFilter(cached.StatusCode)) | ||
| { | ||
| _outputCache.Add(downStreamRequestCacheKey, cached, options.Region, ttl); | ||
| Logger.LogDebug(() => $"Finished response added to cache for the '{downstreamUrlKey}' key."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please wrap the logging for the Debug scenario with a preprocessor directive, as another team member will likely request this 👇
| Logger.LogDebug(() => $"Finished response added to cache for the '{downstreamUrlKey}' key."); | |
| #if DEBUG | |
| Logger.LogDebug(() => $"Finished response added to cache for the '{downstreamUrlKey}' key."); | |
| #endif |
| { | ||
| public CacheOptions Create(FileCacheOptions options) | ||
| => new(options?.TtlSeconds, options?.Region, options?.Header, options?.EnableContentHashing); | ||
| => new(options?.TtlSeconds, options?.Region, options?.Header, options?.EnableContentHashing, options?.StatusCodeFilter); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems I missed this issue in the previous PR. Extending the argument list in a single constructor is not the right approach. It’s better to have a few overloaded constructors that cover most user scenarios.
Here’s what I propose:
| => new(options?.TtlSeconds, options?.Region, options?.Header, options?.EnableContentHashing, options?.StatusCodeFilter); | |
| => new(options ?? new()); |
The null-coalescing operator (??) is used to handle argument null checks within the new constructor which will be designed by you
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can do. I was just following the existing pattern. Not my project, so did not figure it was my place to make that change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, never mind. I will handle it myself.
| var ttlSeconds = options.TtlSeconds ?? globalOptions.TtlSeconds; | ||
| var enableHashing = options.EnableContentHashing ?? globalOptions.EnableContentHashing; | ||
| return new CacheOptions(ttlSeconds, region, header, enableHashing); | ||
| var statusCodes = options.StatusCodeFilter ?? globalOptions.StatusCodeFilter; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a lack of defaulting! Refer to my Idea 2 in the previous comment. We could implement defaulting closer to the Dev Complete stage, and I'll help with that.
Finally, I expect the following improvement:
| var statusCodes = options.StatusCodeFilter ?? globalOptions.StatusCodeFilter; | |
| var statusCodes = options.StatusCodes ?? globalOptions.StatusCodes ?? Ocelot.RecommendedStatusCodes; |
src/Ocelot/Filter/IFilter.cs
Outdated
| public interface IFilter<T> | ||
| { | ||
| bool PassesFilter(T value); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you suggesting using multiple filters for different options?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought perhaps having a generic Filter types could assist in the future. In particular, I saw #1587 and figured it could be used there, too. The type is unnecessary for the current feature though, so it can be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought perhaps having a generic Filter types could assist in the future.
Could assist in future?... Sorry? Are you a member of our team?
In particular, I saw #1587 and figured it could be used there, too.
No, it is better to focus on the feature you're currently working on rather than trying to enhance or refactor other features and code areas. I've requested community help for #1587 since the author has no intention of contributing, so they have been unassigned.
I think this filter might be useful for SecurityOptions, but I am not entirely sure.
|
|
||
| namespace Ocelot.Filter | ||
| { | ||
| public class HttpStatusCodeFilter : Filter<HttpStatusCode> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This filter will likely become useless because defining the values of the C# HttpStatusCode enumeration is challenging for Ocelot users who aren’t .NET developers. I’ve already raised this issue here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I can change all this to be an integer based filter and not an HttpStatusCode enumeration based filter.
|
So, no, I didn't use Copilot or any AI. The only assistance I had was when I copied all the HttpStatusCode enum values to VS Code and did some find/replace to create the InlineData test instances. The rest is hand-written. I honestly didn't think this was particularly complex code. Regarding the question about whether to allow a user to add filters by int, string, or HttpStatusCode - I didn't know if you wanted to allow a user to specify both specific codes AND a code range like I went with explicit whitelist/blacklist because I wanted to avoid handling a conflict. If you're including specific status codes, then you're already implicitly excluding all the status codes not in the specified list. There's no need to specify other status codes to exclude, since they aren't included. The opposite is true - if you're specifying a blacklist, then that implies that everything not in your blacklist is included. If you would still like me to pursue allowing the user to specify codes to include AND codes to exclude, I am happy to iterate with you on what that logic looks like. As I'm writing this, I'm seeing some of your code comments come through, but they are on the commits you asked to be reverted. Do you still want me to revert them, or just make changes going forward? |
|
I have added a link to my LinkedIn profile to my GitHub profile. |
Neither I nor the community are interested in what you wanted. The design should be straightforward, but I noticed a lot of code in this project that violates the KISS principle, serving no real purpose and adding unnecessary clutter.
Fair enough! Look at these use cases and you will see there is no conflict. And you are absolutely right; positive codes exclude any unwanted values. No opposite codes here: "CacheOptions": {
"TtlSeconds": 888,
"StatusCodes": [200, 301, 302, 303] // only positive codes to enable caching
}Use case #741 covers the situation where users discussed the option of excluding a scenario. And no opposite codes here: "CacheOptions": {
"TtlSeconds": 888,
"StatusCodes": [-500, -404, -405, -410] // only negative codes to disable caching
}Looks good? The goal isn't to propose another option, like a blacklisting array. Both scenarios can be covered by a single
No need to rush, as this PR will be part of the Winter'26 milestone. The upcoming .NET 10 release will be free of new features, focusing instead on project upgrades, code reorganization, DevOps and possible bug fixes. Please enjoy your weekend. |
|
Hi Raman, To defend my design choice - in #2337 (comment), you mentioned you wanted to explicitly add an exclusion scenario, which I take to mean that everything will be cached except for the codes in the list. This sounds like a black list to me, hence the FilterType That said, I am only trying to make a contribution so I am happy to pivot and use the positive/negative convention. I have a follow-up question on the validation logic: if there is a mix, then what is the expected behavior? A couple of ideas
If a list is not specified, should the caching logic default to the existing behavior or do you want to implement a default include list? The latter could be considered a breaking change. If you want a default or best practice list, what list do you want used? An additional follow-up regarding the 2xx/3xx/etc . In #2337 (comment), your Idea 1 reads to me that you want As I thought about this some more, I thought about non-standard status codes outside the range of 100-599. Should the user be allowed to specify 000-099 and 600-999 status codes? These might be used in custom systems. If not, how should the validation logic handle them? I will likely be out of reach until mid-next week. If I can contribute when I return and you are willing to keep dealing with me, I will be happy to do so. Alternatively, if you would prefer to close the PR pending further requirements gathering (which admittedly I failed to do prior to submitting the PR), I understand. Thanks, Cliff |
|
Ok, let me check that, @raman-m |
Would you like to mentor this PR? |
ggnaegi
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For me, the code is fine, but I have some concerns about its maintainability, because CachedResponse uses HttpStatusCode while here we’re using an array of int. I think it would be more appropriate to convert the array in CacheOptions and then use HttpStatusCode consistently throughout.
|
@ggnaegi - any other changes you'd like to see? Happy to make them. |
|
@ggnaegi requested changes on December 6
This was my idea, and I requested changes after my code review. I think using integers is the simplest approach for users who are not familiar with C#.NET or are not C# developers. I was also concerned that parsing Enum types from JSON with the
It seems like an acceptable solution if offer both options, which can then be merged into a single collection within the Your opinion, Guil? |
Closes #741
Proposed Changes
HttpStatusCodetoFileCacheOptionsandCacheOptionsto act as a whitelist of status codes.