OData / AspNetCoreOData

ASP.NET Core OData: A server library built upon ODataLib and ASP.NET Core
Other
457 stars 158 forks source link

NextPageLink is not working in expand #712

Open KwizaKaneria opened 2 years ago

KwizaKaneria commented 2 years ago

I set MaxTop=100 and PageSize=100 on my controller and expanding entity.

When the data is larger than 100 then it gives the next page link for main entity and for expanding entity. NextPageLink in main entity is working but it is not working for expanding entity.

Can you please guide us on this behavior?

KenitoInc commented 2 years ago

@KwizaKaneria Is the expanded entity a contained navigation property? Kindly share a repro

gathogojr commented 2 years ago

Gentle ping @KwizaKaneria. Could you please share a repro or more detailed repro steps to help us understand the issue better?

xuzhg commented 2 years ago

Lisi's current PR/commit could be related. https://github.com/OData/AspNetCoreOData/pull/710. Attached the link for reference.

xuzhg commented 2 years ago

@KwizaKaneria Would you please share us a repo that can help us understand your problem and fix it as soon as possible.?

mikepizzo commented 2 years ago

In particular, it's unclear from the description whether the nested nextlink is being generated but is invalid, or is not being generated, or exactly what the behavior may be.

Assuming the nested nextlink is being generated, if you could minimally share a request and a subset of the sample response showing the nested nextlink, and the response returned when calling the nested nextlink, that would be a great start.

It would also be helpful if you could share the $metadata (or relevant portion there-of).

KwizaKaneria commented 2 years ago

Following are the code snippet: `

  1. Code for State entity:

    [DataContract] public class State { [Key] [DataMember(Name = "id")] public int Id { get; set; }

    [DataMember(Name = "code")]
    public string Code { get; set; }
    
    [DataMember(Name = "name")]
    public string Name { get; set; }
    
    [Page(MaxTop = 100, PageSize = 100)]
    [DataMember(Name = "counties")]
    public virtual ICollection<CountyDetails> CountyDetails { get; set; } = new HashSet<CountyDetails>();

    }

    ` 2. Code for County Entity:

       [DataContract]
       [Page(MaxTop = 100, PageSize = 100)]
       public class CountyDetails
      {
      [Key]
      [DataMember(Name ="id")]
      public int Id { get; set; }
    
      [DataMember(Name = "name")]
      public string CountyName { get; set; }
    
      public State State { get; set; }
    
      public int StateId { get; set; }

    }`

  2. Configuration for State Entity:

    public void Configure(EntityTypeBuilder builder) { builder.ToView("vw_State");

        builder.Property(e => e.Id).HasColumnName("StateId");
        builder.Property(e => e.Code).HasColumnName("State_Code");
        builder.Property(e => e.Name).HasColumnName("State_Name");
    }
    1. Configuration for County Entity:

      public void Configure(EntityTypeBuilder builder) { builder.ToView("vw_StateCounties");

      builder.Property(e => e.CountyName).HasColumnName("County_Name");
      builder.Property(e => e.Id).HasColumnName("County_CtySt_ID");

      }`

@mikepizzo The next page link is generated but it is not working. https://localhost:5001/odata/v1/states/17/counties?$skip=100 This is my next page link. It gives 404 in response.

gathogojr commented 2 years ago

@KwizaKaneria Please confirm whether counties is a contained navigation property

KenitoInc commented 2 years ago

@KwizaKaneria Do you have a controller method that handles /states/{id}/counties?

gathogojr commented 2 years ago

@KwizaKaneria Please confirm whether you have implementing the relevant controller action in support of navigation property routing. In your case you'd need to implement a controller action named GetCounties as follows:


