最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

c# - EF Core storedcomputed property query database - Stack Overflow

programmeradmin2浏览0评论

I'm trying to migrate my C# project from ASP.NET with EF to ASP.NET Core MVC and EF Core. I'm self taught and I'm the only person who works on this project so my project architecture and standards I've been working to could be slightly off.

My models typically look like this:

public class Quotation
{
    public int ID { get; set; }

    public int QuotationReference { get; set; }

    [Display(Name = "Version")]
    public int VersionNo { get; set; }

    public string VersionDisplay
    {
        get
        {
            if (_context.Quotations.Where(s => s.QuotationReference == QuotationReference).OrderByDescending(s => s.VersionNo).Count() > 0)
            {
                return VersionNo.ToString() + "/" + _context.Quotations.Where(s => s.QuotationReference == QuotationReference).OrderByDescending(s => s.VersionNo).Select(s => s.VersionNo).First();
            }
            return "";
        }
    }
}

Since moving to EF Core, the _context is null when this tries to run. How can I have calculated fields which query the database within my models?

If I shouldn't be having calculated properties within my model where should I be doing these calculations within my architecture?

Most of my controllers look something along the lines of the below when I query the database and then load the results into a view model.

public ActionResult Create()
{
    EmployeeHolidaysController EHC = new EmployeeHolidaysController(_context,_userManager,_hostingEnvironment);
    EHC.EmployeeHolidayMaintance();

    EmployeeViewModel vm = new EmployeeViewModel();
    vm.User = _context.Users.Find(_userManager.GetUserId(User));
    vm.Employee = new Employee();
    vm.ProductionDepartments = new SelectList(_context.Departments, "ID", "DepartmentName", vm.Employee.ProductionDepartmentID);
    vm.Employee.DateOfBirth = DateTime.Now.AddYears(-20);
    vm.Employee.EmployementStartDate = DateTime.Now;

    return View(vm);
}

Where should I be putting these calculated properties? I'd ideally like them to calculate at runtime as there may have been other data added since the individual record have been saved.

I'm trying to migrate my C# project from ASP.NET with EF to ASP.NET Core MVC and EF Core. I'm self taught and I'm the only person who works on this project so my project architecture and standards I've been working to could be slightly off.

My models typically look like this:

public class Quotation
{
    public int ID { get; set; }

    public int QuotationReference { get; set; }

    [Display(Name = "Version")]
    public int VersionNo { get; set; }

    public string VersionDisplay
    {
        get
        {
            if (_context.Quotations.Where(s => s.QuotationReference == QuotationReference).OrderByDescending(s => s.VersionNo).Count() > 0)
            {
                return VersionNo.ToString() + "/" + _context.Quotations.Where(s => s.QuotationReference == QuotationReference).OrderByDescending(s => s.VersionNo).Select(s => s.VersionNo).First();
            }
            return "";
        }
    }
}

Since moving to EF Core, the _context is null when this tries to run. How can I have calculated fields which query the database within my models?

If I shouldn't be having calculated properties within my model where should I be doing these calculations within my architecture?

Most of my controllers look something along the lines of the below when I query the database and then load the results into a view model.

public ActionResult Create()
{
    EmployeeHolidaysController EHC = new EmployeeHolidaysController(_context,_userManager,_hostingEnvironment);
    EHC.EmployeeHolidayMaintance();

    EmployeeViewModel vm = new EmployeeViewModel();
    vm.User = _context.Users.Find(_userManager.GetUserId(User));
    vm.Employee = new Employee();
    vm.ProductionDepartments = new SelectList(_context.Departments, "ID", "DepartmentName", vm.Employee.ProductionDepartmentID);
    vm.Employee.DateOfBirth = DateTime.Now.AddYears(-20);
    vm.Employee.EmployementStartDate = DateTime.Now;

    return View(vm);
}

Where should I be putting these calculated properties? I'd ideally like them to calculate at runtime as there may have been other data added since the individual record have been saved.

Share Improve this question edited Nov 20, 2024 at 17:08 marc_s 756k184 gold badges1.4k silver badges1.5k bronze badges asked Nov 20, 2024 at 15:42 Max BurridgeMax Burridge 1 3
  • 1 If _context is a member if the controller class, at what point do you instantiate it? Unless it is static it will not persist between separate controller requests. Also, how does the Quotation class get passed the instance? – Andy Hames Commented Nov 20, 2024 at 15:55
  • I understand that it doesn't work because it's never initiated, but I don't then know how to have a calulated property which queries the database. Or where I should be having calulated properties which I want to run at runtime. I hope that makes sense. – Max Burridge Commented Nov 20, 2024 at 16:04
  • IMHO, a readonly/aka/computed/aka/get-only property... should never "go back to another thing" to figure itself out. :: You can populate all items in your main-class that are then used to compute the computed property. for example, if you have a Person object, and it has LastName, FirstName, MiddleName, Suffix, you ~can create a "FullName" computed property that does all the voodoo to figure out spaces and maybe-commas for the full-name. but "reaching out" to another thing/service/dbcontext to create that property..not a great idea IMHO. populate the "parts", use parts to make computed. – granadaCoder Commented Nov 20, 2024 at 20:52
