From 4bd36d8614268c1e7aa0b6eaab1794b51f61db33 Mon Sep 17 00:00:00 2001 From: Rick Anderson Date: Wed, 13 Nov 2019 16:48:18 -1000 Subject: [PATCH 1/6] ODATA Expand migration --- .../sample/odata-expand/CU_expand.csproj | 13 + .../Controllers/EnrollmentController.cs | 30 +++ .../Controllers/ValuesController.cs | 45 ++++ .../sample/odata-expand/Data/DbInitializer.cs | 253 ++++++++++++++++++ .../sample/odata-expand/Data/SchoolContext.cs | 87 ++++++ .../sample/odata-expand/Models/Course.cs | 50 ++++ .../odata-expand/Models/CourseAssignment.cs | 15 ++ .../sample/odata-expand/Models/Department.cs | 73 +++++ .../sample/odata-expand/Models/Enrollment.cs | 52 ++++ .../sample/odata-expand/Models/Instructor.cs | 37 +++ .../odata-expand/Models/OfficeAssignment.cs | 16 ++ .../SchoolViewModels/AssignedCourseData.cs | 9 + .../SchoolViewModels/CourseViewModel.cs | 12 + .../SchoolViewModels/EnrollmentDateGroup.cs | 13 + .../SchoolViewModels/InstructorIndexData.cs | 14 + .../sample/odata-expand/Models/Student.cs | 133 +++++++++ .../sample/odata-expand/Models/StudentVM.cs | 13 + .../odata-expand/Models/StudentZsecret.cs | 12 + .../ODataValidators/MyEnableQueryAttribute.cs | 25 ++ .../ODataValidators/MyExpandValidator.cs | 34 +++ .../sample/odata-expand/PaginatedList.cs | 49 ++++ .../sample/odata-expand/Program.cs | 138 ++++++++++ .../sample/odata-expand/Startup.cs | 62 +++++ .../odata-expand/appsettings.Development.json | 9 + .../sample/odata-expand/appsettings.json | 11 + Odata-docs/webapi/odata-expand.md | 128 +++++++++ 26 files changed, 1333 insertions(+) create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/CU_expand.csproj create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/Controllers/EnrollmentController.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/Controllers/ValuesController.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/Data/DbInitializer.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/Data/SchoolContext.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/Course.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/CourseAssignment.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/Department.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/Enrollment.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/Instructor.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/OfficeAssignment.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/SchoolViewModels/AssignedCourseData.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/SchoolViewModels/CourseViewModel.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/SchoolViewModels/EnrollmentDateGroup.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/SchoolViewModels/InstructorIndexData.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/Student.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/StudentVM.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/StudentZsecret.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/ODataValidators/MyEnableQueryAttribute.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/ODataValidators/MyExpandValidator.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/PaginatedList.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/Program.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/Startup.cs create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/appsettings.Development.json create mode 100644 Odata-docs/webapi/odata-advanced/sample/odata-expand/appsettings.json create mode 100644 Odata-docs/webapi/odata-expand.md diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/CU_expand.csproj b/Odata-docs/webapi/odata-advanced/sample/odata-expand/CU_expand.csproj new file mode 100644 index 00000000..444b5f64 --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/CU_expand.csproj @@ -0,0 +1,13 @@ + + + + netcoreapp2.2 + + + + + + + + + diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/Controllers/EnrollmentController.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Controllers/EnrollmentController.cs new file mode 100644 index 00000000..db497ff9 --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Controllers/EnrollmentController.cs @@ -0,0 +1,30 @@ +//#define First +using ContosoUniversity.Models; +using ContosoUniversity.ODataValidators; +using Microsoft.AspNet.OData; +using Microsoft.AspNetCore.Mvc; +using System.Linq; + +namespace ContosoUniversity.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class EnrollmentController : ControllerBase + { +#if First + #region snippet_EnableQuery + [HttpGet, EnableQuery] + public IQueryable Get([FromServices]SchoolContext context) + => context.Enrollment; + #endregion + +#else + +#region snippet_MyEnableQuery + [HttpGet, MyEnableQuery] + public IQueryable Get([FromServices]SchoolContext context) + => context.Enrollment; +#endregion +#endif + } +} diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/Controllers/ValuesController.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Controllers/ValuesController.cs new file mode 100644 index 00000000..06654a3b --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Controllers/ValuesController.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace TodoApi.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class ValuesController : ControllerBase + { + // GET api/values + [HttpGet] + public ActionResult> Get() + { + return new string[] { "value1", "value2" }; + } + + // GET api/values/5 + [HttpGet("{id}")] + public ActionResult Get(int id) + { + return "value"; + } + + // POST api/values + [HttpPost] + public void Post([FromBody] string value) + { + } + + // PUT api/values/5 + [HttpPut("{id}")] + public void Put(int id, [FromBody] string value) + { + } + + // DELETE api/values/5 + [HttpDelete("{id}")] + public void Delete(int id) + { + } + } +} diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/Data/DbInitializer.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Data/DbInitializer.cs new file mode 100644 index 00000000..ed3b8dae --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Data/DbInitializer.cs @@ -0,0 +1,253 @@ +using System; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using ContosoUniversity.Models; + +namespace ContosoUniversity.Data +{ + public static class DbInitializer + { + public static void Initialize(SchoolContext context) + { + //context.Database.EnsureCreated(); + + // Look for any students. + if (context.Student.Any()) + { + return; // DB has been seeded + } + + var students = new Student[] + { + new Student { FirstMidName = "Carson", LastName = "Alexander", + EnrollmentDate = DateTime.Parse("2010-09-01") }, + new Student { FirstMidName = "Meredith", LastName = "Alonso", + EnrollmentDate = DateTime.Parse("2012-09-01") }, + new Student { FirstMidName = "Arturo", LastName = "Anand", + EnrollmentDate = DateTime.Parse("2013-09-01") }, + new Student { FirstMidName = "Gytis", LastName = "Barzdukas", + EnrollmentDate = DateTime.Parse("2012-09-01") }, + new Student { FirstMidName = "Yan", LastName = "Li", + EnrollmentDate = DateTime.Parse("2012-09-01") }, + new Student { FirstMidName = "Peggy", LastName = "Justice", + EnrollmentDate = DateTime.Parse("2011-09-01") }, + new Student { FirstMidName = "Laura", LastName = "Norman", + EnrollmentDate = DateTime.Parse("2013-09-01") }, + new Student { FirstMidName = "Nino", LastName = "Olivetto", + EnrollmentDate = DateTime.Parse("2005-09-01") } + }; + + foreach (Student s in students) + { + context.Student.Add(s); + } + context.SaveChanges(); + + var instructors = new Instructor[] + { + new Instructor { FirstMidName = "Kim", LastName = "Abercrombie", + HireDate = DateTime.Parse("1995-03-11") }, + new Instructor { FirstMidName = "Fadi", LastName = "Fakhouri", + HireDate = DateTime.Parse("2002-07-06") }, + new Instructor { FirstMidName = "Roger", LastName = "Harui", + HireDate = DateTime.Parse("1998-07-01") }, + new Instructor { FirstMidName = "Candace", LastName = "Kapoor", + HireDate = DateTime.Parse("2001-01-15") }, + new Instructor { FirstMidName = "Roger", LastName = "Zheng", + HireDate = DateTime.Parse("2004-02-12") } + }; + + foreach (Instructor i in instructors) + { + context.Instructors.Add(i); + } + context.SaveChanges(); + + var departments = new Department[] + { + new Department { Name = "English", Budget = 350000, + StartDate = DateTime.Parse("2007-09-01"), + InstructorID = instructors.Single( i => i.LastName == "Abercrombie").ID }, + new Department { Name = "Mathematics", Budget = 100000, + StartDate = DateTime.Parse("2007-09-01"), + InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID }, + new Department { Name = "Engineering", Budget = 350000, + StartDate = DateTime.Parse("2007-09-01"), + InstructorID = instructors.Single( i => i.LastName == "Harui").ID }, + new Department { Name = "Economics", Budget = 100000, + StartDate = DateTime.Parse("2007-09-01"), + InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID } + }; + + foreach (Department d in departments) + { + context.Departments.Add(d); + } + context.SaveChanges(); + + var courses = new Course[] + { + new Course {CourseID = 1050, Title = "Chemistry", Credits = 3, + DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID + }, + new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3, + DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID + }, + new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3, + DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID + }, + new Course {CourseID = 1045, Title = "Calculus", Credits = 4, + DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID + }, + new Course {CourseID = 3141, Title = "Trigonometry", Credits = 4, + DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID + }, + new Course {CourseID = 2021, Title = "Composition", Credits = 3, + DepartmentID = departments.Single( s => s.Name == "English").DepartmentID + }, + new Course {CourseID = 2042, Title = "Literature", Credits = 4, + DepartmentID = departments.Single( s => s.Name == "English").DepartmentID + }, + }; + + foreach (Course c in courses) + { + context.Courses.Add(c); + } + context.SaveChanges(); + + var officeAssignments = new OfficeAssignment[] + { + new OfficeAssignment { + InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID, + Location = "Smith 17" }, + new OfficeAssignment { + InstructorID = instructors.Single( i => i.LastName == "Harui").ID, + Location = "Gowan 27" }, + new OfficeAssignment { + InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID, + Location = "Thompson 304" }, + }; + + foreach (OfficeAssignment o in officeAssignments) + { + context.OfficeAssignments.Add(o); + } + context.SaveChanges(); + + var courseInstructors = new CourseAssignment[] + { + new CourseAssignment { + CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID, + InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID + }, + new CourseAssignment { + CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID, + InstructorID = instructors.Single(i => i.LastName == "Harui").ID + }, + new CourseAssignment { + CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID, + InstructorID = instructors.Single(i => i.LastName == "Zheng").ID + }, + new CourseAssignment { + CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID, + InstructorID = instructors.Single(i => i.LastName == "Zheng").ID + }, + new CourseAssignment { + CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID, + InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID + }, + new CourseAssignment { + CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID, + InstructorID = instructors.Single(i => i.LastName == "Harui").ID + }, + new CourseAssignment { + CourseID = courses.Single(c => c.Title == "Composition" ).CourseID, + InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID + }, + new CourseAssignment { + CourseID = courses.Single(c => c.Title == "Literature" ).CourseID, + InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID + }, + }; + + foreach (CourseAssignment ci in courseInstructors) + { + context.CourseAssignments.Add(ci); + } + context.SaveChanges(); + + var enrollments = new Enrollment[] + { + new Enrollment { + StudentID = students.Single(s => s.LastName == "Alexander").ID, + CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID, + Grade = Grade.A + }, + new Enrollment { + StudentID = students.Single(s => s.LastName == "Alexander").ID, + CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID, + Grade = Grade.C + }, + new Enrollment { + StudentID = students.Single(s => s.LastName == "Alexander").ID, + CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID, + Grade = Grade.B + }, + new Enrollment { + StudentID = students.Single(s => s.LastName == "Alonso").ID, + CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID, + Grade = Grade.B + }, + new Enrollment { + StudentID = students.Single(s => s.LastName == "Alonso").ID, + CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID, + Grade = Grade.B + }, + new Enrollment { + StudentID = students.Single(s => s.LastName == "Alonso").ID, + CourseID = courses.Single(c => c.Title == "Composition" ).CourseID, + Grade = Grade.B + }, + new Enrollment { + StudentID = students.Single(s => s.LastName == "Anand").ID, + CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID + }, + new Enrollment { + StudentID = students.Single(s => s.LastName == "Anand").ID, + CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID, + Grade = Grade.B + }, + new Enrollment { + StudentID = students.Single(s => s.LastName == "Barzdukas").ID, + CourseID = courses.Single(c => c.Title == "Chemistry").CourseID, + Grade = Grade.B + }, + new Enrollment { + StudentID = students.Single(s => s.LastName == "Li").ID, + CourseID = courses.Single(c => c.Title == "Composition").CourseID, + Grade = Grade.B + }, + new Enrollment { + StudentID = students.Single(s => s.LastName == "Justice").ID, + CourseID = courses.Single(c => c.Title == "Literature").CourseID, + Grade = Grade.B + } + }; + + foreach (Enrollment e in enrollments) + { + var enrollmentInDataBase = context.Enrollment.Where( + s => + s.Student.ID == e.StudentID && + s.Course.CourseID == e.CourseID).SingleOrDefault(); + if (enrollmentInDataBase == null) + { + context.Enrollment.Add(e); + } + } + context.SaveChanges(); + } + } +} diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/Data/SchoolContext.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Data/SchoolContext.cs new file mode 100644 index 00000000..6da4ad48 --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Data/SchoolContext.cs @@ -0,0 +1,87 @@ +#define BeforeInheritance // BeforeInheritance //Intro // AfterInheritance // or Intro or TableNames or BeforeInheritance + +#if Intro +#region snippet_Intro +using Microsoft.EntityFrameworkCore; + +namespace ContosoUniversity.Models +{ + public class SchoolContext : DbContext + { + public SchoolContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Student { get; set; } + public DbSet Enrollment { get; set; } + public DbSet Course { get; set; } + } +} +#endregion + +#elif TableNames +#region snippet_TableNames +using ContosoUniversity.Models; +using Microsoft.EntityFrameworkCore; + +namespace ContosoUniversity.Data +{ + public class SchoolContext : DbContext + { + public SchoolContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Enrollment { get; set; } + public DbSet Student { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ToTable("Course"); + modelBuilder.Entity().ToTable("Enrollment"); + modelBuilder.Entity().ToTable("Student"); + } + } +} +#endregion + +#elif BeforeInheritance +#region snippet_BeforeInheritance +using ContosoUniversity.Models; +using Microsoft.EntityFrameworkCore; + +namespace ContosoUniversity.Models +{ + public class SchoolContext : DbContext + { + public SchoolContext(DbContextOptions options) : base(options) + { + } + + public DbSet Courses { get; set; } + public DbSet Enrollment { get; set; } + public DbSet Student { get; set; } + public DbSet Departments { get; set; } + public DbSet Instructors { get; set; } + public DbSet OfficeAssignments { get; set; } + public DbSet CourseAssignments { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity().ToTable("Course"); + modelBuilder.Entity().ToTable("Enrollment"); + modelBuilder.Entity().ToTable("Student"); + modelBuilder.Entity().ToTable("Department"); + modelBuilder.Entity().ToTable("Instructor"); + modelBuilder.Entity().ToTable("OfficeAssignment"); + modelBuilder.Entity().ToTable("CourseAssignment"); + + modelBuilder.Entity() + .HasKey(c => new { c.CourseID, c.InstructorID }); + } + } +} +#endregion +#endif diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/Course.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/Course.cs new file mode 100644 index 00000000..aa97a659 --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/Course.cs @@ -0,0 +1,50 @@ +#define Final // Final // or Intro + +#if Intro +#region snippet_Intro +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ContosoUniversity.Models +{ + public class Course + { + [DatabaseGenerated(DatabaseGeneratedOption.None)] + public int CourseID { get; set; } + public string Title { get; set; } + public int Credits { get; set; } + + public ICollection Enrollments { get; set; } + } +} +#endregion + +#elif Final +#region snippet_Final +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ContosoUniversity.Models +{ + public class Course + { + [DatabaseGenerated(DatabaseGeneratedOption.None)] + [Display(Name = "Number")] + public int CourseID { get; set; } + + [StringLength(50, MinimumLength = 3)] + public string Title { get; set; } + + [Range(0, 5)] + public int Credits { get; set; } + + public int DepartmentID { get; set; } + + public Department Department { get; set; } + public ICollection Enrollments { get; set; } + public ICollection CourseAssignments { get; set; } + } +} +#endregion +#endif diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/CourseAssignment.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/CourseAssignment.cs new file mode 100644 index 00000000..ee1a766e --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/CourseAssignment.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ContosoUniversity.Models +{ + public class CourseAssignment + { + public int InstructorID { get; set; } + public int CourseID { get; set; } + public Instructor Instructor { get; set; } + public Course Course { get; set; } + } +} diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/Department.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/Department.cs new file mode 100644 index 00000000..1f0390b2 --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/Department.cs @@ -0,0 +1,73 @@ +#define Begin // Begin // Final + +#if Begin +#region snippet_Begin +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ContosoUniversity.Models +{ + public class Department + { + public int DepartmentID { get; set; } + + [StringLength(50, MinimumLength = 3)] + public string Name { get; set; } + + [DataType(DataType.Currency)] + [Column(TypeName = "money")] + public decimal Budget { get; set; } + + [DataType(DataType.Date)] + [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] + [Display(Name = "Start Date")] + public DateTime StartDate { get; set; } + + public int? InstructorID { get; set; } + + public Instructor Administrator { get; set; } + public ICollection Courses { get; set; } + } +} +#endregion + + +#elif Final +#region snippet_Final +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ContosoUniversity.Models +{ + public class Department + { + public int DepartmentID { get; set; } + + [StringLength(50, MinimumLength = 3)] + public string Name { get; set; } + + [DataType(DataType.Currency)] + [Column(TypeName = "money")] + public decimal Budget { get; set; } + + [DataType(DataType.Date)] + [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] + [Display(Name = "Start Date")] + public DateTime StartDate { get; set; } + + public int? InstructorID { get; set; } + + [Timestamp] + public byte[] RowVersion { get; set; } + + public Instructor Administrator { get; set; } + public ICollection Courses { get; set; } + } +} +#endregion +#endif + diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/Enrollment.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/Enrollment.cs new file mode 100644 index 00000000..543a4ab8 --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/Enrollment.cs @@ -0,0 +1,52 @@ +#define Final // Final // or Intro + +#if Intro +#region snippet_Intro +namespace ContosoUniversity.Models +{ + public enum Grade + { + A, B, C, D, F + } + +#region snippet_Intro2 + public class Enrollment + { + public int EnrollmentID { get; set; } + public int CourseID { get; set; } + public int StudentID { get; set; } + public Grade? Grade { get; set; } + + public Course Course { get; set; } + public Student Student { get; set; } + } +#endregion +} +#endregion + +#elif Final +#region snippet_Final +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ContosoUniversity.Models +{ + public enum Grade + { + A, B, C, D, F + } + + public class Enrollment + { + public int EnrollmentID { get; set; } + public int CourseID { get; set; } + public int StudentID { get; set; } + [DisplayFormat(NullDisplayText = "No grade")] + public Grade? Grade { get; set; } + + public Course Course { get; set; } + public Student Student { get; set; } + } +} +#endregion +#endif diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/Instructor.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/Instructor.cs new file mode 100644 index 00000000..5e3d9bee --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/Instructor.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ContosoUniversity.Models +{ + public class Instructor + { + public int ID { get; set; } + + [Required] + [Display(Name = "Last Name")] + [StringLength(50)] + public string LastName { get; set; } + + [Required] + [Column("FirstName")] + [Display(Name = "First Name")] + [StringLength(50)] + public string FirstMidName { get; set; } + + [DataType(DataType.Date)] + [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] + [Display(Name = "Hire Date")] + public DateTime HireDate { get; set; } + + [Display(Name = "Full Name")] + public string FullName + { + get { return LastName + ", " + FirstMidName; } + } + + public ICollection CourseAssignments { get; set; } + public OfficeAssignment OfficeAssignment { get; set; } + } +} diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/OfficeAssignment.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/OfficeAssignment.cs new file mode 100644 index 00000000..e3140f94 --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/OfficeAssignment.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ContosoUniversity.Models +{ + public class OfficeAssignment + { + [Key] + public int InstructorID { get; set; } + [StringLength(50)] + [Display(Name = "Office Location")] + public string Location { get; set; } + + public Instructor Instructor { get; set; } + } +} diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/SchoolViewModels/AssignedCourseData.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/SchoolViewModels/AssignedCourseData.cs new file mode 100644 index 00000000..6fe7c0ee --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/SchoolViewModels/AssignedCourseData.cs @@ -0,0 +1,9 @@ +namespace ContosoUniversity.Models.SchoolViewModels +{ + public class AssignedCourseData + { + public int CourseID { get; set; } + public string Title { get; set; } + public bool Assigned { get; set; } + } +} diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/SchoolViewModels/CourseViewModel.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/SchoolViewModels/CourseViewModel.cs new file mode 100644 index 00000000..95ce1ea3 --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/SchoolViewModels/CourseViewModel.cs @@ -0,0 +1,12 @@ +namespace ContosoUniversity.Models.SchoolViewModels +{ +#region snippet + public class CourseViewModel + { + public int CourseID { get; set; } + public string Title { get; set; } + public int Credits { get; set; } + public string DepartmentName { get; set; } + } +#endregion +} diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/SchoolViewModels/EnrollmentDateGroup.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/SchoolViewModels/EnrollmentDateGroup.cs new file mode 100644 index 00000000..9bffc90a --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/SchoolViewModels/EnrollmentDateGroup.cs @@ -0,0 +1,13 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace ContosoUniversity.Models.SchoolViewModels +{ + public class EnrollmentDateGroup + { + [DataType(DataType.Date)] + public DateTime? EnrollmentDate { get; set; } + + public int StudentCount { get; set; } + } +} diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/SchoolViewModels/InstructorIndexData.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/SchoolViewModels/InstructorIndexData.cs new file mode 100644 index 00000000..6a93cc23 --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/SchoolViewModels/InstructorIndexData.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ContosoUniversity.Models.SchoolViewModels +{ + public class InstructorIndexData + { + public IEnumerable Instructors { get; set; } + public IEnumerable Courses { get; set; } + public IEnumerable Enrollments { get; set; } + } +} diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/Student.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/Student.cs new file mode 100644 index 00000000..76a2bc81 --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/Student.cs @@ -0,0 +1,133 @@ +#define BeforeInheritance // BeforeInheritance //Column // DataType // Intro // AfterInheritance // or Intro or StringLength or DataType or BeforeInheritance + +#if Intro +#region snippet_Intro +using System; +using System.Collections.Generic; + +namespace ContosoUniversity.Models +{ + public class Student + { + public int ID { get; set; } + public string LastName { get; set; } + public string FirstMidName { get; set; } + public DateTime EnrollmentDate { get; set; } + + public ICollection Enrollments { get; set; } + } +} +#endregion + +#elif DataType +#region snippet_DataType +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace ContosoUniversity.Models +{ + public class Student + { + public int ID { get; set; } + public string LastName { get; set; } + public string FirstMidName { get; set; } + [DataType(DataType.Date)] + [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] + public DateTime EnrollmentDate { get; set; } + + public ICollection Enrollments { get; set; } + } +} +#endregion + +#elif StringLength +#region snippet_StringLength +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace ContosoUniversity.Models +{ + public class Student + { + public int ID { get; set; } + [StringLength(50)] + public string LastName { get; set; } + [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")] + public string FirstMidName { get; set; } + [DataType(DataType.Date)] + [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] + public DateTime EnrollmentDate { get; set; } + + public ICollection Enrollments { get; set; } + } +} +#endregion + +#elif Column +#region snippet_Column +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ContosoUniversity.Models +{ + public class Student + { + public int ID { get; set; } + [StringLength(50)] + public string LastName { get; set; } + [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")] + [Column("FirstName")] + public string FirstMidName { get; set; } + [DataType(DataType.Date)] + [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] + public DateTime EnrollmentDate { get; set; } + + public ICollection Enrollments { get; set; } + } +} +#endregion + + +#elif BeforeInheritance +#region snippet_BeforeInheritance +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace ContosoUniversity.Models +{ + public class Student + { + public int ID { get; set; } + [Required] + [StringLength(50)] + [Display(Name = "Last Name")] + public string LastName { get; set; } + [Required] + [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")] + [Column("FirstName")] + [Display(Name = "First Name")] + public string FirstMidName { get; set; } + [DataType(DataType.Date)] + [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] + [Display(Name = "Enrollment Date")] + public DateTime EnrollmentDate { get; set; } + [Display(Name = "Full Name")] + public string FullName + { + get + { + return LastName + ", " + FirstMidName; + } + } + + public ICollection Enrollments { get; set; } + } +} +#endregion +#endif diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/StudentVM.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/StudentVM.cs new file mode 100644 index 00000000..465ca40f --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/StudentVM.cs @@ -0,0 +1,13 @@ +using System; + +namespace ContosoUniversity.Models +{ + public class StudentVM + { + public int ID { get; set; } + public string LastName { get; set; } + public string FirstMidName { get; set; } + public DateTime EnrollmentDate { get; set; } + } +} + diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/StudentZsecret.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/StudentZsecret.cs new file mode 100644 index 00000000..34273874 --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Models/StudentZsecret.cs @@ -0,0 +1,12 @@ +#if Intro +#region snippet_Intro +public class Student +{ + public int ID { get; set; } + public string LastName { get; set; } + public string FirstMidName { get; set; } + public DateTime EnrollmentDate { get; set; } + public string Secret { get; set; } +} +#endregion +#endif diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/ODataValidators/MyEnableQueryAttribute.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/ODataValidators/MyEnableQueryAttribute.cs new file mode 100644 index 00000000..a9e99ebe --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/ODataValidators/MyEnableQueryAttribute.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNet.OData; +using Microsoft.AspNet.OData.Query; +using Microsoft.AspNetCore.Http; + +namespace ContosoUniversity.ODataValidators +{ + #region snippet + public class MyEnableQueryAttribute : EnableQueryAttribute + { + private readonly DefaultQuerySettings defaultQuerySettings; + public MyEnableQueryAttribute() + { + this.defaultQuerySettings = new DefaultQuerySettings(); + this.defaultQuerySettings.EnableExpand = true; + this.defaultQuerySettings.EnableSelect = true; + } + public override void ValidateQuery(HttpRequest request, ODataQueryOptions queryOpts) + { + queryOpts.SelectExpand.Validator = + new MyExpandValidator(this.defaultQuerySettings); + base.ValidateQuery(request, queryOpts); + } + } + #endregion +} \ No newline at end of file diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/ODataValidators/MyExpandValidator.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/ODataValidators/MyExpandValidator.cs new file mode 100644 index 00000000..a78ffa9f --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/ODataValidators/MyExpandValidator.cs @@ -0,0 +1,34 @@ +using ContosoUniversity.Models; +using Microsoft.AspNet.OData.Query; +using Microsoft.AspNet.OData.Query.Validators; +using Microsoft.OData; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ContosoUniversity.ODataValidators +{ + #region snippet + public class MyExpandValidator : SelectExpandQueryValidator + { + public MyExpandValidator(DefaultQuerySettings defaultQuerySettings) + : base(defaultQuerySettings) + { + + } + public override void Validate(SelectExpandQueryOption selectExpandQueryOption, + ODataValidationSettings validationSettings) + { + if (selectExpandQueryOption.RawExpand.Contains(nameof(Course.CourseAssignments))) + { + throw new ODataException( + $"Query on {nameof(Course.CourseAssignments)} not allowed"); + } + + base.Validate(selectExpandQueryOption, validationSettings); + } + } + #endregion +} + diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/PaginatedList.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/PaginatedList.cs new file mode 100644 index 00000000..af0bd798 --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/PaginatedList.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace ContosoUniversity +{ + public class PaginatedList : List + { + public int PageIndex { get; private set; } + public int TotalPages { get; private set; } + + public PaginatedList(List items, int count, int pageIndex, int pageSize) + { + PageIndex = pageIndex; + TotalPages = (int)Math.Ceiling(count / (double)pageSize); + + this.AddRange(items); + } + + public bool HasPreviousPage + { + get + { + return (PageIndex > 1); + } + } + + public bool HasNextPage + { + get + { + return (PageIndex < TotalPages); + } + } + + public static async Task> CreateAsync( + IQueryable source, int pageIndex, int pageSize) + { + var count = await source.CountAsync(); + var items = await source.Skip( + (pageIndex - 1) * pageSize) + .Take(pageSize).ToListAsync(); + return new PaginatedList(items, count, pageIndex, pageSize); + } + } +} + diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/Program.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Program.cs new file mode 100644 index 00000000..81f6a2dd --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Program.cs @@ -0,0 +1,138 @@ +#define Migrate //Initialize // First +#if First +#region snippet +using ContosoUniversity.Models; // SchoolContext +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; // CreateScope +using Microsoft.Extensions.Logging; +using System; + +namespace ContosoUniversity +{ + public class Program + { + public static void Main(string[] args) + { + var host = CreateWebHostBuilder(args).Build(); + + using (var scope = host.Services.CreateScope()) + { + var services = scope.ServiceProvider; + + try + { + var context = services.GetRequiredService(); + context.Database.EnsureCreated(); + } + catch (Exception ex) + { + var logger = services.GetRequiredService>(); + logger.LogError(ex, "An error occurred creating the DB."); + } + } + + host.Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} +#endregion +#endif + +#if Initialize +using ContosoUniversity.Data; // DbInitializer +using ContosoUniversity.Models; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; // CreateScope +using Microsoft.Extensions.Logging; +using System; + +namespace ContosoUniversity +{ +#region snippet2 + public class Program + { + public static void Main(string[] args) + { + var host = CreateWebHostBuilder(args).Build(); + + using (var scope = host.Services.CreateScope()) + { + var services = scope.ServiceProvider; + + try + { + var context = services.GetRequiredService(); + // using ContosoUniversity.Data; + DbInitializer.Initialize(context); + } + catch (Exception ex) + { + var logger = services.GetRequiredService>(); + logger.LogError(ex, "An error occurred creating the DB."); + } + } + + host.Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +#endregion +} +#endif + +// Migrate version is a copy of previous version with context.Database.Migrate(); +// so downloads don't need to run database update + +#if Migrate +using ContosoUniversity.Data; // DbInitializer +using ContosoUniversity.Models; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; // CreateScope +using Microsoft.Extensions.Logging; +using System; + +namespace ContosoUniversity +{ + public class Program + { + public static void Main(string[] args) + { + var host = CreateWebHostBuilder(args).Build(); + + using (var scope = host.Services.CreateScope()) + { + var services = scope.ServiceProvider; + + try + { + var context = services.GetRequiredService(); + // using ContosoUniversity.Data; + DbInitializer.Initialize(context); + } + catch (Exception ex) + { + var logger = services.GetRequiredService>(); + logger.LogError(ex, "An error occurred creating the DB."); + } + } + + host.Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} +#endif diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/Startup.cs b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Startup.cs new file mode 100644 index 00000000..79eed278 --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/Startup.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.EntityFrameworkCore; +using ContosoUniversity.Models; +using Microsoft.AspNet.OData.Extensions; + +namespace ContosoUniversity +{ + #region snippet + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + + services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); + + services.AddDbContext(options => + options.UseInMemoryDatabase("OData-expand")); + + services.AddOData(); + } + + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseExceptionHandler("/Error"); + app.UseHsts(); + } + + app.UseHttpsRedirection(); + + app.UseMvc(routeBuilder => + { + routeBuilder.EnableDependencyInjection(); + routeBuilder.Expand().Select(); + }); + } + } + #endregion +} diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/appsettings.Development.json b/Odata-docs/webapi/odata-advanced/sample/odata-expand/appsettings.Development.json new file mode 100644 index 00000000..e203e940 --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/Odata-docs/webapi/odata-advanced/sample/odata-expand/appsettings.json b/Odata-docs/webapi/odata-advanced/sample/odata-expand/appsettings.json new file mode 100644 index 00000000..c14e3449 --- /dev/null +++ b/Odata-docs/webapi/odata-advanced/sample/odata-expand/appsettings.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "SchoolContext": "Server=(localdb)\\mssqllocaldb;Database=CU-1;Trusted_Connection=True;MultipleActiveResultSets=true" + } +} \ No newline at end of file diff --git a/Odata-docs/webapi/odata-expand.md b/Odata-docs/webapi/odata-expand.md new file mode 100644 index 00000000..4f43efcb --- /dev/null +++ b/Odata-docs/webapi/odata-expand.md @@ -0,0 +1,128 @@ +--- +title: "OData Expand" +author: FIVIL +description: Using OData expand to query related data +ms.author: riande +ms.custom: mvc +ms.date: 4/5/2019 +uid: web-api/advanced/odata-expand +--- + +# OData Expand + +By [FIVIL](https://twitter.com/F_IVI_L) and [Rick Anderson](https://twitter.com/RickAndMSFT) + +This article demonstrates querying related entities using [OData](https://www.odata.org/). + +The [ContosoUniversity sample](https://github.com/aspnet/AspNetCore.Docs/tree/master/aspnetcore/data/ef-rp/intro/samples/cu21) is used for the starter project. + +A malicious or naive client may construct a query that consumes excessive resources. Such a query can disrupt access to your service. Review before starting this tutorial. + + +[Download the completed app](https://github.com/aspnet/Docs/tree/master/aspnetcore/web-api/advanced/odata-advanced/sample/odata-expand) ([How to download](xref:index#how-to-download-a-sample)). +--> + +## Enable OData + +Update *Startup.cs* with the following highlighted code: + +[!code-csharp[](odata-advanced/sample/odata-expand/Startup.cs?highlight=19,36-40&name=snippet)] + +The preceding code: + +* Calls `services.AddOData();` to enable OData middleware. +* Calls `routeBuilder.Expand().Select()` to enable querying related entities with OData. + +## Add a controller + +Create new Controller named `EnrollmentController` and with the following action: + +[!code-csharp[](odata-advanced/sample/odata-expand/Controllers/EnrollmentController.cs?name=snippet_EnableQuery)] + +The preceding code enables OData queries and returns enrollment entities `SchoolContext`. + +## $expand + +OData `expand` functionality can be used to query related data. For example, to get the *Course* data for each *Enrollment* entity, include `?$expand=course` at the end of the request path: + +This tutorial uses Postman to test the web API. + +* Install [Postman](https://www.getpostman.com/apps) +* Start the web app. +* Start Postman. +* Disable **SSL certificate verification** + + * From **File > Settings** (**General* tab), disable **SSL certificate verification**. + > [!WARNING] + > Re-enable SSL certificate verification after testing the controller. + +* Create a new request. + * Set the HTTP method to `GET`. + * Set the request URL to `https://localhost:5001/api/Enrollment/?$expand=course($expand=Department)`. Change the port as necessary. +* Select **Send**. +* The *Course* data for each *Enrollment* entity is included in the response. + +## Expand depth + +Expand can be applied to more than one level of navigation property. For example, to get the *Department* data of each *Course* for each *Enrollment* entity, include `?$expand=course($expand=Department)` at the end of the request path. The following JSON shows a portion of the output: + +```json +[ + { + "Course": { + "Department": { + "DepartmentID": 3, + "Name": "Engineering", + "Budget": 350000, + "StartDate": "2007-09-01T00:00:00", + "InstructorID": 3 + }, + "CourseID": 1050, + "Title": "Chemistry", + "Credits": 3, + "DepartmentID": 3 + }, + "EnrollmentID": 1, + "CourseID": 1050, + "StudentID": 1, + "Grade": 0 + }, + { + "Course": { + +] +``` + +By default, Web API allows the maximum expansion depth of two. To override the default, set the `MaxExpansionDepth` property on the `[EnableQuery]` attribute. + +## Security concerns + +Consider disallowing expand: + +* On sensitive data for security reasons. +* On non-trivial data sets for performance reasons. + +In this section, code is added to prevent querying `CourseAssignments` related data. + +Override `SelectExpandQueryValidator` to prevent `$expand=CourseAssignments`. Create a new class named `MyExpandValidator` with the following code: + +[!code-csharp[](odata-advanced/sample/odata-expand/ODataValidators/MyExpandValidator.cs?name=snippet)] + +The preceding code throws an exception if `$expand` is used with `CourseAssignments`. + +Create a new class named `MyEnableQueryAttribute` with the following code: + +[!code-csharp[](odata-advanced/sample/odata-expand/ODataValidators/MyEnableQueryAttribute.cs?name=snippet)] + +The preceding code creates the `MyEnableQuery` attribute. The `MyEnableQuery` attribute adds the `MyExpandValidator`, which prevents `$expand=CourseAssignments` + +Replace the `EnableQuery` attribute with `MyEnableQuery` attribute in the `EnrollmentController`: + +[!code-csharp[](odata-advanced/sample/odata-expand/Controllers/EnrollmentController.cs?name=snippet_MyEnableQuery)] + +In Postman: + +* Send the previous `Get` request `https://localhost:5001/api/Enrollment/?$expand=course($expand=Department)`. The request returns data because `($expand=Department)` is not prohibited. +* Send a `Get` request for with `($expand=CourseAssignments)`. For example, `https://localhost:5001/api/Enrollment/?$expand=course($expand=CourseAssignments)` + + The preceding query returns `400 Bad Request`. \ No newline at end of file From ea891dff31ae423a1d59427da2385f45c8cab21f Mon Sep 17 00:00:00 2001 From: Rick Anderson Date: Wed, 13 Nov 2019 16:57:02 -1000 Subject: [PATCH 2/6] link --- Odata-docs/webapi/odata-expand.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Odata-docs/webapi/odata-expand.md b/Odata-docs/webapi/odata-expand.md index 4f43efcb..9a988c0f 100644 --- a/Odata-docs/webapi/odata-expand.md +++ b/Odata-docs/webapi/odata-expand.md @@ -16,7 +16,7 @@ This article demonstrates querying related entities using [OData](https://www.od The [ContosoUniversity sample](https://github.com/aspnet/AspNetCore.Docs/tree/master/aspnetcore/data/ef-rp/intro/samples/cu21) is used for the starter project. -A malicious or naive client may construct a query that consumes excessive resources. Such a query can disrupt access to your service. Review before starting this tutorial. +A malicious or naive client may construct a query that consumes excessive resources. Such a query can disrupt access to your service. [Download the completed app](https://github.com/aspnet/Docs/tree/master/aspnetcore/web-api/advanced/odata-advanced/sample/odata-expand) ([How to download](xref:index#how-to-download-a-sample)). From 84ef2afacd8c51eb5c04813c21420da77ee2ed92 Mon Sep 17 00:00:00 2001 From: Rick Anderson Date: Wed, 13 Nov 2019 17:11:25 -1000 Subject: [PATCH 3/6] link --- Odata-docs/webapi/odata-expand.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Odata-docs/webapi/odata-expand.md b/Odata-docs/webapi/odata-expand.md index 9a988c0f..6758beec 100644 --- a/Odata-docs/webapi/odata-expand.md +++ b/Odata-docs/webapi/odata-expand.md @@ -3,7 +3,6 @@ title: "OData Expand" author: FIVIL description: Using OData expand to query related data ms.author: riande -ms.custom: mvc ms.date: 4/5/2019 uid: web-api/advanced/odata-expand --- @@ -18,10 +17,6 @@ The [ContosoUniversity sample](https://github.com/aspnet/AspNetCore.Docs/tree/ma A malicious or naive client may construct a query that consumes excessive resources. Such a query can disrupt access to your service. - -[Download the completed app](https://github.com/aspnet/Docs/tree/master/aspnetcore/web-api/advanced/odata-advanced/sample/odata-expand) ([How to download](xref:index#how-to-download-a-sample)). ---> - ## Enable OData Update *Startup.cs* with the following highlighted code: From d74197742b09c15c5cf73767c33fe0bae8029a3c Mon Sep 17 00:00:00 2001 From: Rick Anderson Date: Wed, 13 Nov 2019 17:17:43 -1000 Subject: [PATCH 4/6] link --- Odata-docs/webapi/odata-expand.md | 1 - Odata-docs/webapi/odata-security.md | 81 +++++++++++++++++++ .../ODataAPI/Controllers/TodoController.cs | 50 ++++++++++++ .../ODataAPI/Controllers/ValuesController.cs | 80 ++++++++++++++++++ .../sample/ODataAPI/Models/Employee.cs | 17 ++++ .../sample/ODataAPI/Models/TodoContext.cs | 18 +++++ .../sample/ODataAPI/Models/TodoItem.cs | 22 +++++ .../sample/ODataAPI/ODataAPI.csproj | 15 ++++ .../ODataAttribute/MyEnableQueryAttribute.cs | 30 +++++++ .../MyFilterNavPropQueryValidator.cs | 29 +++++++ .../ODataAttribute/MyFilterQueryValidator.cs | 41 ++++++++++ .../ODataAttribute/MyOrderByValidator.cs | 29 +++++++ .../odata-security/sample/ODataAPI/Program.cs | 24 ++++++ .../odata-security/sample/ODataAPI/Startup.cs | 58 +++++++++++++ .../sample/ODataAPI/StartupEDM.cs | 61 ++++++++++++++ .../ODataAPI/appsettings.Development.json | 9 +++ .../sample/ODataAPI/appsettings.json | 8 ++ 17 files changed, 572 insertions(+), 1 deletion(-) create mode 100644 Odata-docs/webapi/odata-security.md create mode 100644 Odata-docs/webapi/odata-security/sample/ODataAPI/Controllers/TodoController.cs create mode 100644 Odata-docs/webapi/odata-security/sample/ODataAPI/Controllers/ValuesController.cs create mode 100644 Odata-docs/webapi/odata-security/sample/ODataAPI/Models/Employee.cs create mode 100644 Odata-docs/webapi/odata-security/sample/ODataAPI/Models/TodoContext.cs create mode 100644 Odata-docs/webapi/odata-security/sample/ODataAPI/Models/TodoItem.cs create mode 100644 Odata-docs/webapi/odata-security/sample/ODataAPI/ODataAPI.csproj create mode 100644 Odata-docs/webapi/odata-security/sample/ODataAPI/ODataAttribute/MyEnableQueryAttribute.cs create mode 100644 Odata-docs/webapi/odata-security/sample/ODataAPI/ODataAttribute/MyFilterNavPropQueryValidator.cs create mode 100644 Odata-docs/webapi/odata-security/sample/ODataAPI/ODataAttribute/MyFilterQueryValidator.cs create mode 100644 Odata-docs/webapi/odata-security/sample/ODataAPI/ODataAttribute/MyOrderByValidator.cs create mode 100644 Odata-docs/webapi/odata-security/sample/ODataAPI/Program.cs create mode 100644 Odata-docs/webapi/odata-security/sample/ODataAPI/Startup.cs create mode 100644 Odata-docs/webapi/odata-security/sample/ODataAPI/StartupEDM.cs create mode 100644 Odata-docs/webapi/odata-security/sample/ODataAPI/appsettings.Development.json create mode 100644 Odata-docs/webapi/odata-security/sample/ODataAPI/appsettings.json diff --git a/Odata-docs/webapi/odata-expand.md b/Odata-docs/webapi/odata-expand.md index 6758beec..c137397f 100644 --- a/Odata-docs/webapi/odata-expand.md +++ b/Odata-docs/webapi/odata-expand.md @@ -4,7 +4,6 @@ author: FIVIL description: Using OData expand to query related data ms.author: riande ms.date: 4/5/2019 -uid: web-api/advanced/odata-expand --- # OData Expand diff --git a/Odata-docs/webapi/odata-security.md b/Odata-docs/webapi/odata-security.md new file mode 100644 index 00000000..b145d0cb --- /dev/null +++ b/Odata-docs/webapi/odata-security.md @@ -0,0 +1,81 @@ +--- +title: Security Guidance for ASP.NET Core Web API OData +author: rick-anderson +description: Describes security issues to consider when exposing a dataset through OData for ASP.NET Core Web API +ms.author: riande +ms.date: 05/05/2019 +--- + +# Security guidance for ASP.NET Core Web API OData + +By [Mike Wasson](https://github.com/MikeWasson), [FIVIL](https://github.com/fivil) and [Rick Anderson](https://twitter.com/RickAndMSFT) + +This page describes some of the security issues that you should consider when exposing a dataset through [OData](https://www.odata.org/) for ASP.NET Core Web API. + +## Query security + +Suppose your model includes an `Employee` type with a `Salary` property. You might want to exclude this property to hide it from clients. Properties can be excluded with `[IgnoreDataMember]`: + +[!code-csharp[Main](odata-security/sample/ODataAPI/Models/Employee.cs?name=snippet)] + +A malicious or naive client can construct a query that: + +* Takes significant system resources. Such a query can disrupt your service. +* Leaks sensitive information from a clever join. + +The `[EnableQuery]` attribute is an action filter that parses, validates, and applies the query. The filter converts the query options into a [LINQ](/dotnet/csharp/linq/) expression. When the controller returns an type, the `IQueryable` LINQ provider converts the LINQ expression into a query. Therefore, performance depends on the LINQ provider that is used, and on the particular characteristics of the dataset or database schema. + + + +If all clients are trusted (for example, in an enterprise environment), or if the dataset is small, query performance might not be an issue. Otherwise, consider the following recommendations: + +- Test your service with anticipated queries and profile the DB. +- Enable server-driven paging to avoid returning a large data set in one query. + + [!code-csharp[Main](odata-security/sample/ODataAPI/Controllers/ValuesController.cs?name=snippet_PageSize)] + +- Does the app require `$filter` and `$orderby`? Some apps might allow client paging, using `$top` and `$skip`, but disable the other query options. + + [!code-csharp[Main](odata-security/sample/ODataAPI/Controllers/ValuesController.cs?name=snippet_AllowedQueryOptions)] + +- Consider restricting `$orderby` to properties in a clustered index. Sorting large data without a clustered index is resource-intensive. + + [!code-csharp[Main](odata-security/sample/ODataAPI/Controllers/ValuesController.cs?name=snippet_AllowedOrderByProperties)] + +- Maximum node count: The **MaxNodeCount** property on **[EnableQuery]** sets the maximum number nodes allowed in the `$filter` syntax tree. The default value is 100, but you may want to set a lower value. A large number of nodes can be slow to compile. This is important when using [LINQ to Objects](/dotnet/csharp/programming-guide/concepts/linq/linq-to-objects). + + [!code-csharp[Main](odata-security/sample/ODataAPI/Controllers/ValuesController.cs?name=snippet_MaxNodeCount)] +- Consider disabling the `any` and `all` functions, as these can be resource-intensive: + + [!code-csharp[Main](odata-security/sample/ODataAPI/Controllers/ValuesController.cs?name=snippet_any)] + +- If any string properties contain large strings—for example, a product description or a blog entry—consider disabling the string functions. + + [!code-csharp[Main](odata-security/sample/ODataAPI/Controllers/ValuesController.cs?name=snippet_large)] + +- Consider disallowing filtering on navigation properties. Filtering on navigation properties can result in a join. Joins can be slow, depending on the database schema. The following code shows a query validator that prevents filtering on navigation properties. + + [!code-csharp[Main](odata-security/sample/ODataAPI/ODataAttribute/MyFilterNavPropQueryValidator.cs?name=snippet)] + +- Consider restricting `$filter` queries by writing a validator that is customized for the database. For example, consider these two queries: + + - All movies with actors whose last name starts with `A`. + - All movies released in 1994. + + Unless movies are indexed by actors, the first query might require the DB engine to scan the entire list of movies. Whereas the second query might be acceptable, assuming movies are indexed by release year. + + The following code shows a validator that allows filtering on the `ReleaseYear` and `Title` properties but no other properties. + + [!code-csharp[Main](odata-security/sample/ODataAPI/ODataAttribute/MyFilterQueryValidator.cs?name=snippet)] + +- In general, consider which $filter functions are required. If clients don't need the full expressiveness of `$filter`, limit the allowed functions. + +## EDM security + +The query semantics are based on the [Entity Data Model](https://www.odata.org/documentation/odata-version-2-0/overview/) (EDM), not the underlying model types. You can exclude a property from the EDM and it will not be visible to the query. For example, suppose your model includes an `Employee` type with a `Salary` property. You might want to exclude this property from the EDM to hide it from clients. + +Properties can be excluded with `[IgnoreDataMember]` or programmatically with the EDM. The following code removes the `Salary` property from the EDM programmatically: + +[!code-csharp[Main](odata-security/sample/ODataAPI/StartupEDM.cs?name=snippet)] \ No newline at end of file diff --git a/Odata-docs/webapi/odata-security/sample/ODataAPI/Controllers/TodoController.cs b/Odata-docs/webapi/odata-security/sample/ODataAPI/Controllers/TodoController.cs new file mode 100644 index 00000000..fa4c4246 --- /dev/null +++ b/Odata-docs/webapi/odata-security/sample/ODataAPI/Controllers/TodoController.cs @@ -0,0 +1,50 @@ +using Microsoft.AspNetCore.Mvc; +using ODataAPI.Models; +using ODataAPI.ODataAttribute; +using System.Linq; +using System.Threading.Tasks; + +namespace ODataAPI.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class TodoController : ControllerBase + { + private readonly TodoContext context; + public TodoController(TodoContext context) + { + this.context = context; + } + + #region snippet_eq + [HttpGet] + [MyEnableQuery()] + public ActionResult> GetTodoItems() + { + return this.context.TodoItems; + } + #endregion + + [HttpGet("{id}")] + public async Task> GetTodoItem(long id) + { + var todoItem = await this.context.TodoItems.FindAsync(id); + + if (todoItem == null) + { + return NotFound(); + } + + return todoItem; + } + + [HttpPost] + public async Task> PostTodoItem(TodoItem item) + { + this.context.TodoItems.Add(item); + await this.context.SaveChangesAsync(); + + return CreatedAtAction(nameof(GetTodoItem), new { id = item.Id }, item); + } + } +} \ No newline at end of file diff --git a/Odata-docs/webapi/odata-security/sample/ODataAPI/Controllers/ValuesController.cs b/Odata-docs/webapi/odata-security/sample/ODataAPI/Controllers/ValuesController.cs new file mode 100644 index 00000000..0d558ca1 --- /dev/null +++ b/Odata-docs/webapi/odata-security/sample/ODataAPI/Controllers/ValuesController.cs @@ -0,0 +1,80 @@ +using Microsoft.AspNet.OData; +using Microsoft.AspNet.OData.Query; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; + +namespace ODataAPI.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class ValuesController : ControllerBase + { + // GET api/values + #region snippet_PageSize + // Enable server-driven paging. Requires using Microsoft.AspNet.OData; + [EnableQuery(PageSize = 10)] + #endregion + [HttpGet] + public ActionResult> Get() + { + return new string[] { "value1", "value2" }; + } + + // GET /api/values/NewRoute + #region snippet_large + // Disable string functions. + [EnableQuery(AllowedFunctions = AllowedFunctions.AllFunctions & + ~AllowedFunctions.AllStringFunctions)] + #endregion + [HttpGet] + [Route("NewRoute")] + public ActionResult> GetAll() + { + return new string[] { "value3", "value4" }; + } + + // GET api/values/5 + #region snippet_AllowedQueryOptions + // Allow client paging but no other query options. + // Requires using Microsoft.AspNet.OData.Query; + [EnableQuery(AllowedQueryOptions = AllowedQueryOptions.Skip | + AllowedQueryOptions.Top)] + #endregion + // Set the allowed $orderby properties. + [HttpGet("{id}")] + public ActionResult Get(int id) + { + return "value"; + } + + // POST api/values + #region snippet_AllowedOrderByProperties + [EnableQuery(AllowedOrderByProperties = "Id,Name")] // Comma separated list + #endregion + [HttpPost] + public void Post([FromBody] string value) + { + } + + // PUT api/values/5 + // Set the maximum node count. + #region snippet_MaxNodeCount + [EnableQuery(MaxNodeCount = 20)] + #endregion + [HttpPut("{id}")] + public void Put(int id, [FromBody] string value) + { + } + + // DELETE api/values/5 + #region snippet_any + // Disable any() and all() functions. + [EnableQuery(AllowedFunctions = AllowedFunctions.AllFunctions & + ~AllowedFunctions.All & ~AllowedFunctions.Any)] + #endregion + [HttpDelete("{id}")] + public void Delete(int id) + { + } + } +} diff --git a/Odata-docs/webapi/odata-security/sample/ODataAPI/Models/Employee.cs b/Odata-docs/webapi/odata-security/sample/ODataAPI/Models/Employee.cs new file mode 100644 index 00000000..fd90d88e --- /dev/null +++ b/Odata-docs/webapi/odata-security/sample/ODataAPI/Models/Employee.cs @@ -0,0 +1,17 @@ +using System.Runtime.Serialization; + +namespace TodoApi.Models +{ + #region snippet + public class Employee + { + public int Id { get; set; } + public string Name { get; set; } + public string Title { get; set; } + + // Requires using System.Runtime.Serialization; + [IgnoreDataMember] + public decimal Salary { get; set; } + } + #endregion +} diff --git a/Odata-docs/webapi/odata-security/sample/ODataAPI/Models/TodoContext.cs b/Odata-docs/webapi/odata-security/sample/ODataAPI/Models/TodoContext.cs new file mode 100644 index 00000000..658756e8 --- /dev/null +++ b/Odata-docs/webapi/odata-security/sample/ODataAPI/Models/TodoContext.cs @@ -0,0 +1,18 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ODataAPI.Models +{ + public class TodoContext : DbContext + { + public TodoContext(DbContextOptions options) + : base(options) + { + } + + public DbSet TodoItems { get; set; } + } +} diff --git a/Odata-docs/webapi/odata-security/sample/ODataAPI/Models/TodoItem.cs b/Odata-docs/webapi/odata-security/sample/ODataAPI/Models/TodoItem.cs new file mode 100644 index 00000000..e78699dc --- /dev/null +++ b/Odata-docs/webapi/odata-security/sample/ODataAPI/Models/TodoItem.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ODataAPI.Models +{ + public class TodoItem + { + #region OldProps + public long Id { get; set; } + public string Name { get; set; } + public bool IsComplete { get; set; } + #endregion + + #region NewProps + public string Type { get; set; } + public int priority { get; set; } + public System.DateTime DueDate { get; set; } + #endregion + } +} diff --git a/Odata-docs/webapi/odata-security/sample/ODataAPI/ODataAPI.csproj b/Odata-docs/webapi/odata-security/sample/ODataAPI/ODataAPI.csproj new file mode 100644 index 00000000..82c19dfd --- /dev/null +++ b/Odata-docs/webapi/odata-security/sample/ODataAPI/ODataAPI.csproj @@ -0,0 +1,15 @@ + + + + netcoreapp2.2 + InProcess + + + + + + + + + + diff --git a/Odata-docs/webapi/odata-security/sample/ODataAPI/ODataAttribute/MyEnableQueryAttribute.cs b/Odata-docs/webapi/odata-security/sample/ODataAPI/ODataAttribute/MyEnableQueryAttribute.cs new file mode 100644 index 00000000..6323a9ab --- /dev/null +++ b/Odata-docs/webapi/odata-security/sample/ODataAPI/ODataAttribute/MyEnableQueryAttribute.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNet.OData; +using Microsoft.AspNet.OData.Query; +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ODataAPI.ODataAttribute +{ + #region snippet + public class MyEnableQueryAttribute : EnableQueryAttribute + { + private readonly DefaultQuerySettings defaultQuerySettings; + public MyEnableQueryAttribute() : base() + { + defaultQuerySettings = new DefaultQuerySettings(); + defaultQuerySettings.EnableOrderBy = true; + } + public override void ValidateQuery(HttpRequest request, ODataQueryOptions queryOptions) + { + if (queryOptions.OrderBy != null) + { + queryOptions.OrderBy.Validator = new MyOrderByValidator(defaultQuerySettings); + } + base.ValidateQuery(request, queryOptions); + } + } + #endregion +} diff --git a/Odata-docs/webapi/odata-security/sample/ODataAPI/ODataAttribute/MyFilterNavPropQueryValidator.cs b/Odata-docs/webapi/odata-security/sample/ODataAPI/ODataAttribute/MyFilterNavPropQueryValidator.cs new file mode 100644 index 00000000..02b362d1 --- /dev/null +++ b/Odata-docs/webapi/odata-security/sample/ODataAPI/ODataAttribute/MyFilterNavPropQueryValidator.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNet.OData.Query; +using Microsoft.AspNet.OData.Query.Validators; +using Microsoft.OData; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; + +namespace ODataAPI.ODataAttribute +{ + #region snippet + // Validator to prevent filtering on navigation properties. + public class MyFilterNavPropQueryValidator : FilterQueryValidator + { + + public override void ValidateNavigationPropertyNode( + QueryNode sourceNode, + IEdmNavigationProperty navigationProperty, + ODataValidationSettings settings) + { + throw new ODataException("Filtering on navigation properties prohibited"); + } + + public MyFilterNavPropQueryValidator(DefaultQuerySettings defaultQuerySettings) + : base(defaultQuerySettings) + { + + } + } + #endregion +} diff --git a/Odata-docs/webapi/odata-security/sample/ODataAPI/ODataAttribute/MyFilterQueryValidator.cs b/Odata-docs/webapi/odata-security/sample/ODataAPI/ODataAttribute/MyFilterQueryValidator.cs new file mode 100644 index 00000000..5fe66287 --- /dev/null +++ b/Odata-docs/webapi/odata-security/sample/ODataAPI/ODataAttribute/MyFilterQueryValidator.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNet.OData.Query; +using Microsoft.AspNet.OData.Query.Validators; +using Microsoft.OData; +using Microsoft.OData.UriParser; +using System; +using System.Linq; + +namespace ODataAPI.ODataAttribute +{ + #region snippet + // Validator to restrict which properties can be used in $filter expressions. + public class MyFilterQueryValidator : FilterQueryValidator + { + static readonly string[] allowedProperties = { "ReleaseYear", "Title" }; + + public override void ValidateSingleValuePropertyAccessNode( + SingleValuePropertyAccessNode propertyAccessNode, + ODataValidationSettings settings) + { + string propertyName = null; + if (propertyAccessNode != null) + { + propertyName = propertyAccessNode.Property.Name; + } + + if (propertyName != null && !allowedProperties.Contains(propertyName)) + { + throw new ODataException( + String.Format("Filter on {0} not allowed", propertyName)); + } + base.ValidateSingleValuePropertyAccessNode(propertyAccessNode, settings); + } + + public MyFilterQueryValidator(DefaultQuerySettings defaultQuerySettings) + : base(defaultQuerySettings) + { + + } + } + #endregion +} diff --git a/Odata-docs/webapi/odata-security/sample/ODataAPI/ODataAttribute/MyOrderByValidator.cs b/Odata-docs/webapi/odata-security/sample/ODataAPI/ODataAttribute/MyOrderByValidator.cs new file mode 100644 index 00000000..b85f5c08 --- /dev/null +++ b/Odata-docs/webapi/odata-security/sample/ODataAPI/ODataAttribute/MyOrderByValidator.cs @@ -0,0 +1,29 @@ +using Microsoft.AspNet.OData.Query; +using Microsoft.AspNet.OData.Query.Validators; +using Microsoft.OData; +using Microsoft.OData.UriParser; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ODataAPI.ODataAttribute +{ + public class MyOrderByValidator : OrderByQueryValidator + { + public MyOrderByValidator(DefaultQuerySettings defaultQuerySettings) : base(defaultQuerySettings) + { + } + // Disallow the 'desc' parameter for $orderby option. + public override void Validate(OrderByQueryOption orderByOption, + ODataValidationSettings validationSettings) + { + if (orderByOption.OrderByNodes.Any( + node => node.Direction == OrderByDirection.Descending)) + { + throw new ODataException("The 'desc' option is not supported."); + } + base.Validate(orderByOption, validationSettings); + } + } +} diff --git a/Odata-docs/webapi/odata-security/sample/ODataAPI/Program.cs b/Odata-docs/webapi/odata-security/sample/ODataAPI/Program.cs new file mode 100644 index 00000000..725154ff --- /dev/null +++ b/Odata-docs/webapi/odata-security/sample/ODataAPI/Program.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace ODataAPI +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} diff --git a/Odata-docs/webapi/odata-security/sample/ODataAPI/Startup.cs b/Odata-docs/webapi/odata-security/sample/ODataAPI/Startup.cs new file mode 100644 index 00000000..d47ff299 --- /dev/null +++ b/Odata-docs/webapi/odata-security/sample/ODataAPI/Startup.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.OData.Extensions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using ODataAPI.Models; + +namespace ODataAPI +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddDbContext(opt => + opt.UseInMemoryDatabase("TodoList")); + services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2); + services.AddOData(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + } + + app.UseHttpsRedirection(); + app.UseMvc(routeBuilder => + { + routeBuilder.EnableDependencyInjection(); + routeBuilder.Select().Count().OrderBy().Filter(); + }); + } + } +} diff --git a/Odata-docs/webapi/odata-security/sample/ODataAPI/StartupEDM.cs b/Odata-docs/webapi/odata-security/sample/ODataAPI/StartupEDM.cs new file mode 100644 index 00000000..cf6a16df --- /dev/null +++ b/Odata-docs/webapi/odata-security/sample/ODataAPI/StartupEDM.cs @@ -0,0 +1,61 @@ +using Microsoft.AspNet.OData.Builder; +using Microsoft.AspNet.OData.Extensions; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OData.Edm; +using TodoApi.Models; + +namespace TodoApi +{ + public class StartupEDM + { + public StartupEDM(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddOData(); + services.AddMvc(options => + { + options.EnableEndpointRouting = false; + }).SetCompatibilityVersion(CompatibilityVersion.Version_2_2); ; + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseHsts(); + } + + app.UseHttpsRedirection(); + app.UseMvc(b => + { + b.MapODataServiceRoute("odata", "odata", GetEdmModel()); + }); + } + + #region snippet + private static IEdmModel GetEdmModel() + { + ODataConventionModelBuilder builder = new ODataConventionModelBuilder(); + var employees = builder.EntitySet("Employees"); + employees.EntityType.Ignore(emp => emp.Salary); + return builder.GetEdmModel(); + } + #endregion + } +} diff --git a/Odata-docs/webapi/odata-security/sample/ODataAPI/appsettings.Development.json b/Odata-docs/webapi/odata-security/sample/ODataAPI/appsettings.Development.json new file mode 100644 index 00000000..e203e940 --- /dev/null +++ b/Odata-docs/webapi/odata-security/sample/ODataAPI/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/Odata-docs/webapi/odata-security/sample/ODataAPI/appsettings.json b/Odata-docs/webapi/odata-security/sample/ODataAPI/appsettings.json new file mode 100644 index 00000000..def9159a --- /dev/null +++ b/Odata-docs/webapi/odata-security/sample/ODataAPI/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "AllowedHosts": "*" +} From 01f57f67b6e4ffaff4b7ebeca8778b300d6d5433 Mon Sep 17 00:00:00 2001 From: Rick Anderson Date: Wed, 13 Nov 2019 17:37:51 -1000 Subject: [PATCH 5/6] link --- Odata-docs/TOC.yml | 4 ++++ Odata-docs/webapi/odata-security.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Odata-docs/TOC.yml b/Odata-docs/TOC.yml index bdf5297d..1215401e 100644 --- a/Odata-docs/TOC.yml +++ b/Odata-docs/TOC.yml @@ -167,6 +167,10 @@ items: - name: Getting started href: /odata/webapi/netcore + - name: OData Expand + href: /odata/webapi/odata-expand + - name: Security guidance + href: /odata/webapi/odata-security - name: ODataLib items: - name: Core lib diff --git a/Odata-docs/webapi/odata-security.md b/Odata-docs/webapi/odata-security.md index b145d0cb..52cc51bb 100644 --- a/Odata-docs/webapi/odata-security.md +++ b/Odata-docs/webapi/odata-security.md @@ -23,7 +23,7 @@ A malicious or naive client can construct a query that: * Takes significant system resources. Such a query can disrupt your service. * Leaks sensitive information from a clever join. -The `[EnableQuery]` attribute is an action filter that parses, validates, and applies the query. The filter converts the query options into a [LINQ](/dotnet/csharp/linq/) expression. When the controller returns an type, the `IQueryable` LINQ provider converts the LINQ expression into a query. Therefore, performance depends on the LINQ provider that is used, and on the particular characteristics of the dataset or database schema. +The `[EnableQuery]` attribute is an action filter that parses, validates, and applies the query. The filter converts the query options into a [LINQ](/dotnet/csharp/linq/) expression. When the controller returns an `System.Linq.IQueryable` type, the `IQueryable` LINQ provider converts the LINQ expression into a query. Therefore, performance depends on the LINQ provider that is used, and on the particular characteristics of the dataset or database schema. +A malicious or naive client may construct a query that consumes excessive resources. Such a query can disrupt access to your service. Review [Security Guidance for ASP.NET Core Web API OData](odata-security.md) before starting this tutorial. ## Enable OData