[EnableQuery(PageSize = 10, MaxTop = 10)]
[HttpGet("{key}/counties")] // If you have a `Route` attribute at controller level, i.e., Route("YOURROUTEPREFIX/states"), or Route("states") if you have not configured a route prefix
// Or [HttpGet("YOURROUTEPREFIX/states/{key}/counties")]
// Or [HttpGet("states/{key}/counties")], i.e., If you haven't configured a route prefix
public ActionResult GetCounties([FromRoute] int key)
{
    // ...
}
KwizaKaneria commented 2 years ago

@KenitoInc @gathogojr We did not have a separate container for this.

gathogojr commented 2 years ago

@KwizaKaneria I don't understand your statement about separate container. Can you confirm whether you implemented the required controller method?

mikepizzo commented 2 years ago

By "container" I assume you mean that you don't have a separate top-level collection ("entityset") that contains all of the countyDetails. That's what @gathogojr meant by "contained" -- the countyDetails are "contained" within each state.

In order to support directly retrieving countyDetails from a state, you'll have to implement the controller method on the state controller for retrieving the countyDetails for a particular state, as per @KenitoInc's comment and @gathogojr's example.

This controller method will support requesting counties for a specific state, for example:

  /states/{stateId}/counties

Which is the resource path used for nested page links.

HTH!

KwizaKaneria commented 2 years ago

@mikepizzo So are we need to register this route in the API gateway?

gathogojr commented 2 years ago

Hi @KwizaKaneria. Do you need further help with this? Did you try the suggested solutions?

KenitoInc commented 2 years ago

@KwizaKaneria Did you manage to resolve your issue?

KwizaKaneria commented 2 years ago

@KenitoInc No it is not resolved yet.

SViradiya-MarutiTech commented 1 year ago

@gathogojr @mikepizzo How nextlink is generated differently? If my request isv1/states?$filter=id eq 55&$expand=counties then nextpagelink should be same as v1/states?$filter=id eq 55&$expand=counties&$skip=100. Why we need to create another controller method? another REST API?

gathogojr commented 1 year ago

@SViradiya-MarutiTech In your scenario, you wouldn't need to create another controller method.

mansurdegirmenci commented 1 year ago

i think i have the same problem.

EdmModel:

public static class EdmModelBuilder
{
    internal static IEdmModel GetEdmModelv1()
    {
        var builder = new ODataConventionModelBuilder();

        #region UserDto

        var userDto = builder.EntitySet<UserDto>("Users").EntityType;
        userDto.Name = "Users";

        userDto.Function("Get")
            .ReturnsFromEntitySet<UserDto>("Users")
            .Parameter<int>("key").Required();

        #endregion

        #region PhotoDto

        var photoDto = builder.EntitySet<PhotoDto>("Photos").EntityType;
        photoDto.Name = "Photos";

        photoDto.Function("Get")
            .ReturnsFromEntitySet<PhotoDto>("Photos")
            .Parameter<int>("key").Required();

        #endregion

        return builder.GetEdmModel();
    }
}

UserDTO:

public record UserDto
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public string UserName { get; set; }

    [EmailAddress] public string Email { get; set; }

    [Phone] public string PhoneNumber { get; set; }

    [DataType(DataType.DateTime)] public DateTime BirthDateTime { get; set; }

    [DataType(DataType.DateTime)] public DateTime CreatedDateTime { get; set; }

    public char Gender { get; set; }

    public virtual ICollection<Photo> Photos { get; set; }
}

PhotoDTO:

public record PhotoDto
{
    public int Id { get; set; }
    public Guid Guid { get; set; }
    public string Name { get; set; }
    public DateTime SaveDateTime { get; set; }
    public string Size { get; set; }
    public string SizeName { get; set; }
    public int NumberOfShows { get; set; }
    public UserDto User { get; set; }
}

UsersController:

public class UsersController : ODataController
{
    #region ctor

    private readonly ILogger<UsersController> _logger;

    private readonly IMapper _mapper;

    private readonly UserManager<AspNetUser> _userManager;

