Ik ben bezig met een ASP.NET Core project, en ik merk dat ik in een bepaalde controller ontzettend vaak dezelfde code aan het copy/pasten ben. Dit is natuurlijk niet handig, dus ik zocht een manier om dit te verhelpen.
De controller gaat over een soort van admin interface dat gebruikers kunnen gebruiken om hun account te beheren. Denk een beetje aan een soort Play Store voor apps, je kan apps toevoegen en beheren. In mijn geval heet een app een 'theme'.
In elke action moet ik twee dingen doen:
- Het theme ophalen vanuit de database
- Verifieren dat de huidige gebruiker ook echt de eigenaar van het theme is.
De code ziet er in bijna elke action hetzelfde uit:
Regels 4 - 14 komen in vrijwel elke action terug, wat natuurlijk niet handig is.
Nu begrijp ik dat dit soort checks thuishoren in een Filter. Na wat zoekwerk kom ik op de volgende implementatie:
In principe werkt dit, en kan ik deze code steeds laten draaien door een [ValidateThemeOwner] attribuut te gebruiken.
De vraag nu: ik schiet hier nog steeds niet veel mee op. Ik heb namelijk nog steeds de theme en de user nodig in de action om naar de view te sturen. Ik moet dus in de action alsnog de theme en de user ophalen. Het enige verschil is dat ik er nu "veilig" vanuit kan gaan dat ze beide bestaan en dat de "check for owner" niet meer nodig is. Maar dat zijn twee regels code die ik schrap. Daar tegenover staat dat ik nu twee keer een database query moet doen (een keer in de filter, daarna nog eens in de action). Lijkt me niet goed..?
Hoe stuur ik de theme en user vanuit de filter (die ik daar toch al heb) naar de action (waar ik ze nodig heb en niet opnieuw wil gaan zoeken)?
Ik kan twee manieren vinden op internet:
1. RouteData gebruiken. In de Filter kan ik zoiets doen:
Daarna weer ophalen uit de RouteData in de action. Ok, maar is niet zo handig want ik ga nu "magic strings" gebruiken ("theme"). Niet heel erg flexibel en niet zo mooi. Natuurlijk kan ik een const definieren ergens maar probleem blijft.
2. De objecten aan de ActionParameters toevoegen:
In principle vergelijkbaar probleem met magic strings. Daarnaast is dit nog veel slechter denk ik omdat er nu ineens "random" parameters aan de GET method toegevoegd worden die er eigenlijk niet thuishoren en die door een filter gevuld worden. Code wordt compleet onleesbaar en de API klopt niet eens meer (GET signature zal niet kloppen want je moet geen theme en user meesturen).
Hoe los ik dit op een fatsoenlijke manier op?
De controller gaat over een soort van admin interface dat gebruikers kunnen gebruiken om hun account te beheren. Denk een beetje aan een soort Play Store voor apps, je kan apps toevoegen en beheren. In mijn geval heet een app een 'theme'.
In elke action moet ik twee dingen doen:
- Het theme ophalen vanuit de database
- Verifieren dat de huidige gebruiker ook echt de eigenaar van het theme is.
De code ziet er in bijna elke action hetzelfde uit:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| [HttpGet] public async Task<IActionResult> Dashboard(int? id) { // Find theme var theme = await _storeRepository.GetThemeAsync(id); if (theme == null || theme.Deleted) return NotFound(); // Find user var user = await GetCurrentUserAsync(); // Verify user is theme owner if (user.Id != theme.OwnerId) return Unauthorized(); var model = new ThemeStoreViewModel(); model.User = user; model.Theme = theme; return View(model); } |
Regels 4 - 14 komen in vrijwel elke action terug, wat natuurlijk niet handig is.
Nu begrijp ik dat dit soort checks thuishoren in een Filter. Na wat zoekwerk kom ik op de volgende implementatie:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| public class ValidateThemeOwnerAttribute : TypeFilterAttribute { public ValidateThemeOwnerAttribute() : base(typeof(ValidateThemeOwnerImpl)) { } private class ValidateThemeOwnerImpl : IAsyncActionFilter { private readonly StoreRepository _repository; public ValidateThemeOwnerImpl(StoreRepository repository) { _repository = repository; } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { if (context.ActionArguments.ContainsKey("id")) { // Get theme by id var id = context.ActionArguments["id"] as int?; var theme = await _repository.GetThemeAsync(id); // Check exists if (theme == null || theme.Deleted) { context.Result = new NotFoundObjectResult(id.Value); return; } // Get user var controller = (UserManagerControllerBase) context.Controller; var user = await controller.GetCurrentUserAsync(); // Check is user is owner if (theme.OwnerId != user?.Id) { context.Result = new UnauthorizedResult(); return; } await next(); } } } } |
In principe werkt dit, en kan ik deze code steeds laten draaien door een [ValidateThemeOwner] attribuut te gebruiken.
De vraag nu: ik schiet hier nog steeds niet veel mee op. Ik heb namelijk nog steeds de theme en de user nodig in de action om naar de view te sturen. Ik moet dus in de action alsnog de theme en de user ophalen. Het enige verschil is dat ik er nu "veilig" vanuit kan gaan dat ze beide bestaan en dat de "check for owner" niet meer nodig is. Maar dat zijn twee regels code die ik schrap. Daar tegenover staat dat ik nu twee keer een database query moet doen (een keer in de filter, daarna nog eens in de action). Lijkt me niet goed..?
Hoe stuur ik de theme en user vanuit de filter (die ik daar toch al heb) naar de action (waar ik ze nodig heb en niet opnieuw wil gaan zoeken)?
Ik kan twee manieren vinden op internet:
1. RouteData gebruiken. In de Filter kan ik zoiets doen:
C#:
1
| context.RouteData.Values.Add("theme", theme); |
Daarna weer ophalen uit de RouteData in de action. Ok, maar is niet zo handig want ik ga nu "magic strings" gebruiken ("theme"). Niet heel erg flexibel en niet zo mooi. Natuurlijk kan ik een const definieren ergens maar probleem blijft.
2. De objecten aan de ActionParameters toevoegen:
C#:
1
2
3
4
5
6
7
8
| context.ActionParameters["theme"] = theme; context.ActionParameters["user"] = user; // In action: [ValidateThemeOwner] public async Task<IActionResult> Dashboard(int? id, Theme theme, User user) { } |
In principle vergelijkbaar probleem met magic strings. Daarnaast is dit nog veel slechter denk ik omdat er nu ineens "random" parameters aan de GET method toegevoegd worden die er eigenlijk niet thuishoren en die door een filter gevuld worden. Code wordt compleet onleesbaar en de API klopt niet eens meer (GET signature zal niet kloppen want je moet geen theme en user meesturen).
Hoe los ik dit op een fatsoenlijke manier op?