Example E965
Visible to All Users

How to: Store file attachments in the file system instead of the database (XPO and EF Core)

Basics

This example contains a FileSystemDataModule - an XAF module that you can reuse in your own applications. You will be interested in two types that allow you to attach a file to an object. Both types do not store the files in the database, but keep them in the file system:

  • FileSystemStoreObject - stores files in a centralized file system location instead of the database. Use the module's static FileSystemStoreLocation property to specify the file location.
  • FileSystemLinkObject - stores soft links to files instead of saving their contents to the database. This type is intended for use in WinForms applications only.

Both types implement the IFileData interface and thus work with the DevExpress File Attachments module.

Refer to the following video to see this functionality in action: http://www.screencast.com/t/Xl1GMfxw.

Steps to implement

  1. Copy the "FileSystemData" project and include it into your solution. Rebuild the solution and make sure the new project was built successfully.
  2. Register the FileSystemDataModule in the YourSolutionName.Module/Module.xx file as described in the following help topic: Ways to Register a Module > Add a Module in Code.
  3. Extend a business class with a property that will hold file attachments. Choose the FileSystemStoreObject or FileSystemLinkObject type for the property. See descriptions above to compare the two types.
  4. Add the following attributes to the file attachment property: Aggregated, ExpandObjectMembers(ExpandObjectMembers.Never) and ImmediatePostData.
  5. Decorate the business class with the FileAttachmentAttribute that links to the new property. This attribute enables additional file management UI commands.
  6. Handle the CustomOpenFileWithDefaultProgram event of the DevExpress.ExpressApp.FileAttachments.Win.FileAttachmentsWindowsFormsModule class. Review the implementation in this example (E965.Win\WinApplication.xx). Move that code to the following file in your project: YourSolutionName.Win/WinApplication.xx.
  7. XPO Only. Make sure that your application and related modules do not override the DevExpress.Persistent.BaseImpl.BaseObject.OidInitializationMode property. The property must return OidInitializationMode.AfterConstruction to ensure the correct operation of this module. (FileSystemDataModule sets this value as needed.)

Important Notes

  1. The FileSystemLinkObject class can be used in WinForms applications only.
  2. The current version of this example does not support the middle-tier scenario. For more information, refer to the following ticket: A problem occurs when file attachments are stored in the file system with middle-tier configuration.
  3. If you plan to migrate existing FileData objects from the database to a file system, use the techniques described in the following topic: Data Manipulation and Business Logic. The article explains how you can read FileData objects and create new FileSystemStoreObject objects based on their content. Since both classes implement IFileData, you can call their LoadFromStream() and SaveToStream() methods to copy data. Even though we do not provide an example for this migration procedure, we hope that the snippet below help you get started:
C#
// Use any other IObjectSpace APIs to query required data. FileData fd = ObjectSpace.FindObject<FileData>(null); FileSystemStoreObject fss = ObjectSpace.CreateObject<FileSystemStoreObject>(); Stream sourceStream = new MemoryStream(); ((IFileData)fd).SaveToStream(sourceStream); sourceStream.Position = 0; ((IFileData)fss).LoadFromStream(fd.FileName, sourceStream); ObjectSpace.CommitChanges();

Note that you can rework this code to use UnitOfWork instead of IObjectSpace.

Files to Review (XPO)

Files to Review (EF Core)

Documentation

See Also

Does this example address your development requirements/objectives?

(you will be redirected to DevExpress.com to submit your response)

Example Code