    public UsersController(ILogger<UsersController> logger,
        IMapper mapper,
        UserManager<AspNetUser> userManager)
    {
        _mapper = mapper;
        _userManager = userManager;
        _logger = logger;
    }

    #endregion

    [HttpGet, EnableQuery(PageSize = 10)]
    public async Task<IActionResult> Get(ODataQueryOptions<UserDto> options)
    {
        var users = await _userManager.Users
            .AsNoTracking()
            .GetQueryAsync(_mapper, options);

        return Ok(users);
    }

    [HttpGet, EnableQuery]
    public async Task<IActionResult> Get(int key, ODataQueryOptions<UserDto> options)
    {
        var user = await _userManager.Users
            .AsNoTracking()
            .GetQuery(_mapper, options)
            .SingleOrDefaultAsync(x => x.Id.Equals(key));

        return Ok(user);
    }
}

PhotosController:

public class PhotosController : ODataController
{
    #region ctor

    private readonly ILogger<PhotosController> _logger;

    private readonly IWebHostEnvironment _webHostEnvironment;

    private readonly IMapper _mapper;

    private readonly DataListContext _dataListContext;

    public PhotosController(ILogger<PhotosController> logger,
        IWebHostEnvironment webHostEnvironment,
        IMapper mapper,
        DataListContext dataListContext)
    {
        this._logger = logger;
        this._webHostEnvironment = webHostEnvironment;
        this._mapper = mapper;
        this._dataListContext = dataListContext;
    }

    #endregion

    [HttpGet, EnableQuery(PageSize = 10)]
    public async Task<IActionResult> Get(ODataQueryOptions<PhotoDto> options)
    {
        var photos = await _dataListContext.Photos
            .AsNoTracking()
            .GetQueryAsync(_mapper, options);

        return Ok(photos);
    }

    [HttpGet, EnableQuery]
    public async Task<IActionResult> Get(int key, ODataQueryOptions<PhotoDto> options)
    {
        var photo = await _dataListContext.Photos
            .AsNoTracking()
            .GetQuery(_mapper, options)
            .SingleOrDefaultAsync(x => x.Id.Equals(key));

        return Ok(photo);
    }
}

User Model:

public partial class AspNetUser : IdentityUser<int>
{
    [PersonalData] public string FirstName { get; set; }
    [PersonalData] public string LastName { get; set; }
    [PersonalData] public DateTime? BirthDateTime { get; set; }
    public string Location { get; set; }
    public DateTime CreatedDateTime { get; set; }
    public bool IsActive { get; set; }
    public Gender Gender { get; set; }
    public virtual ICollection<Photo> Photos { get; set; }
}

Photo Model:

public class Photo
{
    [Column(Order = 1)]
    public int Id { get; set; }

    [Column(Order = 2)]
    public Guid? Guid { get; set; }

    [Column(Order = 3)]
    public int UserId { get; set; }

    [Column(Order = 4)]
    public string OrijinalName { get; set; }

    [Column(Order = 5)]
    public string ChangedName { get; set; }

    [Column(Order = 6)]
    public DateTime SaveDateTime { get; set; }

    [Column(Order = 7)]
    public string Size { get; set; }

    [Column(Order = 8)]
    public SizeName SizeName { get; set; }

    [Column(Order = 9)]
    public int NumberOfShows { get; set; }

    [Column(Order = 10)]
    public virtual AspNetUser AspNetUser { get; set; }

    // fotoğraf çekilen bölgenin bilgileri varsa (location)
    // kategori ile bölge harmanlanacak
}

When i send request to this url the result is successful. https://localhost:5001/api/users?$expand=Photos($orderby=Id%20desc)

but the suggested next link doesn't work. image

julealgon commented 1 year ago

What did you expect as the next link value @mansurdegirmenci ?

mansurdegirmenci commented 1 year ago

https://localhost:5001/api/Users?$expand=photos($skiptoken=Id-25) Shouldn't the url be like this? The next link already created by Odata is not working. @julealgon

image

image

