diff --git a/src/isn/Program.cs b/src/isn/Program.cs index b2e8ab8..4e3db1a 100644 --- a/src/isn/Program.cs +++ b/src/isn/Program.cs @@ -20,11 +20,11 @@ namespace isn currentSource = settings.DefaultSource; } } - static OptionSet storeoptions = new OptionSet { + static readonly OptionSet storeoptions = new OptionSet { { "s|source=", "use source", val => currentSource = currentSource ?? val }, { "h|help", "show this message and exit", h => shouldShowPushHelp = h != null }, }; - private static string _configFileName = + private static readonly string _configFileName = Path.Combine( Path.Combine(Environment.GetFolderPath( Environment.SpecialFolder.UserProfile), ".isn"), @@ -32,21 +32,21 @@ namespace isn ; public const string push = "push"; - static OptionSet options = new OptionSet { + static readonly OptionSet options = new OptionSet { { "h|help", "show this message and exit", h => shouldShowHelp = h != null }, }; - static OptionSet pushoptions = new OptionSet { + static readonly OptionSet pushoptions = new OptionSet { { "k|api-key=", "use api key", val => apiKey = apiKey ?? val }, { "p|store-api-key", "store used api key (=)", val => storApiKey = val != null }, { "s|source=", "use source", val => currentSource = currentSource ?? val }, { "h|help", "show this message and exit", h => shouldShowPushHelp = h != null }, }; - static OptionSet sourceoptions = new OptionSet { + static readonly OptionSet sourceoptions = new OptionSet { { "h|help", "show this message and exit", h => shouldShowSourceHelp = h != null }, }; - static OptionSet showOptions = new OptionSet { + static readonly OptionSet showOptions = new OptionSet { { "h|help", "show this message and exit", h => shouldShowSourceHelp = h != null }, }; @@ -154,7 +154,7 @@ namespace isn var pushCmd = new Command(push) { - Run = async sargs => + Run = sargs => { var pargs = pushoptions.Parse(sargs); if (shouldShowPushHelp) diff --git a/src/isnd/Controllers/Packages/Catalog.cs b/src/isnd/Controllers/Packages/Catalog.cs index 587abfc..f0e7b78 100644 --- a/src/isnd/Controllers/Packages/Catalog.cs +++ b/src/isnd/Controllers/Packages/Catalog.cs @@ -24,28 +24,32 @@ namespace isnd.Controllers return Ok(PackageManager.CurrentCatalogPages[int.Parse(id)]); } - [HttpGet(_pkgRootPrefix + "{apiVersion}/" + ApiConfig.Registration + "/{id}/index.json")] - public async Task CatalogRegistrationAsync(string apiVersion, string id) + [HttpGet(_pkgRootPrefix + "{apiVersion}/" + ApiConfig.Registration + "/{id}/{lower}.json")] + public IActionResult CatalogRegistration(string apiVersion, string id, string lower) { - var pkgs = packageManager.SearchById(id, null, null); - if (pkgs == null) return NotFound(); - return Ok(pkgs); + if (lower.Equals("index", System.StringComparison.OrdinalIgnoreCase)) + { + var query = new Data.Catalog.RegistrationPageIndexQuery + { + Query = id, + Prerelease = true + }; + var index = packageManager.GetPackageRegistrationIndex(query); + if (index == null) return NotFound(); + // query.TotalHits = result.Items.Select(i=>i.Items.Length).Aggregate((a,b)=>a+b); + return Ok(index); + } + var leaf = packageManager.SearchById(id,lower,null); + if (leaf.Count()==0) return NotFound(new { id, lower }); + return Ok(leaf.First()); } + [HttpGet(_pkgRootPrefix + ApiConfig.CatalogLeaf + "/{id}/{version}/{lower}/index.json")] - public async Task CatalogLeafAsync(string id, string pversion, string lower) + public IActionResult CatalogLeaf(string id, string pversion, string lower) { bool askForindex = lower == null; - if (false) - { - if (!NuGetVersion.TryParse(lower, out NuGetVersion version)) - return BadRequest(lower); - - var pkgFromname = packageManager.GetVersions(id, version, true); - if (pkgFromname == null) return NotFound(); - return Ok(pkgFromname); - } var pkgvs = this.packageManager.GetCatalogLeaf(id, pversion, lower).ToArray(); if (pkgvs.Count() == 0) return NotFound(); List types = pkgvs.Select( diff --git a/src/isnd/Controllers/Packages/RegistrationPageIndexQueryAndResult.cs b/src/isnd/Controllers/Packages/RegistrationPageIndexQueryAndResult.cs new file mode 100644 index 0000000..25a2965 --- /dev/null +++ b/src/isnd/Controllers/Packages/RegistrationPageIndexQueryAndResult.cs @@ -0,0 +1,10 @@ +using isnd.Data.Catalog; + +namespace isnd +{ + public class RegistrationPageIndexQueryAndResult + { + public RegistrationPageIndexQuery Query { get; set; } + public RegistrationPageIndex Result { get; set; } + } +} \ No newline at end of file diff --git a/src/isnd/Controllers/Packages/Search.cs b/src/isnd/Controllers/Packages/Search.cs index 7ba9ee4..2bf3ada 100644 --- a/src/isnd/Controllers/Packages/Search.cs +++ b/src/isnd/Controllers/Packages/Search.cs @@ -11,7 +11,7 @@ namespace isnd.Controllers { // GET {@id}?q={QUERY}&skip={SKIP}&take={TAKE}&prerelease={PRERELEASE}&semVerLevel={SEMVERLEVEL}&packageType={PACKAGETYPE} [HttpGet(_pkgRootPrefix + ApiConfig.Search)] - public async Task Search( + public IActionResult Search( string q, int skip = 0, int take = 25, diff --git a/src/isnd/Controllers/Packages/WebViews.cs b/src/isnd/Controllers/Packages/WebViews.cs index 7c70e57..da886f2 100644 --- a/src/isnd/Controllers/Packages/WebViews.cs +++ b/src/isnd/Controllers/Packages/WebViews.cs @@ -17,7 +17,8 @@ namespace isnd.Controllers // Web search public async Task Index(RegistrationPageIndexQuery model) { - return View(packageManager.GetPackageRegistrationIndex(model)); + return View(new RegistrationPageIndexQueryAndResult{Query = model, + Result = packageManager.GetPackageRegistrationIndex(model)}); } public async Task Details(string pkgid) diff --git a/src/isnd/Data/Catalog/PackageRegistrationIndexViewModel.cs b/src/isnd/Data/Catalog/PackageRegistrationIndexViewModel.cs deleted file mode 100644 index 772b820..0000000 --- a/src/isnd/Data/Catalog/PackageRegistrationIndexViewModel.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Collections.Generic; -using Newtonsoft.Json; - -namespace isnd.Data.Catalog -{ - public class RegistrationPageIndexQuery : RegistrationPageIndex - { - - public RegistrationPageIndexQuery() : base("") - { - } - public RegistrationPageIndexQuery(string bid, IEnumerable leaves) : base(bid, leaves) - { - } - - [JsonProperty("query")] - public string Query { get; set; } - - [JsonProperty("prerelease")] - public bool Prerelease { get; set; } - - public int Skip { get; set; } - - public int Take { get; set; } = 25; - public int TotalHits { get; set; } - } -} \ No newline at end of file diff --git a/src/isnd/Data/Catalog/RegistrationPage.cs b/src/isnd/Data/Catalog/RegistrationPage.cs index 6db4728..0da3ec7 100644 --- a/src/isnd/Data/Catalog/RegistrationPage.cs +++ b/src/isnd/Data/Catalog/RegistrationPage.cs @@ -1,18 +1,62 @@ using System; using System.Collections.Generic; +using System.Linq; +using isnd.Data.Packages; using Newtonsoft.Json; +using NuGet.Versioning; namespace isnd.Data.Catalog { - public class RegistrationPage + public class RegistrationPage { [JsonProperty("@id")] [JsonRequired] - public string Id { get; } - public RegistrationPage (string id) + public string Id { get; protected set;} + + private List items; + + protected string Bid { get ; private set; } + public string DlBase { get; } + + public RegistrationPage (string bid, string dlBase) + { + Bid = bid; + DlBase = dlBase; + this.items = new List(); + } + + public RegistrationPage (string bid, string pkgid, string dlBase, IEnumerable items) + { + Bid = bid; + Parent = Bid + "/index.json"; + DlBase = dlBase; + this.items = new List(items); + SetPackageId(pkgid); + UpdateCompact(); + } + + protected void SetPackageId(string pkgid) + { + this.Id = Bid + "/" + pkgid + "/index.json"; + } + + private void UpdateCompact() { - Id = id; - Items = new List(); + NuGetVersion upper = new NuGetVersion(0,0,0); + + // Assert.True(items.All(p=>p.Id == id)); + + foreach (var p in items) + { + if (upper < p.NugetVersion) upper = p.NugetVersion; + } + Upper = upper.ToFullString(); + NuGetVersion lower = upper; + foreach (var p in items) + { + if (lower > p.NugetVersion) lower = p.NugetVersion; + } + Lower = lower.ToFullString(); } /// @@ -21,19 +65,26 @@ namespace isnd.Data.Catalog /// [JsonProperty("items")] - public List Items { get; set; } + public RegistrationLeaf[] Items { get => items.Select((p) => p.ToLeave(Bid, DlBase)).ToArray(); } + + public void AddVersionRange(IEnumerable vitems) + { + if (vitems.Count() == 0) return; + items.AddRange(vitems); + UpdateCompact(); + } /// /// The highest SemVer 2.0.0 version in the page (inclusive) /// /// [JsonProperty("upper"), JsonRequired] - public Version Upper { get; set; } + public string Upper { get; private set; } /// /// The lowest SemVer 2.0.0 version in the page (inclusive) /// /// [JsonProperty("lower"), JsonRequired] - public Version Lower { get; set; } + public string Lower { get; private set; } /// /// The URL to the registration index diff --git a/src/isnd/Data/Catalog/RegistrationPageIndex.cs b/src/isnd/Data/Catalog/RegistrationPageIndex.cs index bb188f5..6a7d7eb 100644 --- a/src/isnd/Data/Catalog/RegistrationPageIndex.cs +++ b/src/isnd/Data/Catalog/RegistrationPageIndex.cs @@ -1,5 +1,7 @@ +using isnd.Data.Packages; using Newtonsoft.Json; using System.Collections.Generic; +using System.Linq; namespace isnd.Data.Catalog { @@ -10,30 +12,24 @@ namespace isnd.Data.Catalog /// /// [JsonProperty("@id")] - public string Id { get; set; } - public RegistrationPageIndex(string id) + public string Id { get; protected set; } + + public RegistrationPageIndex(string bid, string id) { - Id = id; + Id = bid + "/" + id + "/index.json"; Items = new List(); } - - public RegistrationPageIndex(IEnumerable pages) - { - Items = new List(pages); - } - public RegistrationPageIndex(string id, IEnumerable leaves) : this(id) + public RegistrationPageIndex(string bid, string id, string dlBase, IEnumerable pkgs) : this(bid, id) { // leaves; this.Items = new List - ( - - - ) - { - - - }; + (pkgs.GroupBy(l => l.Id) + .Select(lg => new RegistrationPage + (bid, lg.Key, dlBase, lg.ToArray() + .Select(p => p.Versions).Aggregate + ((l, m) => { l.AddRange(m); return l.ToList(); }) + ))); } [JsonProperty("count")] diff --git a/src/isnd/Data/Catalog/RegistrationPageIndexQuery.cs b/src/isnd/Data/Catalog/RegistrationPageIndexQuery.cs new file mode 100644 index 0000000..41f5e89 --- /dev/null +++ b/src/isnd/Data/Catalog/RegistrationPageIndexQuery.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using isnd.Data.Packages; +using Newtonsoft.Json; + +namespace isnd.Data.Catalog +{ + public class RegistrationPageIndexQuery + { + + public RegistrationPageIndexQuery() + { + } + + [JsonProperty("query")] + public string Query { get; set; } + + [JsonProperty("prerelease")] + public bool Prerelease { get; set; } = true; + [JsonProperty("skip")] + + public int Skip { get; set; } = 0; + [JsonProperty("take")] + + public int Take { get; set; } = 25; + [JsonProperty("totalHits")] + public int TotalHits { get; set; } + } +} \ No newline at end of file diff --git a/src/isnd/Data/Packages/PackageVersion.cs b/src/isnd/Data/Packages/PackageVersion.cs index 9415ebc..757b295 100644 --- a/src/isnd/Data/Packages/PackageVersion.cs +++ b/src/isnd/Data/Packages/PackageVersion.cs @@ -2,6 +2,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using isn.abst; +using isnd.Data.Catalog; using isnd.Data.Packages; using isnd.Data.Packages.Catalog; using Newtonsoft.Json; @@ -28,6 +29,11 @@ namespace isnd.Data public int Revision { get; set; } + + /// + /// Full version string + /// + /// [StringLength(256)] [Required][Key] public string FullString { get; set; } @@ -48,13 +54,30 @@ namespace isnd.Data public string CommitId { get => CommitNId.ToString(); } public virtual Commit LatestCommit {get; set; } - public string NugetLink => $"/{Constants.PaquetFileEstension}/{PackageId}/{FullString}/{PackageId}-{FullString}." + public string NugetLink => $"{Constants.PaquetFileEstension}/{PackageId}/{FullString}/{PackageId}-{FullString}." + Constants.PaquetFileEstension; - public string NuspecLink => $"/{Constants.SpecFileEstension}/{PackageId}/{FullString}/{PackageId}-{FullString}." + public string NuspecLink => $"{Constants.SpecFileEstension}/{PackageId}/{FullString}/{PackageId}-{FullString}." + Constants.SpecFileEstension; public string SementicVersionString { get => $"{Major}.{Minor}.{Patch}"; } public NuGetVersion NugetVersion { get => new NuGetVersion(FullString); } + + public RegistrationLeaf ToLeave(string bid, string dlbase) + { + string leaveid = bid + "/" + this.PackageId + "/" + FullString + ".json"; + return new RegistrationLeaf + { + Id = leaveid, + PackageContent = dlbase + NugetLink, + Entry = new CatalogEntry + { + Id = leaveid, + idp = PackageId, + version = FullString, + authors = $"{this.Package.Owner.FullName} <${Package.Owner.Email}>" + } + }; + } } } \ No newline at end of file diff --git a/src/isnd/Helpers/PackageIdHelpers.cs b/src/isnd/Helpers/PackageIdHelpers.cs index e5cd398..a16b02c 100644 --- a/src/isnd/Helpers/PackageIdHelpers.cs +++ b/src/isnd/Helpers/PackageIdHelpers.cs @@ -2,12 +2,11 @@ namespace isnd.Helpers { public static class PackageIdHelpers { - internal static bool SeparatedByMinusMatch(string id, string q) { foreach (var part in id.Split('-')) { - if (part.StartsWith(q, System.StringComparison.OrdinalIgnoreCase)) return true; + if (part.Equals(q, System.StringComparison.OrdinalIgnoreCase)) return true; } return false; } @@ -24,7 +23,7 @@ namespace isnd.Helpers while (id.Length > i && char.IsLower(id[i])) i++; if (i == 0) break; id = id.Substring(i); - if (id.StartsWith(query, System.StringComparison.OrdinalIgnoreCase)) return true; + if (id.Equals(query, System.StringComparison.OrdinalIgnoreCase)) return true; } return false; } diff --git a/src/isnd/Helpers/PackageVersionHelpers.cs b/src/isnd/Helpers/PackageVersionHelpers.cs index 6c21437..49f41d6 100644 --- a/src/isnd/Helpers/PackageVersionHelpers.cs +++ b/src/isnd/Helpers/PackageVersionHelpers.cs @@ -11,35 +11,10 @@ namespace isnd.Helpers { public static class PackageVersionHelpers { - public static bool IsOwner(this ClaimsPrincipal user, PackageVersion v) { var userId = user.FindFirstValue(ClaimTypes.NameIdentifier); return v.Package.OwnerId == userId; } - - public static RegistrationPageIndex CreateRegistrationPages(this IEnumerable leaves, - string bid) - { - List pages = new List(); - var ids = leaves.Select(l => l.Entry.Id).Distinct().ToArray(); - foreach (var id in ids) - { - var lbi = leaves.Where(l=>l.Entry.Id == id).OrderBy(l=> - new Version(l.Entry.version)); - var latest = new Version(lbi.Last().Entry.version); - pages.Add(new RegistrationPage(bid + id + "/" + latest.Major + "." - + latest.Minor + "." - + latest.Build) - { - Count = lbi.Count(), - Lower = new Version(lbi.First().Entry.version), - Upper = latest, - Items = lbi.ToList(), - Parent = bid + id + "/" + ApiConfig.IndexDotJson, - }); - } - return new RegistrationPageIndex(pages); - } } } \ No newline at end of file diff --git a/src/isnd/Interfaces/IPackageManager.cs b/src/isnd/Interfaces/IPackageManager.cs index f091819..8087ed4 100644 --- a/src/isnd/Interfaces/IPackageManager.cs +++ b/src/isnd/Interfaces/IPackageManager.cs @@ -28,7 +28,7 @@ namespace isnd.Interfaces IEnumerable SearchById(string pkgId, string semver, string pkgType); RegistrationPageIndex GetCatalogIndex(); - RegistrationPageIndexQuery GetPackageRegistrationIndex(RegistrationPageIndexQuery query); + RegistrationPageIndex GetPackageRegistrationIndex(RegistrationPageIndexQuery query); } } \ No newline at end of file diff --git a/src/isnd/Services/PackageManager.cs b/src/isnd/Services/PackageManager.cs index 07b2b8e..78c9eff 100644 --- a/src/isnd/Services/PackageManager.cs +++ b/src/isnd/Services/PackageManager.cs @@ -118,7 +118,7 @@ namespace isnd.Services Type = "RegistrationsBaseUrl/3.6.0", Comment = "Base URL of storage where isn package registration info is stored in GZIP format. This base URL includes SemVer 2.0.0 packages." }); - + return res; } @@ -184,9 +184,9 @@ namespace isnd.Services var oldIndex = CurrentCatalogIndex; var oldPages = CurrentCatalogPages; string baseid = extUrl + ApiConfig.Catalog; - string bidreg = $"{extUrl}v3.4.0/{ApiConfig.Registration}/"; + string bidreg = $"{extUrl}v3.4.0/{ApiConfig.Registration}"; string basepageid = extUrl + ApiConfig.CatalogPage; - CurrentCatalogIndex = new RegistrationPageIndex(baseid); + CurrentCatalogIndex = new RegistrationPageIndex(baseid,"index"); CurrentCatalogPages = new List(); var scope = dbContext.Commits.OrderBy(c => c.TimeStamp); @@ -197,14 +197,14 @@ namespace isnd.Services { if (i >= this.isndSettings.CatalogPageLen) { - page = new RegistrationPage(basepageid + "-" + p++) + page = new RegistrationPage(basepageid, extUrl) { Parent = baseid, CommitId = commit.CommitId, CommitTimeStamp = commit.CommitTimeStamp }; CurrentCatalogPages.Add(page); - var pageRef = new RegistrationPage(page.Id) + var pageRef = new RegistrationPage(page.Id, extUrl) { CommitId = commit.CommitId, CommitTimeStamp = commit.CommitTimeStamp @@ -218,27 +218,27 @@ namespace isnd.Services .Include(pkg => pkg.LatestVersion) .Where( pkg => pkg.Versions.Count(v => v.CommitId == commit.CommitId) > 0 - ).GroupBy((q)=> q.Id); + ).GroupBy((q) => q.Id); // pkg.Versions.OrderByDescending(vi => vi.CommitNId).First().FullString foreach (var pkgid in validPkgs) { StringBuilder refid = new StringBuilder(bidreg); refid.AppendFormat("{0}/", pkgid.Key); - /* var pkgref = new PackageRef - { - Version = v.FullString, - LastCommit = v.LatestCommit, - CommitId = v.LatestCommit.CommitId, - CommitTimeStamp = v.LatestCommit.CommitTimeStamp, - RefId = refid.ToString(), - Id = v.PackageId, - RefType = v.LatestCommit.Action == PackageAction.PublishPackage - ? "nuget:PackageDetails" : - "nuget:PackageDelete" - }; */ - foreach (var pkgv in pkgid) - page.Items.Add(pkgv.ToLeave(bidreg)); + /* var pkgref = new PackageRef + { + Version = v.FullString, + LastCommit = v.LatestCommit, + CommitId = v.LatestCommit.CommitId, + CommitTimeStamp = v.LatestCommit.CommitTimeStamp, + RefId = refid.ToString(), + Id = v.PackageId, + RefType = v.LatestCommit.Action == PackageAction.PublishPackage + ? "nuget:PackageDetails" : + "nuget:PackageDelete" + }; */ + foreach (var pkgv in pkgid) + page.AddVersionRange(pkgv.Versions); } reason = commit; i++; @@ -293,8 +293,8 @@ namespace isnd.Services return dbContext.PackageVersions .Include(v => v.Package) .Include(v => v.LatestCommit) - .Where(v => v.PackageId == pkgId - && (semver == null || + .Where(v => v.PackageId == pkgId + && (semver == null || semver.StartsWith(v.SementicVersionString)) && (pkgType == null || pkgType == v.Type)); } @@ -312,13 +312,16 @@ namespace isnd.Services public IEnumerable SearchById(string pkgId, string semver, string pkgType) { - string bid = $"{extUrl}v3.4.0/{ApiConfig.Registration}/"; + string bid = $"{extUrl}v3.4.0/{ApiConfig.Registration}"; return dbContext.PackageVersions .Include(v => v.Package) .Include(v => v.Package.Owner) .Include(v => v.LatestCommit) - .Where(v => v.PackageId == pkgId && semver.StartsWith(v.SementicVersionString) - && (pkgType == null || pkgType == v.Type)).Select(p => p.Package.ToLeave(bid)); + .Where(v => v.PackageId == pkgId && semver.Equals(v.FullString, StringComparison.OrdinalIgnoreCase) + && (pkgType == null || pkgType == v.Type)) + .OrderByDescending(p=> p.CommitId) + .Select(p => p.Package.ToLeave(bid)) + ; } public PackageVersion GetPackage(string pkgId, string semver, string pkgType) { @@ -329,24 +332,18 @@ namespace isnd.Services && (pkgType == null || pkgType == v.Type)); } - public RegistrationPageIndexQuery GetPackageRegistrationIndex(RegistrationPageIndexQuery query) + public RegistrationPageIndex GetPackageRegistrationIndex(RegistrationPageIndexQuery query) { // RegistrationPageIndexAndQuery - var scope = dbContext.Packages - .Include(p => p.Versions) - .Include(p => p.Owner) - .Where( - p => (PackageIdHelpers.CamelCaseMatch(p.Id, query.Query) - || PackageIdHelpers.SeparatedByMinusMatch(p.Id, query.Query)) - && (query.Prerelease || p.Versions.Any(v => !v.IsPrerelease)) - ); + var scope = dbContext.Packages.Include(p => p.Versions).Include(p => p.Owner) + .Where(p => (PackageIdHelpers.CamelCaseMatch(p.Id, query.Query) + || PackageIdHelpers.SeparatedByMinusMatch(p.Id, query.Query)) + && (query.Prerelease || p.Versions.Any(v => !v.IsPrerelease))); var total = scope.Count(); var pkgs = scope.Skip(query.Skip).Take(query.Take).ToArray(); - string bid = $"{extUrl}v3.4.0/{ApiConfig.Registration}/"; - var leaves = pkgs.Select(p => p.ToLeave(bid)); - - return - new RegistrationPageIndexQuery(bid, leaves); + string bid = $"{extUrl}v3.4.0/{ApiConfig.Registration}"; + return + new RegistrationPageIndex(bid, query.Query, extUrl, pkgs); } } } \ No newline at end of file diff --git a/src/isnd/Views/Packages/Index.cshtml b/src/isnd/Views/Packages/Index.cshtml index 2b85c99..fcebfdc 100644 --- a/src/isnd/Views/Packages/Index.cshtml +++ b/src/isnd/Views/Packages/Index.cshtml @@ -1,4 +1,4 @@ -@model RegistrationPageIndexQuery +@model isnd.RegistrationPageIndexQueryAndResult @{ ViewData["Title"] = "Index"; @@ -11,9 +11,9 @@
- - - + + +
@@ -25,16 +25,16 @@ - @Html.DisplayNameFor(model => model.Items[0].Id) + @Html.DisplayNameFor(model => model.Result.Items[0].Id) - @Html.DisplayNameFor(model => model.Items[0].CommitId) + @Html.DisplayNameFor(model => model.Result.Items[0].CommitId) -@foreach (var page in Model.Items) { +@foreach (var page in Model.Result.Items) { @foreach (var item in page.Items) {