Add a comment  | 

2 Answers 2

Reset to default 0

In order to have a calculated field such as the property getter in the example above, you can use dependency injection to pass the instantiated _context instance to the Quotation class so that it is then available for the class' methods to use.

This would commonly be done as an argument to the class constructor, for exmaple:

public class Quotation
{
    public int ID { get; set; }
    public int QuotationReference { get; set; }

    [Display(Name = "Version")]
    public int VersionNo { get; set; }

    private readonly DbContext _context;

    public Quotation(DbContext context)
    {
        _context = context;
    }

    public string VersionDisplay
    {
        get
        {
            if (_context.Quotations.Where(s => s.QuotationReference == QuotationReference).OrderByDescending(s => s.VersionNo).Count() > 0)
            {
                return VersionNo.ToString() + "/" + _context.Quotations.Where(s => s.QuotationReference == QuotationReference).OrderByDescending(s => s.VersionNo).Select(s => s.VersionNo).First();
            }
            return "";
        }
    }
}

Although an alternative option would be to set a property on the class after it has been instantiated.

EDIT Following clarification in comments, if the code is auto-generated code (e.g. by EF) you could separate the logic into a method in a partial class and then pass the context as a parameter to that method, for example:

public partial class Quotation
{
    public string GetVersionDisplay(DbContext context)
    {
        if (context.Quotations.Where(s => s.QuotationReference == QuotationReference).OrderByDescending(s => s.VersionNo).Count() > 0)
        {
            return VersionNo.ToString() + "/" + context.Quotations.Where(s => s.QuotationReference == QuotationReference).OrderByDescending(s => s.VersionNo).Select(s => s.VersionNo).First();
        }
        return "";
    }
}

You should then be able to iterate your quotation list and call

quotation.GetVersionDisplay(_context)

Ideally from an architectural perspective, entities are a data domain object and that should be kept separate from view domain concerns. Displaying a current version (I.e. 1/1 vs 1/3 etc.) is a view concern as the data for a given row just knows "I am version 1" Passing a DbContext instance to a domain entity is something that should be avoided. It can lead to SELECT N+1 scenarios and gets messy since properties that "fetch" via the DB context and cannot work in expressions that will translate down to SQL, will be mixed as far as Intellisense is concerned. I.e. a VersionDisplay property in the entity could not be used in a query like .Where(x => x.VersionDisplay == "1/1") that tried fetching data without first materializing entities to memory.

If I am loading a list of quotations to display, I'd likely only be concerned with displaying each row's version since I'd be displaying each version and likely ordering them by version. (descending) The view model in that case would be a summarized version of the quotation, just the fields I want to display on the screen. For instance that would be the quotation reference, version number. I'd also want the PK so I can load a detail view. If the quotation record has 10 or 20 columns we don't need to load every column for every summary row, just the PK and what we want to display.

var quotations = await _context.Quotations
    .AsNoTracking()
    .OrderBy(x => x.QuotationReference)
    .ThenByDescending(x => x.VersionNumber)
    .Select(x => new QuotationSummaryViewModel
    {
        QuotationId = x.QuotationId,
        QuotationReference = x.QuotationReference,
        VerionNumber = x.VersionNumber
    }).ToListAsync();

When I display a particular row and want to display that this is row version 1/3 then that can be calculated when I fetch the QuotationDetailViewModel which would contain most, if not all columns from the Quotation table:

var quotationDetail = await _context.Quotations
    .AsNoTracking()
    .Where(x => x.QuotationId == quotationId)
    .Select(x => new QuotationDetailViewModel
    {
        QuotationId = x.QuotationId,
        QuotationReference = x.,
        VerionNumber = x.VersionNumber,
        // .. other columns to display, including related data...
        MaxVersion = _context.Quotations
            .Where(q => q.QuotationReference == x.QuotationReference)
            .Max(q => q.VersionNumber)
    }).SingleAsync();

The QuotationDetailViewModel class can expose a "DisplayVersion" property:

public string DisplayVersion => $"{Version}/{MaxVersion}";

... for the view to display in a bound UI element.

If your UI would like to be reactive to the fact that new versions might get added by other users then it can make a periodic Ajax fetch to get the max version given the QuotationReference and refresh the UI element for the display version. My bound UI element would use "data-" attributes on the display UI element for the quotationReference, version # and max version # so my JS timer or whatever event handler would have the applicable values to call back, compare, and compose a new display string:

public IActionResult GetMaxQuotationVersion(int quotationReference)
{
    int maxVersion = _context.Quotations
        .Where(x => x.QuotationReference == quotationReference)
        .Max(x => x.VersionNumber);
    return Json(new { MaxVersionNumber = maxVersion });
}

.. or similar, where the polling JS method refreshes the displayed version if the max version # comes back different. Alternatively the app could keep a running static/shared cache of quotations and versions that is refreshed on-demand or as versions are added to avoid unnecessary DB requests each refresh.

The main point would be to keep the max version a UI-based concern rather than a domain one for the entity to worry about.

发布评论

评论列表(0)

  1. 暂无评论