julealgon commented 1 year ago

Could you perhaps share the entire response payload for that example @mansurdegirmenci ?

mansurdegirmenci commented 1 year ago

Request:
https://localhost:5001/api/users?$expand=photos

{
    "@odata.context": "https://localhost:5001/api/$metadata#Users(Photos())",
    "value": [
        {
            "Id": 1,
            "FirstName": "",
            "LastName": "",
            "UserName": "",
            "Email": "",
            "PhoneNumber": "",
            "BirthDateTime": "",
            "CreatedDateTime": "",
            "Gender": "M",
            "Photos": [
                {
                    "Id": 16,
                    "Guid": "537405d1-99a0-453c-a0e7-b092b3611eb9",
                    "Name": "Art-PNG-HD-Image.png",
                    "SaveDateTime": "2023-04-25T00:16:57.1733333+03:00",
                    "Size": "724182",
                    "SizeName": "BYTE",
                    "NumberOfShows": 0
                },
                {
                    "Id": 17,
                    "Guid": "11536263-b738-4e9a-9f75-2169ec51f38d",
                    "Name": "gray02.png",
                    "SaveDateTime": "2023-04-27T21:55:36.41+03:00",
                    "Size": "1055",
                    "SizeName": "BYTE",
                    "NumberOfShows": 0
                },
                {
                    "Id": 18,
                    "Guid": "6b843cdf-81da-4405-a957-566201778677",
                    "Name": "20230101_191019.jpg",
                    "SaveDateTime": "2023-04-29T01:14:15.6833333+03:00",
                    "Size": "3363327",
                    "SizeName": "BYTE",
                    "NumberOfShows": 1
                },
                {
                    "Id": 19,
                    "Guid": "d8f5c238-b3d4-4619-9b0a-e5a6a2fadd9e",
                    "Name": "EISsy4VWkAUh6ur.jpeg",
                    "SaveDateTime": "2023-04-29T01:15:47.2333333+03:00",
                    "Size": "19796",
                    "SizeName": "BYTE",
                    "NumberOfShows": 1
                },
                {
                    "Id": 20,
                    "Guid": "8d7b3044-b98e-4343-829c-bc742cf197ab",
                    "Name": "Backend-.NET-Developer-Roadmap-2022.png",
                    "SaveDateTime": "2023-04-29T13:15:01.95+03:00",
                    "Size": "2002976",
                    "SizeName": "BYTE",
                    "NumberOfShows": 2
                },
                {
                    "Id": 21,
                    "Guid": "f111d3d2-df6f-42e2-a7f1-b74b5cb4e59b",
                    "Name": "gray02.png",
                    "SaveDateTime": "2023-05-06T00:39:02.65+03:00",
                    "Size": "1055",
                    "SizeName": "BYTE",
                    "NumberOfShows": 0
                },
                {
                    "Id": 22,
                    "Guid": "1b387c30-d331-49ae-a5ae-d3de61a190c7",
                    "Name": "delete.png",
                    "SaveDateTime": "2023-05-06T00:39:50.3066667+03:00",
                    "Size": "715",
                    "SizeName": "BYTE",
                    "NumberOfShows": 0
                },
                {
                    "Id": 23,
                    "Guid": "76e8f461-0913-4702-96f6-b61df9a10d2c",
                    "Name": "edit.png",
                    "SaveDateTime": "2023-05-06T00:46:53.4266667+03:00",
                    "Size": "450",
                    "SizeName": "BYTE",
                    "NumberOfShows": 0
                },
                {
                    "Id": 24,
                    "Guid": "8a8b876c-81f6-4987-8f6d-4c975a4eddeb",
                    "Name": "add.png",
                    "SaveDateTime": "2023-05-06T18:57:22.39+03:00",
                    "Size": "733",
                    "SizeName": "BYTE",
                    "NumberOfShows": 0
                },
                {
                    "Id": 25,
                    "Guid": "eee7b757-51f1-4cbc-b31f-047e442e15e0",
                    "Name": "20201231_143332.jpg",
                    "SaveDateTime": "2023-05-07T22:11:28.4866667+03:00",
                    "Size": "1923676",
                    "SizeName": "BYTE",
                    "NumberOfShows": 0
                }
            ],
            "Photos@odata.nextLink": "https://localhost:5001/api/Users/1/Photos?$skiptoken=Id-25"
        }
    ]
}