XPO/E965/FileSystemData/BusinessObjects/FileSystemLinkObject.cs
C#
using System; using System.IO; using DevExpress.Xpo; using DevExpress.ExpressApp; using System.ComponentModel; using DevExpress.Persistent.Base; using DevExpress.Persistent.BaseImpl; using DevExpress.Persistent.Validation; namespace FileSystemData.BusinessObjects { /// <summary> /// This class enables you to add soft links to real files instead of saving their contents to the database. It is intended for use in Windows Forms applications only. /// </summary> [DefaultProperty(nameof(FileName))] public class FileSystemLinkObject : BaseObject, IFileData, IEmptyCheckable, ISupportFullName { public FileSystemLinkObject(Session session) : base(session) { } #region IFileData Members [Size(260), Custom("AllowEdit", "False")] public string FileName { get { return GetPropertyValue<string>(nameof(FileName)); } set { SetPropertyValue(nameof(FileName), value); } } void IFileData.Clear() { Size = 0; FileName = string.Empty; } //Dennis: Fires when uploading a file. void IFileData.LoadFromStream(string fileName, Stream source) { Size = (int)source.Length; FileName = fileName; } //Dennis: Fires when saving or opening a file. void IFileData.SaveToStream(Stream destination) { try { if (destination == null) FileSystemDataModule.OpenFileWithDefaultProgram(FullName); else FileSystemDataModule.CopyFileToStream(FullName, destination); } catch (Exception exc) { throw new UserFriendlyException(exc); } } [Persistent] public int Size { get { return GetPropertyValue<int>(nameof(Size)); } private set { SetPropertyValue(nameof(Size), value); } } #endregion #region IEmptyCheckable Members public bool IsEmpty { get { return !File.Exists(FullName); } } #endregion #region ISupportFullName Members [Size(260), Custom("AllowEdit", "False")] public string FullName { get { return GetPropertyValue<string>(nameof(FullName)); } set { SetPropertyValue(nameof(FullName), value); } } #endregion } }
XPO/E965/FileSystemData/BusinessObjects/FileSystemStoreObject.cs
C#
using System; using System.IO; using DevExpress.Xpo; using DevExpress.ExpressApp; using System.ComponentModel; using DevExpress.Persistent.Base; using DevExpress.ExpressApp.Utils; using DevExpress.Persistent.BaseImpl; using DevExpress.Persistent.Validation; namespace FileSystemData.BusinessObjects { /// <summary> /// This class enables you to store uploaded files in a centralized file system location instead of the database. You can configure the file system store location via the static FileSystemDataModule.FileSystemStoreLocation property. /// </summary> [DefaultProperty(nameof(FileName))] public class FileSystemStoreObject : BaseObject, IFileData, IEmptyCheckable { private Stream tempSourceStream; private string tempFileName = string.Empty; private static object syncRoot = new object(); public FileSystemStoreObject(Session session) : base(session) { } public string RealFileName { get { if (!string.IsNullOrEmpty(FileName) && Oid != Guid.Empty) return Path.Combine(FileSystemDataModule.FileSystemStoreLocation, string.Format("{0}-{1}", Oid, FileName)); return null; } } protected virtual void SaveFileToStore() { if(!string.IsNullOrEmpty(RealFileName) && TempSourceStream != null) { try { using(Stream destination = File.Create(RealFileName)) { //T582918 FileSystemDataModule.CopyStream(TempSourceStream, destination); Size = (int)destination.Length; } } catch(DirectoryNotFoundException exc) { throw new UserFriendlyException(exc); } } } private void RemoveOldFileFromStore() { //Dennis: We need to remove the old file from the store when saving the current object. if (!string.IsNullOrEmpty(tempFileName) && tempFileName != RealFileName) {//B222892 try { File.Delete(tempFileName); tempFileName = string.Empty; } catch (DirectoryNotFoundException exc) { throw new UserFriendlyException(exc); } } } protected override void OnSaving() { base.OnSaving(); Guard.ArgumentNotNullOrEmpty(FileSystemDataModule.FileSystemStoreLocation, "FileSystemStoreLocation"); lock (syncRoot) { if (!Directory.Exists(FileSystemDataModule.FileSystemStoreLocation)) Directory.CreateDirectory(FileSystemDataModule.FileSystemStoreLocation); } SaveFileToStore(); RemoveOldFileFromStore(); } protected override void OnDeleting() { //Dennis: We need to remove the old file from the store. Clear(); base.OnDeleting(); } protected override void Invalidate(bool disposing) { if (disposing && TempSourceStream != null) { TempSourceStream.Close(); TempSourceStream = null; } base.Invalidate(disposing); } #region IFileData Members public void Clear() { //Dennis: When clearing the file name property we need to save the name of the old file to remove it from the store in the future. You can also setup a separate service for that. if (string.IsNullOrEmpty(tempFileName)) tempFileName = RealFileName; FileName = string.Empty; Size = 0; } [Size(260)] public string FileName { get { return GetPropertyValue<string>(nameof(FileName)); } set { SetPropertyValue(nameof(FileName), value); } } [Browsable(false)] public Stream TempSourceStream { get { return tempSourceStream; } set { //Michael: The original Stream might be closed after a while (on the web too - T160753) if (value == null) { tempSourceStream = null; } else { if (value.Length > (long)int.MaxValue) throw new UserFriendlyException("File is too long"); tempSourceStream = new MemoryStream((int)value.Length); FileSystemDataModule.CopyStream(value, tempSourceStream); tempSourceStream.Position = 0; } } } //Dennis: Fires when uploading a file. void IFileData.LoadFromStream(string fileName,Stream source) { //Dennis: When assigning a new file we need to save the name of the old file to remove it from the store in the future. if(fileName != FileName) {// updated, old code was: if (string.IsNullOrEmpty(tempFileName)) tempFileName = RealFileName; } FileName = fileName; TempSourceStream = source; Size = (int)TempSourceStream.Length; OnChanged();//T582918 } //Dennis: Fires when saving or opening a file. void IFileData.SaveToStream(Stream destination) { try { if(!File.Exists(RealFileName)) { return; } if (!string.IsNullOrEmpty(RealFileName)) { if (destination == null) FileSystemDataModule.OpenFileWithDefaultProgram(RealFileName); else FileSystemDataModule.CopyFileToStream(RealFileName, destination); } else if (TempSourceStream != null) FileSystemDataModule.CopyStream(TempSourceStream, destination); } catch (DirectoryNotFoundException exc) { throw new UserFriendlyException(exc); } catch (FileNotFoundException exc) { throw new UserFriendlyException(exc); } } [Persistent] public int Size { get { return GetPropertyValue<int>(nameof(Size)); } private set { SetPropertyValue(nameof(Size), value); } } #endregion #region IEmptyCheckable Members public bool IsEmpty { //T153149 get { return FileDataHelper.IsFileDataEmpty(this) || !(TempSourceStream!= null || File.Exists(RealFileName)); } } #endregion } }
EFCore/FileSystemData/BusinessObjects/FileSystemLinkObject.cs
C#
using System; using System.IO; using DevExpress.ExpressApp; using System.ComponentModel; using DevExpress.Persistent.Base; using DevExpress.Persistent.BaseImpl.EF; using DevExpress.Persistent.Validation; using DevExpress.ExpressApp.DC; namespace FileSystemData.BusinessObjects { /// <summary> /// This class enables you to add soft links to real files instead of saving their contents to the database. It is intended for use in Windows Forms applications only. /// </summary> [DefaultProperty(nameof(FileName))] public class FileSystemLinkObject : BaseObject, IFileData, IEmptyCheckable, ISupportFullName { #region IFileData Members [FieldSize(260)] [ReadOnly(true)] public virtual string FileName { get; set; } void IFileData.Clear() { Size = 0; FileName = string.Empty; } //Dennis: Fires when uploading a file. void IFileData.LoadFromStream(string fileName, Stream source) { Size = (int)source.Length; FileName = fileName; } //Dennis: Fires when saving or opening a file. void IFileData.SaveToStream(Stream destination) { try { if (destination == null) FileSystemDataModule.OpenFileWithDefaultProgram(FullName); else FileSystemDataModule.CopyFileToStream(FullName, destination); } catch (Exception exc) { throw new UserFriendlyException(exc); } } public virtual int Size { get; set; } #endregion #region IEmptyCheckable Members public bool IsEmpty { get { return !File.Exists(FullName); } } #endregion #region ISupportFullName Members [FieldSize(260)] [ReadOnly(true)] public virtual string FullName { get; set; } #endregion } }
EFCore/FileSystemData/BusinessObjects/FileSystemStoreObject.cs
C#
using System; using System.IO; using DevExpress.ExpressApp; using System.ComponentModel; using DevExpress.Persistent.Base; using DevExpress.ExpressApp.Utils; using DevExpress.Persistent.BaseImpl.EF; using DevExpress.Persistent.Validation; using DevExpress.ExpressApp.DC; using System.ComponentModel.DataAnnotations.Schema; namespace FileSystemData.BusinessObjects { /// <summary> /// This class enables you to store uploaded files in a centralized file system location instead of the database. You can configure the file system store location via the static FileSystemDataModule.FileSystemStoreLocation property. /// </summary> [DefaultProperty(nameof(FileName))] public class FileSystemStoreObject : BaseObject, IFileData, IEmptyCheckable { private Stream tempSourceStream; private string tempFileName = string.Empty; private static object syncRoot = new object(); public string RealFileName { get { if (!string.IsNullOrEmpty(FileName) && ID != Guid.Empty) return Path.Combine(FileSystemDataModule.FileSystemStoreLocation, string.Format("{0}-{1}", ID, FileName)); return null; } } protected virtual void SaveFileToStore() { if(!string.IsNullOrEmpty(RealFileName) && TempSourceStream != null) { try { using(Stream destination = File.Create(RealFileName)) { //T582918 FileSystemDataModule.CopyStream(TempSourceStream, destination); Size = (int)destination.Length; } } catch(DirectoryNotFoundException exc) { throw new UserFriendlyException(exc); } } } private void RemoveOldFileFromStore() { //Dennis: We need to remove the old file from the store when saving the current object. if (!string.IsNullOrEmpty(tempFileName) && tempFileName != RealFileName) {//B222892 try { File.Delete(tempFileName); tempFileName = string.Empty; } catch (DirectoryNotFoundException exc) { throw new UserFriendlyException(exc); } } } public override void OnSaving() { base.OnSaving(); if (!ObjectSpace.IsObjectToDelete(this)) { Guard.ArgumentNotNullOrEmpty(FileSystemDataModule.FileSystemStoreLocation, "FileSystemStoreLocation"); lock (syncRoot) { if (!Directory.Exists(FileSystemDataModule.FileSystemStoreLocation)) Directory.CreateDirectory(FileSystemDataModule.FileSystemStoreLocation); } SaveFileToStore(); RemoveOldFileFromStore(); } else { Clear(); } } #region IFileData Members public void Clear() { //Dennis: When clearing the file name property we need to save the name of the old file to remove it from the store in the future. You can also setup a separate service for that. if (string.IsNullOrEmpty(tempFileName)) tempFileName = RealFileName; FileName = string.Empty; Size = 0; } [FieldSize(260)] public virtual string FileName { get; set; } [Browsable(false)] [NotMapped] public Stream TempSourceStream { get { return tempSourceStream; } set { //Michael: The original Stream might be closed after a while (on the web too - T160753) if (value == null) { tempSourceStream = null; } else { if (value.Length > (long)int.MaxValue) throw new UserFriendlyException("File is too long"); tempSourceStream = new MemoryStream((int)value.Length); FileSystemDataModule.CopyStream(value, tempSourceStream); tempSourceStream.Position = 0; } } } //Dennis: Fires when uploading a file. void IFileData.LoadFromStream(string fileName,Stream source) { //Dennis: When assigning a new file we need to save the name of the old file to remove it from the store in the future. if(fileName != FileName) {// updated, old code was: if (string.IsNullOrEmpty(tempFileName)) tempFileName = RealFileName; } FileName = fileName; TempSourceStream = source; Size = (int)TempSourceStream.Length; } //Dennis: Fires when saving or opening a file. void IFileData.SaveToStream(Stream destination) { try { if (!File.Exists(RealFileName)) { return; } if (!string.IsNullOrEmpty(RealFileName)) { if (destination == null) FileSystemDataModule.OpenFileWithDefaultProgram(RealFileName); else FileSystemDataModule.CopyFileToStream(RealFileName, destination); } else if (TempSourceStream != null) FileSystemDataModule.CopyStream(TempSourceStream, destination); } catch (DirectoryNotFoundException exc) { throw new UserFriendlyException(exc); } catch (FileNotFoundException exc) { throw new UserFriendlyException(exc); } } public virtual int Size { get; set; } #endregion #region IEmptyCheckable Members public bool IsEmpty { //T153149 get { return FileDataHelper.IsFileDataEmpty(this) || !(TempSourceStream!= null || File.Exists(RealFileName)); } } #endregion } }

Disclaimer: The information provided on DevExpress.com and affiliated web properties (including the DevExpress Support Center) is provided "as is" without warranty of any kind. Developer Express Inc disclaims all warranties, either express or implied, including the warranties of merchantability and fitness for a particular purpose. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.

Confidential Information: Developer Express Inc does not wish to receive, will not act to procure, nor will it solicit, confidential or proprietary materials and information from you through the DevExpress Support Center or its web properties. Any and all materials or information divulged during chats, email communications, online discussions, Support Center tickets, or made available to Developer Express Inc in any manner will be deemed NOT to be confidential by Developer Express Inc. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.