ViewSets Integration¶
Expose your selectors as REST API endpoints using Django REST Framework.
ODataSelectorViewSetMixin¶
The mixin provides list() and retrieve() actions with OData support:
from rest_framework import viewsets
from fc_selector.django.drf.viewsets import ODataSelectorViewSetMixin
from fc_selector.django.drf.serializers import ODataDTOSerializer
class BlogPostDTOSerializer(ODataDTOSerializer):
class Meta:
dto_class = BlogPostDTO
class BlogPostViewSet(ODataSelectorViewSetMixin, viewsets.GenericViewSet):
serializer_class = BlogPostDTOSerializer
selector_class = BlogPostSelector
This enables:
GET /api/posts/ # List all
GET /api/posts/1/ # Get by ID
GET /api/posts/?$filter=status eq 'published' # Filter
GET /api/posts/?$select=id,title # Select fields
GET /api/posts/?$expand=author # Expand relations
GET /api/posts/?$orderby=created_at desc # Sort
GET /api/posts/?$top=10&$skip=20 # Paginate
Custom Actions¶
Add custom endpoints that combine OData with business logic:
from rest_framework.decorators import action
from rest_framework.response import Response
from fc_selector.django.selector import QueryBuilder
class BlogPostViewSet(ODataSelectorViewSetMixin, viewsets.GenericViewSet):
serializer_class = BlogPostDTOSerializer
selector_class = BlogPostSelector
@action(detail=False, methods=['get'])
def published(self, request):
"""GET /api/posts/published/"""
query = QueryBuilder(request.META.get('QUERY_STRING', ''))
query.and_filter("status eq 'published'")
dtos = BlogPostSelector().get_many(query)
serializer = self.get_serializer(dtos, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'])
def featured(self, request):
"""GET /api/posts/featured/"""
query = QueryBuilder(request.META.get('QUERY_STRING', ''))
query.and_filter("featured eq true")
query.orderby("created_at desc")
dtos = BlogPostSelector().get_many(query)
serializer = self.get_serializer(dtos, many=True)
return Response(serializer.data)
@action(detail=False, methods=['get'], url_path='by-author/(?P<author_id>[^/.]+)')
def by_author(self, request, author_id=None):
"""GET /api/posts/by-author/1/"""
query = QueryBuilder(request.META.get('QUERY_STRING', ''))
query.and_filter(f"author/id eq {author_id}")
dtos = BlogPostSelector().get_many(query)
serializer = self.get_serializer(dtos, many=True)
return Response(serializer.data)
@action(detail=True, methods=['get'])
def stats(self, request, pk=None):
"""GET /api/posts/1/stats/"""
query = (
QueryBuilder()
.select('id', 'title', 'view_count', 'word_count')
.and_filter(f"id eq {pk}")
)
dto = BlogPostSelector().get_one(query)
if not dto:
return Response({'detail': 'Not found'}, status=404)
return Response({
'id': dto.id,
'title': dto.title,
'view_count': dto.view_count,
'word_count': dto.word_count,
})
URL Configuration¶
from rest_framework.routers import DefaultRouter
from .viewsets import BlogPostViewSet, AuthorViewSet
router = DefaultRouter()
router.register('posts', BlogPostViewSet, basename='posts')
router.register('authors', AuthorViewSet, basename='authors')
urlpatterns = [
path('api/', include(router.urls)),
]
Complete ViewSet Example¶
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, AllowAny
from fc_selector.django.selector import QueryBuilder
from fc_selector.django.drf.viewsets import ODataSelectorViewSetMixin
from fc_selector.django.drf.serializers import ODataDTOSerializer
from .selectors.blog_post import BlogPostSelector, BlogPostDTO
class BlogPostDTOSerializer(ODataDTOSerializer):
class Meta:
dto_class = BlogPostDTO
class BlogPostViewSet(ODataSelectorViewSetMixin, viewsets.GenericViewSet):
"""
Read-only ViewSet for BlogPost.
Endpoints:
GET /api/posts/ - List all posts
GET /api/posts/{id}/ - Get single post
GET /api/posts/published/ - Published posts only
GET /api/posts/featured/ - Featured posts only
"""
serializer_class = BlogPostDTOSerializer
selector_class = BlogPostSelector
permission_classes = [AllowAny]
@action(detail=False, methods=['get'])
def published(self, request):
query = QueryBuilder(request.META.get('QUERY_STRING', ''))
query.and_filter("status eq 'published'")
dtos = BlogPostSelector().get_many(query)
return Response(self.get_serializer(dtos, many=True).data)
@action(detail=False, methods=['get'])
def featured(self, request):
query = QueryBuilder(request.META.get('QUERY_STRING', ''))
query.and_filter("featured eq true")
dtos = BlogPostSelector().get_many(query)
return Response(self.get_serializer(dtos, many=True).data)
Cross-Entity Queries¶
Query related entities from parent endpoints:
class AuthorViewSet(ODataSelectorViewSetMixin, viewsets.GenericViewSet):
serializer_class = AuthorDTOSerializer
selector_class = AuthorSelector
@action(detail=True, methods=['get'])
def posts(self, request, pk=None):
"""GET /api/authors/1/posts/"""
# Verify author exists
if not AuthorSelector().exists_by(
QueryBuilder().and_filter(f"id eq {pk}")
):
return Response({'detail': 'Author not found'}, status=404)
# Get author's posts with OData support
query = QueryBuilder(request.META.get('QUERY_STRING', ''))
query.and_filter(f"author/id eq {pk}")
dtos = BlogPostSelector().get_many(query)
serializer = BlogPostDTOSerializer(dtos, many=True)
return Response(serializer.data)
class CategoryViewSet(ODataSelectorViewSetMixin, viewsets.GenericViewSet):
serializer_class = CategoryDTOSerializer
selector_class = CategorySelector
@action(detail=True, methods=['get'])
def posts(self, request, pk=None):
"""GET /api/categories/1/posts/"""
# ManyToMany: use any() in filter
query = QueryBuilder(request.META.get('QUERY_STRING', ''))
query.and_filter(f"categories/any(c: c/id eq {pk})")
dtos = BlogPostSelector().get_many(query)
serializer = BlogPostDTOSerializer(dtos, many=True)
return Response(serializer.data)
OpenAPI / Swagger Documentation¶
FC Selector provides pre-defined OData parameters for drf-spectacular to document your API.
Setup¶
# settings.py
REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
SPECTACULAR_SETTINGS = {
'TITLE': 'My API',
'VERSION': '1.0.0',
}
Adding OData Parameters to Swagger¶
Import and use ODATA_PARAMETERS with @extend_schema:
from drf_spectacular.utils import extend_schema, extend_schema_view
from fc_selector.django.drf import ODATA_PARAMETERS, ODATA_RETRIEVE_PARAMETERS
@extend_schema_view(
list=extend_schema(parameters=ODATA_PARAMETERS),
retrieve=extend_schema(parameters=ODATA_RETRIEVE_PARAMETERS),
)
class BlogPostViewSet(ODataSelectorViewSetMixin, viewsets.GenericViewSet):
serializer_class = BlogPostDTOSerializer
selector_class = BlogPostSelector
Available Parameter Sets¶
| Import | Parameters | Use for |
|---|---|---|
ODATA_PARAMETERS |
All 7 parameters | list actions |
ODATA_RETRIEVE_PARAMETERS |
$select, $expand |
retrieve actions |
Custom Actions¶
Add OData parameters to custom actions:
from drf_spectacular.utils import extend_schema
from fc_selector.django.drf import ODATA_PARAMETERS
class BlogPostViewSet(ODataSelectorViewSetMixin, viewsets.GenericViewSet):
@extend_schema(parameters=ODATA_PARAMETERS)
@action(detail=False, methods=['get'])
def published(self, request):
...
Complete Example with Swagger¶
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from drf_spectacular.utils import extend_schema, extend_schema_view
from fc_selector.django.selector import QueryBuilder
from fc_selector.django.drf.viewsets import ODataSelectorViewSetMixin
from fc_selector.django.drf import ODATA_PARAMETERS, ODATA_RETRIEVE_PARAMETERS
@extend_schema_view(
list=extend_schema(
parameters=ODATA_PARAMETERS,
description="List posts with OData filtering, sorting, and pagination.",
),
retrieve=extend_schema(
parameters=ODATA_RETRIEVE_PARAMETERS,
description="Get a single post. Supports $select and $expand.",
),
)
class BlogPostViewSet(ODataSelectorViewSetMixin, viewsets.GenericViewSet):
serializer_class = BlogPostDTOSerializer
selector_class = BlogPostSelector
@extend_schema(parameters=ODATA_PARAMETERS)
@action(detail=False, methods=['get'])
def published(self, request):
"""List published posts only."""
query = QueryBuilder(request.META.get('QUERY_STRING', ''))
query.and_filter("status eq 'published'")
dtos = self.selector_class().get_many(query)
return Response(self.get_serializer(dtos, many=True).data)
Documented Parameters in Swagger¶
Each parameter includes:
- $filter - OData filter syntax with operators and examples
- $select - Comma-separated field list
- $expand - Relations to eager load
- $orderby - Sort fields with asc/desc
- $top - Limit results
- $skip - Offset for pagination
- $count - Include total count
Read-Only by Design¶
ViewSets using ODataSelectorViewSetMixin are read-only:
- No
create(),update(),partial_update(),destroy()methods - Write operations should use repositories, not selectors
- This follows the Selector pattern from DDD
If you need write operations, use a separate repository:
class BlogPostViewSet(ODataSelectorViewSetMixin, viewsets.GenericViewSet):
# Read operations via selector
selector_class = BlogPostSelector
@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
# Write via repository (not selector)
repository = BlogPostRepository()
repository.update(pk, status='published')
# Return updated data via selector
dto = BlogPostSelector().get_by_pk(pk)
return Response(self.get_serializer(dto).data)