Here it is. @julealgon

julealgon commented 1 year ago

Thanks @mansurdegirmenci .

As I suspected, that next_page link is for the inner "photos" collection of the first user, and not for the external "users" one. Thus, it actually looks correct to me.

This is evident because it is called Photos@odata.nextLink, and not just @odata.nextLink which is the global one.

Are you sure you fully understand the idea behind server-driven paging? If you want the "users" to be paginated, then your response above would need over 10 user elements. What you have now is a single user entry, with over 10 photo child elements. Note that the skiptoken value is referring to the "Photo"s Id and not to the "User"'s Id there.

mansurdegirmenci commented 1 year ago

Thank you for your explanation, I understand what you are saying.. but I still think the generated nextlink is wrong. because it doesn't work. Could I have made a mistake in the "edm model" ?

image

julealgon commented 1 year ago

but I still think the generated nextlink is wrong.

Do you have the necessary endpoint to handle the request though @mansurdegirmenci ? OData will generate the link assuming that you have that endpoint in place.

You need to provide an action to handle the users/{id}/photos route for this to work as intended. You have a Photos controller but that won't be called by this particular route: what you need is a "photos" route in the users controller that finds the user, then returns it's photos.

Check the documentation on how to create the nested property endpoint if you are not aware of how it works.

gathogojr commented 1 year ago

@mansurdegirmenci Like @julealgon pointed out, you need to have an endpoint that is mapped to the /api/Users/{key}/Photos route template. If you're using conventional routing and you have a UsersController implemented:

Alternatively, if you're using attribute routing place the route template on the HttpGet attribute as follows:

[HttpGet("api/Users/{key}/Photos")]
public ActionResult<IEnumerable<Photo>> YourDesiredControllerActionName(int key)
{
    // ...
}
mansurdegirmenci commented 1 year ago

@gathogojr Thank you for the explanatory information.

UsersController:

public class UsersController : ODataController
{
    #region ctor

    private readonly ILogger<UsersController> _logger;

    private readonly IMapper _mapper;

    private readonly UserManager<AspNetUser> _userManager;

    private readonly DataListContext _dataListContext;

    public UsersController(ILogger<UsersController> logger,
        IMapper mapper,
        UserManager<AspNetUser> userManager,
        DataListContext dataListContext)
    {
        _mapper = mapper;
        _userManager = userManager;
        _logger = logger;
        _dataListContext = dataListContext;
    }

    #endregion

    [HttpGet, EnableQuery(PageSize = 10)]
    public async Task<IActionResult> Get(ODataQueryOptions<UserDto> options)
    {
        var query = _userManager.Users.GetQuery(_mapper, options);
        var result = await query.ToListAsync();
        return Ok(result);
    }

    [HttpGet, EnableQuery]
    public async Task<IActionResult> Get([FromODataUri] int key, ODataQueryOptions<UserDto> options)
    {
        var user = await _userManager.Users
            .GetQuery(_mapper, options)
            .SingleOrDefaultAsync(x => x.Id.Equals(key));

        return Ok(user);
    }

    [HttpGet, EnableQuery(PageSize = 10)]
    public async Task<IActionResult> GetPhotos([FromODataUri] int key, ODataQueryOptions<PhotoDto> options)
    {
        var query = _dataListContext.Photos
            .Where(x => x.UserId == key)
            .GetQuery(_mapper, options);

        var result = await query.ToListAsync();

        return Ok(result);
    }
}

EdmModelBuilder:

public static class EdmModelBuilder
{
    internal static IEdmModel GetEdmModelv1()
    {
        var builder = new ODataConventionModelBuilder();

        #region UserDto

        var userDto = builder.EntitySet<UserDto>("Users").EntityType;

        userDto.Function("Get")
            .ReturnsFromEntitySet<UserDto>("Users")
            .Parameter<int>("key").Required();

        userDto.Function("GetPhotos")
            .ReturnsFromEntitySet<UserDto>("Users")
            .Parameter<int>("key").Required();

        #endregion

        #region PhotoDto

        var photoDto = builder.EntitySet<PhotoDto>("Photos").EntityType;

        photoDto.Function("Get")
            .ReturnsFromEntitySet<PhotoDto>("Photos")
            .Parameter<int>("key").Required();

        photoDto.Collection.Function("Download")
            .Returns<IActionResult>()
            .Parameter<int>("id").Required();

        photoDto.Collection.Action("Upload")
            .Returns<IActionResult>();

        #endregion

        return builder.GetEdmModel();
    }
}

this is how i updated the settings. it works but I hope I spelled it right :) I shared it to be useful.

gathogojr commented 1 year ago

@mansurdegirmenci I'm glad to hear that it worked. You don't need to add the following functions:

        userDto.Function("Get")
            .ReturnsFromEntitySet<UserDto>("Users")
            .Parameter<int>("key").Required();

        userDto.Function("GetPhotos")
            .ReturnsFromEntitySet<UserDto>("Users")
            .Parameter<int>("key").Required();

        photoDto.Function("Get")
            .ReturnsFromEntitySet<PhotoDto>("Photos")
            .Parameter<int>("key").Required();

The Get and GetPhotos controller actions in UsersController together with the Get controller action in PhotosController are automatically and conventionally mapped to the respective route templates. Your service should work just fine after you remove the 3 statements in the code snippet above.

Download function and Upload action configurations you should retain. The following would do the job:

public static class EdmModelBuilder
{
    internal static IEdmModel GetEdmModelv1()
    {
        var builder = new ODataConventionModelBuilder();

        #region UserDto

        builder.EntitySet<UserDto>("Users");

        #endregion

        #region PhotoDto

        var photoDto = builder.EntitySet<PhotoDto>("Photos").EntityType;

        photoDto.Collection.Function("Download")
            .Returns<IActionResult>()
            .Parameter<int>("id").Required();

        photoDto.Collection.Action("Upload")
            .Returns<IActionResult>();

        #endregion

        return builder.GetEdmModel();
    }
}

In your controller actions, I'd advise you not to use both EnableQuery and ODataQueryOptions<T> parameter. Use one or the other. By using them together, you're applying the same query options twice. The PageSize you're setting from the EnableQuery attribute can be set by passing a ODataQuerySettings parameter to the ApplyTo method of ODataQueryOptions parameter.

mansurdegirmenci commented 1 year ago

Thanks for bringing up some good topics. AutoMapper.Extensions.OData mentions the same thing on github. I made a few edits, but of course there are some errors.

[HttpGet]
    public async Task<IActionResult> Get(ODataQueryOptions<UserDto> options)
    {
        QuerySettings querySettings = new() { ODataSettings = new ODataSettings { PageSize = 10 } };
        var result = await _userManager.Users.GetQueryAsync(_mapper, options, querySettings);
        return Ok(result);
    }

It's acting like I have 10 users. I have 1 user. Of course, when you go to the other created page, it comes blank, but I think it's a problem that it creates a nextlink.

image

brings new errors :) There was a problem requesting $count. Microsoft.OData.ODataException: The value of type 'Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable`1[[WebUI.Dto.Server.UserDto, WebUI, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]]' could not be converted to a raw string.

image

that's all for now :)


Thank you for your knowledge and help. I have some questions with Odata, maybe you can help.

image

My first question is: Why are there methods marked as "default" in the "upload and download" methods I added? and how can I make my download method as download/16?

public class PhotosController : ODataController
{
    #region ctor

    private readonly ILogger<PhotosController> _logger;

    private readonly IWebHostEnvironment _webHostEnvironment;

    private readonly IMapper _mapper;

    private readonly DataListContext _dataListContext;

    public PhotosController(ILogger<PhotosController> logger,
        IWebHostEnvironment webHostEnvironment,
        IMapper mapper,
        DataListContext dataListContext)
    {
        this._logger = logger;
        this._webHostEnvironment = webHostEnvironment;
        this._mapper = mapper;
        this._dataListContext = dataListContext;
    }

    #endregion

    [HttpGet]
    public async Task<IActionResult> Get(ODataQueryOptions<PhotoDto> options) =>
        Ok(await _dataListContext.Photos.GetQueryAsync(_mapper, options));

    [HttpGet]
    public async Task<IActionResult> Get([FromODataUri] int key, ODataQueryOptions<PhotoDto> options) =>
        Ok(await _dataListContext.Photos.GetQuery(_mapper, options).SingleOrDefaultAsync(x => x.Id.Equals(key)));

    [HttpGet]
    public async Task<IActionResult> Download([FromODataUri] int id)
    {
        _logger.LogInformation("Running photos controller in download method");

        var photo = await _dataListContext.Photos.FindAsync(id);

        if (photo == null)
        {
            _logger.LogInformation("Photo is not found in list");
            return NotFound("Photo is not found in list");
        }

        var path = Path.Combine(_webHostEnvironment.WebRootPath, "images", photo.OrijinalName);

        if (!System.IO.File.Exists(path))
        {
            _logger.LogInformation("Photo file not found");
            return NotFound("Photo file not found");
        }

        _logger.LogInformation("Photo is found");
        return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
    }

    [HttpPost]
    public async Task<IActionResult> Upload()
    {
        _logger.LogInformation("Running photos controller in upload method");

        if (Request.Form.Files.Count == 0)
        {
            return Conflict("No files were found in the request");
        }

        //var feature = HttpContext.Features.Get<IAnonymousIdFeature>();
        //_logger.LogInformation("Photo uploading for {AnonymousId}", feature.AnonymousId);

        var files = Request.Form.Files;
        var path = Path.Combine(_webHostEnvironment.WebRootPath, "images");
        var photos = new List<Models.Photo>();

        foreach (var file in files)
        {
            if (file is null || file.Length == 0)
            {
                continue;
            }

            var fileName = file.FileName;
            var fileSize = file.Length;
            var fileNameWithPath = Path.Combine(path, fileName);

            await using (var stream = new FileStream(fileNameWithPath, FileMode.Create))
            {
                await file.CopyToAsync(stream);
            }

            _logger.LogInformation("The photo has been uploaded to the server. Photo name: {FileName}", fileName);

            photos.Add(new Models.Photo
            {
                OrijinalName = fileName,
                ChangedName = "DÜZELTİLECEK",
                Size = fileSize.ToString(),
                SizeName = SizeName.BYTE,
                //UserId = feature.AnonymousId
            });
        }

        await _dataListContext.Photos.BulkInsertAsync(photos);
        await _dataListContext.SaveChangesAsync();

        return Ok();
    }
}

and my last question is :)

The EntityTypeName on the "api/$metadata" page is referred to as "UserDto and PhotoDto". yes, i used naming like that. but a front-end developer will see this, why would he see that I'm doing a DTO? Let TypeName be Users or Photos. I think ODataConventionModelBuilder can be set, but is there an alternative more useful way?

image

Thank you for taking the time for me. @gathogojr

julealgon commented 1 year ago

@mansurdegirmenci do you think you could start separate issues for the separate questions instead of posting them all here? Otherwise it will derail the original issue here a bit too much and make it harder for other folks searching for specific answers later.

Hopefully that makes sense.